Back to Blog
critical SEVERITY7 min read

How unsafe token deserialization happens in Node.js Temml parser and how to fix it

A critical vulnerability in the Temml math library's parser allowed unsafe token deserialization that could lead to remote code execution when processing user-supplied mathematical expressions. The fix adds strict type validation on fetched token properties before use, preventing exploitation of malformed or crafted payloads.

O
By Orbis AppSec
Published July 22, 2026Reviewed July 22, 2026

Answer Summary

This is an unsafe deserialization vulnerability (CWE-502) in Node.js affecting the Temml math library at libs/temml/temml.cjs:2837. The parser.fetch() function returns tokens that may originate from user input without validation. The fix adds explicit type checking to ensure the fetched token's text property is a string before accessing it, preventing attackers from injecting malicious objects that could be exploited during deserialization.

Vulnerability at a Glance

cweCWE-502 (Deserialization of Untrusted Data)
fixAdd type checking to validate token.text is a string before dereferencing
riskRemote code execution when processing malicious mathematical expressions
languageNode.js/JavaScript
root causeparser.fetch() returns unvalidated tokens that could be crafted malicious objects
vulnerabilityUnsafe token deserialization in parser

How unsafe token deserialization happens in Node.js Temml parser and how to fix it

Understanding the Vulnerability

In the Temml math library's production codebase, a critical vulnerability existed in how the parser handled tokens fetched during mathematical expression parsing. At line 2837 of libs/temml/temml.cjs, the parseCD() function called parser.fetch() to retrieve the next token in the parsing stream, then immediately accessed its .text property without validating that the token was a legitimate string-based object.

This seemingly innocent code pattern created a dangerous window for exploitation:

const next = parser.fetch().text;

The vulnerability arises because parser.fetch() returns tokens that may ultimately originate from user-supplied mathematical expressions. If an attacker could craft a malicious payload and inject it into the parsing stream, they could potentially return an object with a specially crafted .text property. When this untrusted object was deserialized and its properties accessed, it could trigger arbitrary code execution in the Node.js runtime environment.

Why This Matters for Temml Users

The Temml library is used to parse and render mathematical notation in web applications. Any application accepting user-submitted mathematical expressions through forms, APIs, or file uploads becomes a potential attack vector. A containerized service running Temml with network exposure could allow remote attackers to execute code on the server by submitting specially crafted mathematical expressions.

The Vulnerability Explained

The Specific Attack Vector

Let's trace through how this vulnerability could be exploited:

  1. Attack Entry Point: An attacker submits a malicious mathematical expression through a web form or API endpoint that uses Temml
  2. Parser Processing: The expression reaches parseCD() function at line 2837
  3. Unsafe Token Access: parser.fetch() returns an object that the attacker has influenced
  4. Property Dereference: The code immediately accesses .text without checking if the returned value is actually a legitimate token object
  5. Exploitation: The attacker's crafted object could contain a .text property that, when accessed or processed further, triggers code execution

The problematic code snippet from the original vulnerability:

function parseCD(parser) {
    parsedRows.push(parser.parseExpression(false, "\\\\"));
    parser.gullet.endGroup();
    parser.gullet.beginGroup();
    const next = parser.fetch().text;  // ← UNSAFE: No validation of fetch() return value
    if (next === "&" || next === "\\\\") {
      parser.consume();
    } else if (next === "\\end") {
      // ... rest of logic
    }
}

The vulnerability lies in the assumption that parser.fetch() always returns a valid token object with a .text property that is a string. In reality:

  • parser.fetch() could return null or undefined
  • parser.fetch() could return an object where .text is not a string
  • An attacker could potentially craft the parsing state to return a malicious object

Real-World Exploitation Scenario

Imagine a web application that accepts LaTeX-style mathematical notation and renders it using Temml:

// Vulnerable application code
app.post('/render-math', (req, res) => {
    const mathExpression = req.body.expression; // User input
    const html = temml.renderToString(mathExpression);
    res.send(html);
});

An attacker could craft a malicious expression that, when parsed by parseCD(), causes the parser to fetch a specially constructed object instead of a legitimate token. This could lead to:

  • Remote Code Execution: If the object's .text property is a getter function, it could execute arbitrary code
  • Prototype Pollution: Malicious objects could modify JavaScript prototypes, affecting all subsequent operations
  • Information Disclosure: Attackers could craft objects that expose sensitive information when properties are accessed

The Fix

The security team implemented a straightforward but critical fix: add explicit type validation before accessing the token's properties.

Before (Vulnerable)

const next = parser.fetch().text;

After (Secure)

const _fetchedToken = parser.fetch();
const next = (_fetchedToken && typeof _fetchedToken.text === "string") ? _fetchedToken.text : "";

