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:
this.simplify(variables)folds caller-supplied data into the token stream before code generation.expressionToString(..., true)emits JavaScript source text, not a sandboxed bytecode or AST object.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
.envdriven 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:
- Remove
toJSFunction()from any path that handles untrusted input. - Stop accepting non-primitive values in variable maps passed into formula compilation.
- If formula execution is a hard requirement, move it into a dedicated sandboxed worker with minimal privileges and no ambient secrets.
- Prefer engines that do not rely on
new Function()for untrusted formulas. - 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:
- Assume JavaScript executed with the same privileges as the Node.js process.
- Rotate secrets reachable from that process, including cloud credentials, database passwords, CI tokens, and API keys.
- Review logs for formula creation, formula edits, or unusual payload-like strings near expression fields.
- Inspect outbound connections and shell execution telemetry from affected hosts.
- 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.