CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Dashboard
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2017-1001003
9.80.49%

The Unicode Ghost: Breaking Math.js with Invisible Ink

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 21, 2026·6 min read·6 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: It's Just Math, Right?

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 Flaw: A Case of Mistaken Identity

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.

The Code: The Smoking Gun

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.

The Exploit: From Math to Shell

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:

  1. Bypass the Filter: Create an object property pointing to constructor, but spell it with Unicode: {"co\u006Estructor": 1}.
  2. Climb the Prototype Chain: Access the value of that property to get the Object constructor.
  3. Get the God Function: Access the .constructor of the Object constructor to get the global Function constructor.
  4. Execute: Pass a payload string to generate a malicious function and immediately invoke it.

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.

The Impact: Why You Should Panic

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 Fix: Closing the Loophole

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.

Official Patches

math.jsCommit fixing the Unicode normalization bypass

Fix Analysis (1)

Technical Appendix

CVSS Score
9.8/ 10
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Probability
0.49%
Top 35% most exploited

Affected Systems

math.js < 3.17.0Node.js applications using vulnerable math.js for user input evaluationFront-end applications evaluating user-defined math expressions

Affected Versions Detail

Product
Affected Versions
Fixed Version
math.js
josdejong
< 3.17.03.17.0
AttributeDetail
CWE IDCWE-20 / CWE-88
CVSS v3.09.8 (Critical)
Attack VectorNetwork (Remote)
Exploit StatusPoC Available
EPSS Score0.49%
ImpactArbitrary Code Execution

MITRE ATT&CK Mapping

T1059.007Command and Scripting Interpreter: JavaScript
Execution
T1203Exploitation for Client Execution
Execution
CWE-20
Improper Input Validation

Known Exploits & Detection

GitHubOriginal issue report by Masato Kinugawa demonstrating the bypass.

Vulnerability Timeline

Vulnerability reported by Masato Kinugawa
2017-11-18
Patch developed and committed
2017-11-18
math.js version 3.17.0 released
2017-11-18
CVE-2017-1001003 Published
2017-11-27

References & Sources

  • [1]math.js Changelog v3.17.0
  • [2]NIST NVD Entry

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.