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:
- Attack Entry Point: An attacker submits a malicious mathematical expression through a web form or API endpoint that uses Temml
- Parser Processing: The expression reaches
parseCD()function at line 2837 - Unsafe Token Access:
parser.fetch()returns an object that the attacker has influenced - Property Dereference: The code immediately accesses
.textwithout checking if the returned value is actually a legitimate token object - Exploitation: The attacker's crafted object could contain a
.textproperty 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 returnnullorundefinedparser.fetch()could return an object where.textis 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
.textproperty 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:
-
Null/Undefined Check (
_fetchedToken &&): Ensures thatparser.fetch()actually returned an object before attempting property access. If it returns null or undefined, the expression short-circuits and assigns an empty string. -
Type Validation (
typeof _fetchedToken.text === "string"): Explicitly verifies that the.textproperty is a string type. This prevents exploitation through objects with non-string.textproperties or getter functions. -
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-securityto 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
typeofchecks 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.