What Changed and Why

The fix implements three layers of protection:

  1. Null/Undefined Check (_fetchedToken &&): Ensures that parser.fetch() actually returned an object before attempting property access. If it returns null or undefined, the expression short-circuits and assigns an empty string.

  2. Type Validation (typeof _fetchedToken.text === "string"): Explicitly verifies that the .text property is a string type. This prevents exploitation through objects with non-string .text properties or getter functions.

  3. Safe Default (? _fetchedToken.text : ""): If the token passes validation, use it; otherwise, use an empty string as a safe default that won't crash the parser or trigger unexpected behavior.

This approach ensures that:
- The parser continues functioning normally with legitimate tokens
- Malformed or malicious objects are safely rejected
- The code doesn't crash with a TypeError, but handles the error gracefully
- Subsequent logic that checks if (next === "&" || next === "\\\\") continues to work correctly with the safe default

Prevention & Best Practices

To avoid similar vulnerabilities in your own code:

1. Always Validate External Data

Any data that originates from outside your immediate scope (user input, API responses, parsed data) should be validated before use:

// Bad
const value = externalObject.property;

// Good
const value = (externalObject && typeof externalObject.property === "string") 
    ? externalObject.property 
    : defaultValue;

2. Use Schema Validation Libraries

For complex data structures, use validation libraries like Joi, Yup, or Zod:

const schema = Joi.object({
    text: Joi.string().required(),
    type: Joi.string().required()
});

const { error, value } = schema.validate(fetchedToken);
if (error) {
    throw new Error(`Invalid token: ${error.message}`);
}

3. Implement Type Guards in TypeScript

If using TypeScript, leverage the type system:

interface Token {
    text: string;
    type: string;
}

function isValidToken(obj: any): obj is Token {
    return obj && 
           typeof obj.text === "string" && 
           typeof obj.type === "string";
}

const fetchedToken = parser.fetch();
if (!isValidToken(fetchedToken)) {
    throw new Error("Invalid token structure");
}
const next = fetchedToken.text;

4. Use Static Analysis Tools

Enable security-focused linters and static analysis:

  • ESLint with security plugins: Use eslint-plugin-security to detect unsafe patterns
  • Semgrep: Run custom rules to detect property access without validation
  • SonarQube: Continuous code quality and security scanning

5. Apply Defense in Depth

Don't rely on a single validation point:

  • Validate at input boundaries (API endpoints)
  • Validate during parsing (as in this fix)
  • Validate before use in critical operations
  • Implement runtime monitoring to detect exploitation attempts

Key Takeaways

  • Never assume parser.fetch() returns a valid object: Always validate return values before accessing properties, especially when they originate from user input
  • Type checking is essential for security: Using typeof checks prevents attackers from injecting malicious objects with unexpected property types
  • The parseCD() function now safely handles malformed tokens: The fix ensures that invalid tokens default to empty strings rather than causing crashes or enabling exploitation
  • Deserialization vulnerabilities are subtle: They don't require obvious code patterns like eval() — simple property access on untrusted objects can be dangerous
  • Defense in depth matters: This single-point fix prevents exploitation, but applications should also validate input at API boundaries and implement runtime monitoring

How Orbis AppSec Detected This

Source: Mathematical expressions submitted by users through web forms or APIs that invoke Temml's renderToString() function, eventually reaching parser.fetch() in the token stream

Sink: The unsafe property dereference parser.fetch().text at line 2837 in libs/temml/temml.cjs, where an untrusted token object's properties are accessed without validation

Missing control: No type validation or null-safety checks on the return value of parser.fetch() before accessing its .text property. The code assumed all tokens would be legitimate string-based objects.

CWE: CWE-502 (Deserialization of Untrusted Data) — The vulnerability allows untrusted objects to be processed without validation, potentially leading to code execution

Fix: Added explicit type checking with (_fetchedToken && typeof _fetchedToken.text === "string") to ensure only valid string-based tokens are processed, with a safe empty string default for invalid tokens

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

The Temml parser vulnerability demonstrates how even simple, common operations like property access can become security risks when applied to untrusted data. By adding a small amount of defensive validation code, the security team eliminated a critical remote code execution vulnerability that could have affected every application using Temml to process user-submitted mathematical expressions.

The key lesson for developers: never assume external data is well-formed. Whether you're parsing mathematical notation, processing JSON, or handling any data from outside your immediate control, always validate type and structure before use. The few extra lines of validation code are a small price for preventing catastrophic security breaches.

As you review your own codebase, look for similar patterns where you access properties on objects returned from external sources without validation. These are prime candidates for security vulnerabilities.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #16

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.