Feb 21, 2026·6 min read·6 visits
A critical RCE in math.js < 3.17.0 allows attackers to execute arbitrary system code. The vulnerability exploits a disparity between how the security validator reads strings vs. how the JS engine executes them. By masking forbidden property names (like 'constructor') with Unicode escapes (e.g., 'co\u006Estructor'), attackers can bypass the sandbox and pop a shell.
Math.js, the popular extensive math library for JavaScript and Node.js, suffered from a critical sandbox escape vulnerability. By utilizing Unicode escape sequences in object property keys, attackers could bypass the library's internal security blacklist. This allowed access to the `constructor` property, enabling the execution of arbitrary JavaScript code via the `Function` constructor. Essentially, the security guard was checking ID cards for 'Admin', but let 'Adm\u0069n' walk right through the front door.
We tend to trust math. It's absolute, deterministic, and safe. When a developer imports a library like math.js to handle complex expressions or user-defined formulas, they usually assume the worst-case scenario is a division by zero or maybe a resource exhaustion loop. They rarely expect that calculating 2 + 2 could result in a reverse shell opening up on their production server.
math.js is a beast of a library. It handles matrices, big numbers, and crucially, it comes with an expression parser that allows users to define variables and functions. To support this flexibility, the library implements a custom scope—a sandbox. The goal is simple: allow the user to do math, but don't let them touch the underlying JavaScript engine's sensitive internals, specifically the Object prototype chain.
But here is the thing about JavaScript sandboxes: they are like trying to hold water in your hands. Eventually, it leaks. In CVE-2017-1001003, the leak wasn't a complex memory corruption or a race condition. It was a linguistic misunderstanding between the security guard (the validator) and the executioner (the runtime). The validator only spoke ASCII, but the runtime was fluent in Unicode.
The core of the vulnerability lies in the isSafeProperty function. This function serves as the bouncer for the math.js expression evaluator. Its job is to check every property access or object definition against a blacklist of forbidden words. Naturally, this list includes the usual suspects: constructor, __proto__, prototype, and __defineGetter__. If you try to run math.eval('a.constructor'), the bouncer sees the word "constructor", panics, and throws an error. System secure. Good job, everyone.
However, the flaw is a classic example of Canonicalization Issues (CWE-20). The validator checked the raw string provided in the expression. If I type co\u006Estructor, the validator sees a string containing a backslash, a 'u', and some numbers. It compares co\u006Estructor against constructor. Strings are not equal. The bouncer waves it through.
But later down the pipeline, the JavaScript engine—which is compliant with the ECMAScript standard—looks at that same string. It sees the escape sequence \u006E and politely translates it into the character n. Suddenly, co\u006Estructor becomes constructor. The attacker has now accessed the forbidden property, effectively turning the "safe" math expression evaluator into a remote code execution engine.
Let's look at the code responsible for this mess. The vulnerability resided in lib/expression/node/ObjectNode.js, specifically where object properties were being processed.
The Vulnerable Code:
// The naive check
if (!isSafeProperty(node.properties, key)) {
throw new Error('No access to property "' + key + '"');
}Here, key is the raw identifier from the parsed expression tree. It hasn't been normalized. It is exactly what the user typed. If the user types Unicode escapes, key contains them literally.
The Fix (Commit a60f3c8):
The maintainer, Jos de Jong, realized that to validate input correctly, you must view the input exactly as the interpreter will view it. The fix forces the key to be "stringified" and then parsed back, which forces the resolution of Unicode escapes before the security check happens.
// The robust check
var stringifiedKey = stringify(key);
var parsedKey = JSON.parse(stringifiedKey);
// Now we check the resolved version of the key
if (!isSafeProperty(node.properties, parsedKey)) {
throw new Error('No access to property "' + parsedKey + '"');
}By round-tripping the key through JSON.parse(stringify(...)), co\u006Estructor is transformed into constructor before it hits isSafeProperty. The bouncer now recognizes the disguise.
Exploiting this is trivially elegant. We don't need buffer overflows or heap spraying. We just need to ask JavaScript to give us the Function constructor.
In JavaScript, if you have an object obj, obj.constructor returns the function that created it (usually Object). obj.constructor.constructor returns the Function constructor. The Function constructor is effectively eval()—it takes a string of code and creates a function that executes it.
The Attack Chain:
constructor, but spell it with Unicode: {"co\u006Estructor": 1}.Object constructor..constructor of the Object constructor to get the global Function constructor.The Payload:
// This looks like a math expression, but it executes 'kill'
math.eval(`
a = {"co\\u006Estructor": 1};
a.constructor.constructor("return process.kill(process.pid)")()
`);In a real-world scenario, instead of killing the process, an attacker would use child_process (if available in the context) or standard filesystem APIs to exfiltrate environment variables (AWS keys, DB creds) or establish a reverse shell.
This vulnerability scored a CVSS 9.8 for a reason. It is unauthenticated, requires no user interaction (other than the system processing the input), and results in total compromise of the application's confidentiality, integrity, and availability.
Consider where math.js is used. It is often embedded in SaaS platforms to allow users to define custom pricing formulas, financial models, or scientific calculations. These features are by definition "remote code execution as a feature," restricted only by the sandbox. CVE-2017-1001003 dissolves that sandbox entirely.
If your backend runs this code, the attacker runs as the user of your Node.js process. They can read your .env files, dump your database, or use your server as part of a botnet. The "math" library becomes the foothold for a full infrastructure compromise.
The remediation is straightforward: Update math.js to version 3.17.0 or later immediately.
If you cannot update for some reason (legacy dependency hell), you have to implement a sanitizer before the input reaches math.eval. A rough mitigation would be to reject any input containing the \u sequence, although this might break legitimate mathematical notation or string usage depending on your use case.
> [!NOTE] > Developer Takeaway: When validating input, always validate the canonical form of the data. If your system transforms data (URL decoding, Unicode normalization, XML parsing) after your security check, you are vulnerable. Always normalize first, then validate.
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
math.js josdejong | < 3.17.0 | 3.17.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-20 / CWE-88 |
| CVSS v3.0 | 9.8 (Critical) |
| Attack Vector | Network (Remote) |
| Exploit Status | PoC Available |
| EPSS Score | 0.49% |
| Impact | Arbitrary Code Execution |