Math.js: When 1 + 1 Equals Root Shell (CVE-2017-1001003)
Jan 12, 2026·6 min read
Executive Summary (TL;DR)
The math.js library attempted to block access to dangerous properties like `constructor` by comparing property names against a blacklist. However, it failed to normalize Unicode escape sequences before this check. An attacker could pass `co\u006Estructor`, which evades the textual blacklist but is resolved by the JavaScript engine to `constructor`. This grants access to the `Function` constructor, enabling arbitrary code execution on the host server.
A critical sandbox escape in the popular math.js library allows attackers to bypass property restrictions using Unicode escape sequences, leading to Remote Code Execution via the constructor chain.
The Hook: It's Just Math, Right?
JavaScript sandboxes are like screen doors on a submarine. They look solid, they feel safe, but under enough pressure (or just the wrong kind of poke), they fold instantly. Developers love math.js because it provides a powerful expression parser that feels safer than the terrifying eval(). You pass it a string like 2 * x + 5, give it a scope, and it returns a number. Clean. Simple.
But here is the problem: to be useful, math.js needs to handle objects. And in JavaScript, objects are dynamic minefields. If you give a user the ability to define objects, you are one step away from giving them the keys to the kingdom.
To prevent this, math.js implemented what we in the industry call "The Bouncer approach." It had a blacklist of bad words you couldn't use as object keys, specifically constructor, __proto__, and prototype. If you tried math.eval('x.constructor'), it would throw an error. Secure, right? Well, only if you assume the Bouncer speaks every dialect of the language.
The Flaw: Lost in Translation
The vulnerability (CVE-2017-1001003) is a classic case of normalization inconsistency. It falls under the umbrella of "Check, Then Act," but specifically, checking the raw representation while acting on the resolved representation.
When math.js parses an expression like {'key': 'value'}, it has to validate the key. In the vulnerable versions (pre-3.17.0), the code looked at the raw string literal provided by the user. If you wrote {'constructor': 1}, the validator saw the string "constructor", panicked, and blocked it.
However, JavaScript allows Unicode escape sequences in identifiers and strings. The sequence \u006E represents the letter n. If an attacker wrote {'co\u006Estructor': 1}, the validator saw the literal string "co\\u006Estructor". It compared that string to "constructor". Obviously, they aren't the same string. The validator shrugged and let it through.
[!NOTE] The validator was checking the syntax (what was typed), but the engine executes the semantics (what it means).
Once the validation passed, the code was compiled and handed off to the JavaScript engine. The engine, being helpful and compliant, saw co\u006Estructor, decoded the Unicode escape, and accessed the actual constructor property. The Bouncer let in a guy named "V\u006Fldemort" because the list only said "Voldemort".
The Code: The Smoking Gun
Let's look at the crime scene in lib/expression/node/ObjectNode.js. The code iterates over the properties of an object defined in the expression and checks them against isSafeProperty.
Here is the vulnerable logic:
// PRE-PATCH VULNERABLE CODE
for (var key in node.properties) {
// 'key' here is the raw string from the parser, e.g., "co\\u006Estructor"
if (!isSafeProperty(node.properties, key)) {
throw new Error('No access to property "' + key + '"');
}
// ... compilation proceeds ...
}The isSafeProperty function was doing a strict string comparison. It didn't know that \u006E is just a fancy n.
The fix, introduced in version 3.17.0 (commit a60f3c8), forces the key to reveal its true form before the ID check. It effectively says, "I don't care how you spell it, tell me what you are."
// PATCHED CODE
// stringify/parse resolves the unicode characters
var stringifiedKey = stringify(key);
var parsedKey = JSON.parse(stringifiedKey); // "co\\u006Estructor" becomes "constructor"
if (!isSafeProperty(node.properties, parsedKey)) {
throw new Error('No access to property "' + parsedKey + '"');
}By round-tripping the key through JSON serialization, any escape sequences are resolved. "co\u006Estructor" becomes "constructor", and the isSafeProperty check finally catches it.
The Exploit: Climbing the Prototype Chain
So we can access constructor. Why does that matter? In JavaScript, constructor is the gateway to the metaphysical underpinnings of the runtime. If you have an object, obj.constructor gives you Object. obj.constructor.constructor gives you Function.
The Function constructor is essentially eval()'s more sophisticated cousin. It takes a string of code and returns a callable function. If we can reach it, we win.
Here is the attack chain visualised:
And here is the payload logic. We define an object where the key is the Unicode-escaped constructor. The value of that key is another object (because constructor returns a function, and we need to chain deeper).
Eventually, we construct a payload that looks like this to math.eval():
math.eval(`
{
"co\\u006Estructor": {
"co\\u006Estructor": "return process.mainModule.require('child_process').execSync('cat /etc/passwd').toString()"
}
}()
`);Wait, why the nested constructor?
- The outer object's
constructorproperty is set. - When we access it, we are actually accessing the
Objectconstructor. - We walk up to
Object.constructor(which isFunction). - We pass our malicious string to
Function. - The final
()invokes the created function.
Boom. You are now the www-data user.
The Impact: Calculator to Command Line
This isn't just a Cross-Site Scripting (XSS) bug where you annoy a user with an alert box. Because math.js is often used in Node.js environments to handle server-side calculations (pricing engines, scientific data processing, smart contracts), this is a full Remote Code Execution (RCE).
If you are running a service that accepts math formulas from users—perhaps a finance app calculating loan interest or a student portal plotting graphs—and you are using a vulnerable version of math.js, an attacker owns your server.
They can:
- Read environment variables (API keys, AWS credentials).
- Exfiltrate source code.
- Install persistence or crypto miners.
- Pivot to the internal network.
CVSS 9.8 is not a suggestion; it's a scream. The barrier to entry is zero authentication and a single HTTP request.
The Fix: Normalization is Key
The remediation is straightforward: Update math.js to version 3.17.0 or higher. The maintainers fixed this back in late 2017, so if you are still vulnerable, you are practicing digital archaeology.
If you are a developer writing your own parsers or validators, the lesson is broader: Canonicalize before you validate.
Never check the raw input if the system processing that input interprets it differently than your checker does. This applies to:
- Unicode escapes in JSON/JS.
- URL encoding in WAFs (
%2e%2e/vs../). - Case sensitivity in file systems.
If you cannot upgrade immediately (why?), you would need to manually sanitize input strings to reject \ characters before passing them to math.eval, but honestly, just upgrade. Don't try to outsmart the parser; you will lose.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
math.js Jos de Jong | < 3.17.0 | 3.17.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-88 |
| CVSS v3.0 | 9.8 (Critical) |
| Vector | CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Attack Vector | Network (Input Injection) |
| EPSS Score | 0.48% |
| Exploit Status | PoC Available |
MITRE ATT&CK Mapping
Improper Neutralization of Argument Delimiters or Special Characters
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.