critical

CVE

CVE-2026-12866

CWE

CWE-94

Affected Surface

All versions of the npm package `expr-eval`, Node.js applications that call `Expression.prototype.toJSFunction()` on attacker-controlled expressions or variables, Internal tools and backends that let users define formulas, rules, scoring logic, or automation expressions with `expr-eval`, Developer tooling and CI helpers that precompile untrusted expressions for later reuse

expr-eval is a small and widely copied JavaScript formula engine, so it often ends up in places developers stop thinking about as “code execution” surfaces: pricing rules, scorecards, feature gates, workflow conditions, low-code builders, CLI helpers, and internal admin tools. CVE-2026-12866 matters because it shows that expr-eval’s toJSFunction() API does not merely evaluate expressions. It emits JavaScript source and hands that source to the runtime compiler.

That is a fundamentally different trust boundary. Once an application lets untrusted input influence the generated function body, the formula stops being data and becomes code.

Affected package and versions

The affected package is the npm package:

  • expr-eval - all versions

As of the 23 June 2026 advisory publication, public advisories describe no fixed upstream expr-eval release for this CVE. That makes this a “remove or isolate the dangerous API” problem, not a routine semver bump.

The environments that matter most are:

  • Node.js backends that compile user formulas for reuse
  • internal admin tools that let operators define rules or score expressions
  • workflow engines that store expressions in a database and later convert them into callable functions
  • CLI or CI helper scripts that turn config-supplied formulas into reusable JavaScript

Browser-only usage is lower risk than server-side Node.js usage because the strongest public proof of concept relies on Node globals such as process.mainModule.require. The server-side risk is the one application security teams should prioritize.

Root cause in the package source

The vulnerable code path is short and explicit. Expression.prototype.toJSFunction() builds JavaScript source with expressionToString() and then compiles it with new Function():

Expression.prototype.toJSFunction = function (param, variables) {
  var expr = this;
  var f = new Function(
    param,
    'with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return ' +
      expressionToString(this.simplify(variables).tokens, true) +
      '; }'
  );
  return function () {
    return f.apply(expr, arguments);
  };
};

Three properties of that implementation create the security boundary failure:

  1. this.simplify(variables) folds caller-supplied data into the token stream before code generation.
  2. expressionToString(..., true) emits JavaScript source text, not a sandboxed bytecode or AST object.
  3. new Function(...) compiles the generated string inside the current Node.js process.

That means the application is only safe if both the expression and the values flowing into simplification are trusted enough to become source code.

Why toJSFunction() is dangerous even when the expression “looks like math”

The public proof of concept published in the upstream issue uses a malicious object with a custom toString() implementation. On current Node.js releases, the same boundary failure can still be demonstrated with a small adaptation that reaches built-in modules through process.getBuiltinModule():

const { Parser } = require('expr-eval');

const parser = new Parser();
const expr = parser.parse('[cmd]');

const payload = {
  toString: () => `
    (function(){
      const cp = process.getBuiltinModule('child_process');
      return cp.execSync('whoami').toString().trim();
    })()
  `
};

const fn = expr.toJSFunction('', { cmd: payload });
console.log(fn());

In local validation on Node.js 22.14.0, that payload returned the current user account name successfully. The older disclosure used process.mainModule.require(...); modern Node variants change the exact gadget, but not the vulnerable boundary. The core issue is still that attacker-controlled input becomes executable JavaScript inside the application process.

The important lesson is not the exact whoami payload. The important lesson is the data flow:

attacker-controlled formula or variables
  -> simplify(variables)
  -> expressionToString(..., true)
  -> new Function(...)
  -> JavaScript executes inside the Node.js process

If that process holds cloud credentials, CI tokens, application secrets, or production network reachability, the formula engine has become a remote code execution boundary.

Realistic exposure patterns

The vulnerable condition is narrower than “any app with expr-eval installed,” but it is still common:

  • customer-facing products that let users create calculated fields or scoring formulas
  • internal business systems where analysts or operators can save expressions in a database
  • workflow products that compile a rule once and call it repeatedly for performance
  • developer tooling that loads formulas from config files or plugin ecosystems
  • multi-tenant SaaS platforms where one tenant’s expression is executed in a shared Node.js service

The highest-risk design pattern is “take a string from a less trusted party, call parse(), then call toJSFunction() so it can run faster later.” That optimization changes the threat model from expression evaluation to source-code generation.

Scoping and detection

Start by finding whether the package is present at all:

npm ls expr-eval
pnpm why expr-eval
yarn why expr-eval

Then search your codebase for the dangerous API:

rg -n 'toJSFunction\\(' src app lib packages scripts
rg -n 'expr-eval' package.json package-lock.json pnpm-lock.yaml yarn.lock

The most important code review question is not “Do we use expr-eval?” It is:

Can an attacker influence the expression string or the variables object
that later reaches Expression.prototype.toJSFunction()?

Review these input sources closely:

  • HTTP request bodies and query parameters
  • stored formulas loaded from a database
  • YAML, JSON, or .env driven automation rules
  • plugin inputs and customer-uploaded configs
  • background jobs that replay saved expressions against privileged data

If you already know untrusted expressions were compiled in Node.js, treat the host as having executed attacker code. Review process-level secrets, outbound network access, and filesystem activity for the exposure window.

Remediation when no patch exists

Because public advisories do not list a fixed upstream release, remediation has to focus on design changes:

  1. Remove toJSFunction() from any path that handles untrusted input.
  2. Stop accepting non-primitive values in variable maps passed into formula compilation.
  3. If formula execution is a hard requirement, move it into a dedicated sandboxed worker with minimal privileges and no ambient secrets.
  4. Prefer engines that do not rely on new Function() for untrusted formulas.
  5. Treat stored formulas like executable content: version them, review them, and apply trust boundaries before execution.

The most conservative short-term mitigation is simple:

untrusted expression or variables
  -> do not call toJSFunction()

If your product cannot avoid dynamic formulas immediately, at least separate the formula runner from the application process that holds production credentials, signing keys, or deployment tokens.

Incident response guidance

If your application exposed toJSFunction() to attacker-controlled input:

  1. Assume JavaScript executed with the same privileges as the Node.js process.
  2. Rotate secrets reachable from that process, including cloud credentials, database passwords, CI tokens, and API keys.
  3. Review logs for formula creation, formula edits, or unusual payload-like strings near expression fields.
  4. Inspect outbound connections and shell execution telemetry from affected hosts.
  5. Audit any persisted formulas before re-enabling execution.

For defenders, the key takeaway is architectural: a formula compiler that reaches new Function() is part of your application’s code-execution surface. In expr-eval, toJSFunction() crosses that line explicitly, and CVE-2026-12866 confirms that teams should treat it accordingly.

References