CVE-2025-12735
Description
The expr-eval library is a JavaScript expression parser and evaluator designed to safely evaluate mathematical expressions with user-defined variables. However, due to insufficient input validation, an attacker can pass a crafted context object or use MEMBER of the context object into the evaluate() function and trigger arbitrary code execution.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2025-12735 is a code injection vulnerability in the expr-eval JavaScript library that allows arbitrary code execution via a crafted context object passed to evaluate().
Vulnerability
Overview
The expr-eval library is a JavaScript expression parser and evaluator designed to safely evaluate mathematical expressions with user-defined variables. CVE-2025-12735 arises from insufficient input validation in the evaluate() function. An attacker can pass a crafted context object or use a MEMBER of the context object to trigger arbitrary code execution [1][2]. The root cause is that the library did not properly restrict the types of values that could be passed as variables in the context object, allowing functions or objects with malicious code to be injected and executed during expression evaluation.
Exploitation
Details
Exploitation requires the attacker to control the context object passed to evaluate(). This can happen when user-supplied data is used to construct the variables object without sanitization. By including a function or a property that references a function (e.g., via MEMBER access), the attacker can cause the library to execute arbitrary JavaScript code in the context of the application using the library [2][4]. The vulnerability is classified as CWE-94 (Improper Control of Generation of Code) [4].
Impact
Successful exploitation allows an attacker to execute arbitrary code on the server or client where the library is used. This can lead to full compromise of the application, including data theft, privilege escalation, or further lateral movement within the environment. The CVSS score has not yet been assigned by NVD, but the severity is considered high due to the potential for remote code execution [2].
Mitigation
The fix has been implemented in the expr-eval-fork (version 3.0.0) and is available on NPM [1][4]. The breaking change requires that custom functions be defined on Parser.functions rather than passed in the context object to evaluate(). Users of the original expr-eval library should migrate to the fork or apply the patch from commit 1d71bb2ca8f98df8de00e9cc4de8fdd468a468a7ad43 [4]. No official patch has been released version of the original library includes the fix, so the fork is the recommended workaround [1].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
expr-evalnpm | <= 2.0.2 | — |
expr-eval-forknpm | < 3.0.1 | 3.0.1 |
Affected products
3- expr-eval-fork/expr-eval-forkv5Range: 0
- silentmatt/expr-evalv5Range: 0
Patches
11d71bb2ca8f9fix: security issue CWE-94 -> CVE-2025-12735
16 files changed · +223 −104
CHANGELOG.md+9 −0 modified@@ -1,10 +1,19 @@ # Changelog +## [3.0.0] - 2025-11-07 + +### Added + +- BREAKING: `.evaluate()` no longer allows arbitrary and potentially malicious context to be passed for custom function strings. Such functions need to be defined on `Parser.functions`, e.g. `Parser.functions.f = () => {}` rather than `.evaluate({ f: () => {} })`. This fixes [CVE-2025-12735](https://github.com/advisories/GHSA-jc85-fpwf-qm7x). +- BREAKING: add exports map to make usage with modern JS environments smoother, not requiring bundlers. +- BREAKING: require Node 16.9.0 minimum, to support `Object.hasOwn` which is safer than its predecessor `Object.prototype.hasOwnPropery`. + ## [2.0.2] - 2019-09-28 ### Added - Added non-default exports when using the ES module format. This allows `import { Parser } from 'expr-eval'` to work in TypeScript. The default export is still available for backward compatibility. +- This fork publishes a security vulnerability fix for prototype pollution. This was committed to the origin project but never published to NPM. ## [2.0.1] - 2019-09-10
index.js+1 −1 modified@@ -18,7 +18,7 @@ export { }; // Backwards compatibility -export default{ +export default { Parser: Parser, Expression: Expression };
package.json+13 −4 modified@@ -1,18 +1,27 @@ { "name": "expr-eval-fork", - "version": "2.0.2", - "description": "Mathematical expression evaluator fork with prototype pollution fix", + "version": "3.0.0", + "description": "Mathematical expression evaluator fork with exports map, prototype pollution and code injection security fixes", "main": "dist/bundle.js", "module": "dist/index.mjs", "typings": "parser.d.ts", + "engines": { + "node": ">=16.9.0" + }, + "exports": { + ".": { + "types": "./parser.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/bundle.js" + } + }, "directories": { "test": "test" }, - "dependencies": {}, "devDependencies": { "eslint": "^6.3.0", "eslint-config-semistandard": "^15.0.0", - "eslint-config-standard": "^13.0.1", + "eslint-config-standard": "^14.1.0", "eslint-plugin-import": "^2.15.0", "eslint-plugin-node": "^9.2.0", "eslint-plugin-promise": "^4.0.1",
package-lock.json+11 −7 modified@@ -1,17 +1,17 @@ { "name": "expr-eval-fork", - "version": "2.0.2", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "expr-eval-fork", - "version": "2.0.2", + "version": "3.0.0", "license": "MIT", "devDependencies": { "eslint": "^6.3.0", "eslint-config-semistandard": "^15.0.0", - "eslint-config-standard": "^13.0.1", + "eslint-config-standard": "^14.1.0", "eslint-plugin-import": "^2.15.0", "eslint-plugin-node": "^9.2.0", "eslint-plugin-promise": "^4.0.1", @@ -20,6 +20,9 @@ "nyc": "^14.1.1", "rollup": "^1.20.3", "rollup-plugin-uglify": "^6.0.3" + }, + "engines": { + "node": ">=16.9.0" } }, "node_modules/@babel/code-frame": { @@ -1054,12 +1057,13 @@ } }, "node_modules/eslint-config-standard": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-13.0.1.tgz", - "integrity": "sha512-zLKp4QOgq6JFgRm1dDCVv1Iu0P5uZ4v5Wa4DTOkg2RFMxdCX/9Qf7lz9ezRj2dBRa955cWQF/O/LWEiYWAHbTw==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz", + "integrity": "sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg==", "dev": true, + "license": "MIT", "peerDependencies": { - "eslint": ">=6.0.1", + "eslint": ">=6.2.2", "eslint-plugin-import": ">=2.18.0", "eslint-plugin-node": ">=9.1.0", "eslint-plugin-promise": ">=4.2.1",
README.md+3 −3 modified@@ -1,9 +1,9 @@ JavaScript Expression Evaluator =============================== -[](https://www.npmjs.com/package/expr-eval) -[](https://cdnjs.com/libraries/expr-eval) -[](https://travis-ci.org/silentmatt/expr-eval) +[](https://www.npmjs.com/package/expr-eval-fork) +[](https://cdnjs.com/libraries/expr-eval-fork) +[](https://travis-ci.org/silentmatt/expr-eval-fork) > This fork addresses https://github.com/silentmatt/expr-eval/issues/266, security fix has been committed but was never released to NPM > Therefore, we publish expr-eval-fork to NPM to work around this issue.
rollup-min.config.js+1 −1 modified@@ -1,7 +1,7 @@ import rollupConfig from './rollup.config'; import { uglify } from 'rollup-plugin-uglify'; -rollupConfig.plugins = [ uglify() ]; +rollupConfig.plugins = [uglify()]; rollupConfig.output.file = 'dist/bundle.min.js'; export default rollupConfig;
src/evaluate.js+33 −0 modified@@ -1,5 +1,26 @@ import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY } from './instruction'; +/** + * Checks if a function reference 'f' is explicitly allowed to be executed. + * This logic is the core security allowance gate. + */ +function isAllowedFunc(f, expr, values) { + for (var key in expr.functions) { + if (expr.functions[key] === f) return true; + } + + if (f.__expr_eval_safe_def) return true; + + for (var vKey in values) { + if (typeof values[vKey] === 'object' && values[vKey] !== null) { + for (var subKey in values[vKey]) { + if (values[vKey][subKey] === f) return true; + } + } + } + return false; +} + export default function evaluate(tokens, expr, values) { var nstack = []; var n1, n2, n3; @@ -50,7 +71,12 @@ export default function evaluate(tokens, expr, values) { nstack.push(expr.unaryOps[item.value]); } else { var v = values[item.value]; + if (v !== undefined) { + if (typeof v === 'function' && !isAllowedFunc(v, expr, values)) { + /* function is not registered, not marked safe, and not a member function. BLOCKED. */ + throw new Error('Variable references an unallowed function: ' + item.value); + } nstack.push(v); } else { throw new Error('undefined variable: ' + item.value); @@ -67,6 +93,9 @@ export default function evaluate(tokens, expr, values) { args.unshift(resolveExpression(nstack.pop(), values)); } f = nstack.pop(); + if (!isAllowedFunc(f, expr, values)) { + throw new Error('Is not an allowed function.'); + } if (f.apply && f.call) { nstack.push(f.apply(undefined, args)); } else { @@ -94,6 +123,10 @@ export default function evaluate(tokens, expr, values) { value: n1, writable: false }); + Object.defineProperty(f, '__expr_eval_safe_def', { + value: true, + writable: false + }); values[n1] = f; return f; })());
src/expression-to-string.js+2 −2 modified@@ -115,9 +115,9 @@ export default function expressionToString(tokens, toJS) { } if (nstack.length > 1) { if (toJS) { - nstack = [ nstack.join(',') ]; + nstack = [nstack.join(',')]; } else { - nstack = [ nstack.join(';') ]; + nstack = [nstack.join(';')]; } } return String(nstack[0]);
src/functions.js+1 −1 modified@@ -326,7 +326,7 @@ export function sign(x) { return ((x > 0) - (x < 0)) || +x; } -var ONE_THIRD = 1/3; +var ONE_THIRD = 1 / 3; export function cbrt(x) { return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD); }
src/parser.js+8 −8 modified@@ -106,7 +106,7 @@ export function Parser(options) { '<=': lessThanEqual, and: andOperator, or: orOperator, - 'in': inOperator, + in: inOperator, '=': setVar, '[': arrayIndex }; @@ -124,7 +124,7 @@ export function Parser(options) { pyt: Math.hypot || hypot, // backward compat pow: Math.pow, atan2: Math.atan2, - 'if': condition, + if: condition, gamma: gamma, roundTo: roundTo, map: arrayMap, @@ -138,8 +138,8 @@ export function Parser(options) { this.consts = { E: Math.E, PI: Math.PI, - 'true': true, - 'false': false + true: true, + false: false }; } @@ -186,9 +186,9 @@ var optionNameMap = { '==': 'comparison', '!=': 'comparison', '||': 'concatenate', - 'and': 'logical', - 'or': 'logical', - 'not': 'logical', + and: 'logical', + or: 'logical', + not: 'logical', '?': 'conditional', ':': 'conditional', '=': 'assignment', @@ -197,7 +197,7 @@ var optionNameMap = { }; function getOptionName(op) { - return optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op; + return Object.hasOwn(optionNameMap, op) ? optionNameMap[op] : op; } Parser.prototype.isOperatorEnabled = function (op) {
src/simplify.js+4 −3 modified@@ -16,7 +16,7 @@ export default function simplify(tokens, unaryOps, binaryOps, ternaryOps, values } else { nstack.push(item); } - } else if (type === IVAR && values.hasOwnProperty(item.value)) { + } else if (type === IVAR && Object.hasOwn(values, item.value)) { item = new Instruction(INUMBER, values[item.value]); nstack.push(item); } else if (type === IOP2 && nstack.length > 1) { @@ -49,13 +49,14 @@ export default function simplify(tokens, unaryOps, binaryOps, ternaryOps, values } else if (type === IMEMBER && nstack.length > 0) { n1 = nstack.pop(); nstack.push(new Instruction(INUMBER, n1.value[item.value])); - } /* else if (type === IARRAY && nstack.length >= item.value) { + /* } else if (type === IARRAY && nstack.length >= item.value) { var length = item.value; while (length-- > 0) { newexpression.push(nstack.pop()); } newexpression.push(new Instruction(IARRAY, item.value)); - } */ else { + } */ + } else { while (nstack.length > 0) { newexpression.push(nstack.shift()); }
test/expression.js+12 −12 modified@@ -96,7 +96,7 @@ describe('Expression', function () { }); it('[1, 2] || [3, 4] || [5, 6]', function () { - assert.deepStrictEqual(Parser.evaluate('[1, 2] || [3, 4] || [5, 6]'), [ 1, 2, 3, 4, 5, 6 ]); + assert.deepStrictEqual(Parser.evaluate('[1, 2] || [3, 4] || [5, 6]'), [1, 2, 3, 4, 5, 6]); }); it('should fail with undefined variables', function () { @@ -343,11 +343,11 @@ describe('Expression', function () { }); it('a[2] + b[3]', function () { - assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [ 0, 0, 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '9'); - assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [ 0, 0, 5, 0 ] }).toString(), '(5 + b[3])'); - assert.strictEqual(Parser.parse('a[2] + b[5 - 2]').simplify({ b: [ 0, 0, 0, 4, 0 ] }).toString(), '(a[2] + 4)'); - assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [ 0, 0, 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '([0, 0, 5, 0][two] + 4)'); - assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [ 0, 'New\nLine', 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '([0, "New\\nLine", 5, 0][two] + 4)'); + assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [0, 0, 5, 0], b: [0, 0, 0, 4, 0] }).toString(), '9'); + assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [0, 0, 5, 0] }).toString(), '(5 + b[3])'); + assert.strictEqual(Parser.parse('a[2] + b[5 - 2]').simplify({ b: [0, 0, 0, 4, 0] }).toString(), '(a[2] + 4)'); + assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [0, 0, 5, 0], b: [0, 0, 0, 4, 0] }).toString(), '([0, 0, 5, 0][two] + 4)'); + assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [0, 'New\nLine', 5, 0], b: [0, 0, 0, 4, 0] }).toString(), '([0, "New\\nLine", 5, 0][two] + 4)'); }); }); @@ -691,7 +691,7 @@ describe('Expression', function () { it('[4, 3] || [1, 2]', function () { var expr = parser.parse('x || y'); var f = expr.toJSFunction('x, y'); - assert.deepStrictEqual(f([ 4, 3 ], [ 1, 2 ]), [ 4, 3, 1, 2 ]); + assert.deepStrictEqual(f([4, 3], [1, 2]), [4, 3, 1, 2]); }); it('x = x + 1', function () { @@ -906,17 +906,17 @@ describe('Expression', function () { }); it('a[2]', function () { - assert.strictEqual(parser.parse('a[2]').toJSFunction('a')([ 1, 2, 3 ]), 3); + assert.strictEqual(parser.parse('a[2]').toJSFunction('a')([1, 2, 3]), 3); }); it('a[2.9]', function () { - assert.strictEqual(parser.parse('a[2.9]').toJSFunction('a')([ 1, 2, 3, 4, 5 ]), 3); + assert.strictEqual(parser.parse('a[2.9]').toJSFunction('a')([1, 2, 3, 4, 5]), 3); }); it('a[n]', function () { - assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 0), 1); - assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 1), 2); - assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 2), 3); + assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([1, 2, 3], 0), 1); + assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([1, 2, 3], 1), 2); + assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([1, 2, 3], 2), 3); }); it('a["foo"]', function () {
test/functions.js+10 −10 modified@@ -267,20 +267,20 @@ describe('Functions', function () { it('should call built-in functions', function () { var parser = new Parser(); - assert.deepStrictEqual(parser.evaluate('map(sqrt, [0, 1, 16, 81])'), [ 0, 1, 4, 9 ]); - assert.deepStrictEqual(parser.evaluate('map(max, [2, 2, 2, 2, 2, 2])'), [ 2, 2, 2, 3, 4, 5 ]); + assert.deepStrictEqual(parser.evaluate('map(sqrt, [0, 1, 16, 81])'), [0, 1, 4, 9]); + assert.deepStrictEqual(parser.evaluate('map(max, [2, 2, 2, 2, 2, 2])'), [2, 2, 2, 3, 4, 5]); }); it('should call self-defined functions', function () { var parser = new Parser(); - assert.deepStrictEqual(parser.evaluate('f(a) = a*a; map(f, [0, 1, 2, 3, 4])'), [ 0, 1, 4, 9, 16 ]); + assert.deepStrictEqual(parser.evaluate('f(a) = a*a; map(f, [0, 1, 2, 3, 4])'), [0, 1, 4, 9, 16]); }); it('should call self-defined functions with index', function () { var parser = new Parser(); - assert.deepStrictEqual(parser.evaluate('f(a, i) = a+i; map(f, [1,3,5,7,9])'), [ 1, 4, 7, 10, 13 ]); - assert.deepStrictEqual(parser.evaluate('map(anon(a, i) = a+i, [1,3,5,7,9])'), [ 1, 4, 7, 10, 13 ]); - assert.deepStrictEqual(parser.evaluate('f(a, i) = i; map(f, [1,3,5,7,9])'), [ 0, 1, 2, 3, 4 ]); + assert.deepStrictEqual(parser.evaluate('f(a, i) = a+i; map(f, [1,3,5,7,9])'), [1, 4, 7, 10, 13]); + assert.deepStrictEqual(parser.evaluate('map(anon(a, i) = a+i, [1,3,5,7,9])'), [1, 4, 7, 10, 13]); + assert.deepStrictEqual(parser.evaluate('f(a, i) = i; map(f, [1,3,5,7,9])'), [0, 1, 2, 3, 4]); }); }); @@ -343,19 +343,19 @@ describe('Functions', function () { it('should call built-in functions', function () { var parser = new Parser(); - assert.deepStrictEqual(parser.evaluate('filter(not, [1, 0, false, true, 2, ""])'), [ 0, false, '' ]); + assert.deepStrictEqual(parser.evaluate('filter(not, [1, 0, false, true, 2, ""])'), [0, false, '']); }); it('should call self-defined functions', function () { var parser = new Parser(); - assert.deepStrictEqual(parser.evaluate('f(x) = x > 2; filter(f, [1, 2, 0, 3, -1, 4])'), [ 3, 4 ]); + assert.deepStrictEqual(parser.evaluate('f(x) = x > 2; filter(f, [1, 2, 0, 3, -1, 4])'), [3, 4]); assert.deepStrictEqual(parser.evaluate('f(x) = x > 2; filter(f, [1, 2, 0, 1.9, -1, -4])'), []); }); it('should call self-defined functions with index', function () { var parser = new Parser(); - assert.deepStrictEqual(parser.evaluate('f(a, i) = a <= i; filter(f, [1,0,5,3,2])'), [ 0, 3, 2 ]); - assert.deepStrictEqual(parser.evaluate('f(a, i) = i > 3; filter(f, [9,0,5,6,1,2,3,4])'), [ 1, 2, 3, 4 ]); + assert.deepStrictEqual(parser.evaluate('f(a, i) = a <= i; filter(f, [1,0,5,3,2])'), [0, 3, 2]); + assert.deepStrictEqual(parser.evaluate('f(a, i) = i > 3; filter(f, [9,0,5,6,1,2,3,4])'), [1, 2, 3, 4]); }); });
test/operators.js+17 −17 modified@@ -172,7 +172,7 @@ describe('Operators', function () { it('evaluates rhs when lhs is true', function () { var called = spy(returnFalse); - assert.strictEqual(Parser.evaluate('true and called()', { called: called }), false); + assert.strictEqual(Parser.evaluate('true and spies.called()', { spies: { called: called } }), false); assert.strictEqual(called.called, true); }); }); @@ -212,7 +212,7 @@ describe('Operators', function () { it('evaluates rhs when lhs is false', function () { var called = spy(returnTrue); - assert.strictEqual(Parser.evaluate('false or called()', { called: called }), true); + assert.strictEqual(Parser.evaluate('false or spies.called()', { spies: { called: called } }), true); assert.strictEqual(called.called, true); }); }); @@ -221,27 +221,27 @@ describe('Operators', function () { var parser = new Parser(); it('"a" in ["a", "b"]', function () { - assert.strictEqual(parser.evaluate('"a" in toto', { 'toto': ['a', 'b'] }), true); + assert.strictEqual(parser.evaluate('"a" in toto', { toto: ['a', 'b'] }), true); }); it('"a" in ["b", "a"]', function () { - assert.strictEqual(parser.evaluate('"a" in toto', { 'toto': ['b', 'a'] }), true); + assert.strictEqual(parser.evaluate('"a" in toto', { toto: ['b', 'a'] }), true); }); it('3 in [4, 3]', function () { - assert.strictEqual(parser.evaluate('3 in toto', { 'toto': [4, 3] }), true); + assert.strictEqual(parser.evaluate('3 in toto', { toto: [4, 3] }), true); }); it('"c" in ["a", "b"]', function () { - assert.strictEqual(parser.evaluate('"c" in toto', { 'toto': ['a', 'b'] }), false); + assert.strictEqual(parser.evaluate('"c" in toto', { toto: ['a', 'b'] }), false); }); it('"c" in ["b", "a"]', function () { - assert.strictEqual(parser.evaluate('"c" in toto', { 'toto': ['b', 'a'] }), false); + assert.strictEqual(parser.evaluate('"c" in toto', { toto: ['b', 'a'] }), false); }); it('3 in [1, 2]', function () { - assert.strictEqual(parser.evaluate('3 in toto', { 'toto': [1, 2] }), false); + assert.strictEqual(parser.evaluate('3 in toto', { toto: [1, 2] }), false); }); }); @@ -910,27 +910,27 @@ describe('Operators', function () { describe('[] operator', function () { it('a[0]', function () { - assert.strictEqual(Parser.evaluate('a[0]', { a: [ 4, 3, 2, 1 ] }), 4); + assert.strictEqual(Parser.evaluate('a[0]', { a: [4, 3, 2, 1] }), 4); }); it('a[0.1]', function () { - assert.strictEqual(Parser.evaluate('a[0.1]', { a: [ 4, 3, 2, 1 ] }), 4); + assert.strictEqual(Parser.evaluate('a[0.1]', { a: [4, 3, 2, 1] }), 4); }); it('a[3]', function () { - assert.strictEqual(Parser.evaluate('a[3]', { a: [ 4, 3, 2, 1 ] }), 1); + assert.strictEqual(Parser.evaluate('a[3]', { a: [4, 3, 2, 1] }), 1); }); it('a[3 - 2]', function () { - assert.strictEqual(Parser.evaluate('a[3 - 2]', { a: [ 4, 3, 2, 1 ] }), 3); + assert.strictEqual(Parser.evaluate('a[3 - 2]', { a: [4, 3, 2, 1] }), 3); }); it('a["foo"]', function () { assert.strictEqual(Parser.evaluate('a["foo"]', { a: { foo: 'bar' } }), undefined); }); it('a[2]^3', function () { - assert.strictEqual(Parser.evaluate('a[2]^3', { a: [ 1, 2, 3, 4 ] }), 27); + assert.strictEqual(Parser.evaluate('a[2]^3', { a: [1, 2, 3, 4] }), 27); }); }); @@ -959,9 +959,9 @@ describe('Operators', function () { assert.ok(isNaN(parser.evaluate('cbrt(0/0)'))); assert.strictEqual(parser.evaluate('cbrt -1'), -1); assert.strictEqual(parser.evaluate('cbrt 0'), 0); - assert.strictEqual(parser.evaluate('cbrt(-1/0)'), -1/0); + assert.strictEqual(parser.evaluate('cbrt(-1/0)'), -1 / 0); assert.strictEqual(parser.evaluate('cbrt 1'), 1); - assert.strictEqual(parser.evaluate('cbrt(1/0)'), 1/0); + assert.strictEqual(parser.evaluate('cbrt(1/0)'), 1 / 0); assertCloseTo(parser.evaluate('cbrt 2'), 1.2599210498948732, delta); assertCloseTo(parser.evaluate('cbrt -2'), -1.2599210498948732, delta); assert.strictEqual(parser.evaluate('cbrt 8'), 2); @@ -997,7 +997,7 @@ describe('Operators', function () { var delta = 1e-15; assert.ok(isNaN(parser.evaluate('log1p(0/0)'))); - assert.strictEqual(parser.evaluate('log1p -1'), -1/0); + assert.strictEqual(parser.evaluate('log1p -1'), -1 / 0); assert.strictEqual(parser.evaluate('log1p 0'), 0); assertCloseTo(parser.evaluate('log1p 1'), 0.6931471805599453, delta); assert.ok(isNaN(parser.evaluate('log1p -2'))); @@ -1014,7 +1014,7 @@ describe('Operators', function () { assert.ok(isNaN(parser.evaluate('log2(0/0)'))); assert.ok(isNaN(parser.evaluate('log2 -1'))); - assert.strictEqual(parser.evaluate('log2 0'), -1/0); + assert.strictEqual(parser.evaluate('log2 0'), -1 / 0); assert.strictEqual(Parser.evaluate('log2 1'), 0); assert.strictEqual(Parser.evaluate('log2 2'), 1); assert.strictEqual(Parser.evaluate('log2 3'), 1.584962500721156);
test/parser.js+44 −35 modified@@ -234,7 +234,16 @@ describe('Parser', function () { assert.strictEqual(parser.parse('sin;').toString(), '(sin)'); assert.strictEqual(parser.parse('(sin)').toString(), 'sin'); assert.strictEqual(parser.parse('sin; (2)^3').toString(), '(sin;(2 ^ 3))'); - assert.deepStrictEqual(parser.parse('f(sin, sqrt)').evaluate({ f: function (a, b) { return [ a, b ]; }}), [ Math.sin, Math.sqrt ]); + /* To pass a test function f to be used within an expression, you must register it. + * The old (<3.0.0), insecure pattern of passing the function directly in the evaluate context: + * `parser.parse('f(sin)').evaluate({ f: myFunc })` will now throw an exception. + * Instead, you must pre-register the function, which is often best done using a + * concise Immediate Invoked Function Expression (IIFE) for test cases: + * `(function() { parser.functions.f = myFunc; return parser.parse('f(sin)').evaluate();})()`. + * This explicit registration guarantees the function is trusted on the Parser level, + * and safe for execution. + */ + assert.deepStrictEqual((function () { parser.functions.f = function (a, b) { return [a, b]; }; return parser.parse('f(sin, sqrt)').evaluate(); })(), [Math.sin, Math.sqrt]); assert.strictEqual(parser.parse('sin').evaluate(), Math.sin); assert.strictEqual(parser.parse('cos;').evaluate(), Math.cos); assert.strictEqual(parser.parse('cos;tan').evaluate(), Math.tan); @@ -259,31 +268,31 @@ describe('Parser', function () { }); it('should parse valid variable names correctly', function () { - assert.deepStrictEqual(parser.parse('a').variables(), [ 'a' ]); - assert.deepStrictEqual(parser.parse('abc').variables(), [ 'abc' ]); - assert.deepStrictEqual(parser.parse('a+b').variables(), [ 'a', 'b' ]); - assert.deepStrictEqual(parser.parse('ab+c').variables(), [ 'ab', 'c' ]); - assert.deepStrictEqual(parser.parse('a1').variables(), [ 'a1' ]); - assert.deepStrictEqual(parser.parse('a_1').variables(), [ 'a_1' ]); - assert.deepStrictEqual(parser.parse('a_').variables(), [ 'a_' ]); - assert.deepStrictEqual(parser.parse('a_c').variables(), [ 'a_c' ]); - assert.deepStrictEqual(parser.parse('A').variables(), [ 'A' ]); - assert.deepStrictEqual(parser.parse('ABC').variables(), [ 'ABC' ]); - assert.deepStrictEqual(parser.parse('A+B').variables(), [ 'A', 'B' ]); - assert.deepStrictEqual(parser.parse('AB+C').variables(), [ 'AB', 'C' ]); - assert.deepStrictEqual(parser.parse('A1').variables(), [ 'A1' ]); - assert.deepStrictEqual(parser.parse('A_1').variables(), [ 'A_1' ]); - assert.deepStrictEqual(parser.parse('A_C').variables(), [ 'A_C' ]); - assert.deepStrictEqual(parser.parse('abcdefg/hijklmnop+qrstuvwxyz').variables(), [ 'abcdefg', 'hijklmnop', 'qrstuvwxyz' ]); - assert.deepStrictEqual(parser.parse('ABCDEFG/HIJKLMNOP+QRSTUVWXYZ').variables(), [ 'ABCDEFG', 'HIJKLMNOP', 'QRSTUVWXYZ' ]); - assert.deepStrictEqual(parser.parse('abc123+def456*ghi789/jkl0').variables(), [ 'abc123', 'def456', 'ghi789', 'jkl0' ]); - assert.deepStrictEqual(parser.parse('_').variables(), [ '_' ]); - assert.deepStrictEqual(parser.parse('_x').variables(), [ '_x' ]); - assert.deepStrictEqual(parser.parse('$x').variables(), [ '$x' ]); - assert.deepStrictEqual(parser.parse('$xyz').variables(), [ '$xyz' ]); - assert.deepStrictEqual(parser.parse('$a_sdf').variables(), [ '$a_sdf' ]); - assert.deepStrictEqual(parser.parse('$xyz_123').variables(), [ '$xyz_123' ]); - assert.deepStrictEqual(parser.parse('_xyz_123').variables(), [ '_xyz_123' ]); + assert.deepStrictEqual(parser.parse('a').variables(), ['a']); + assert.deepStrictEqual(parser.parse('abc').variables(), ['abc']); + assert.deepStrictEqual(parser.parse('a+b').variables(), ['a', 'b']); + assert.deepStrictEqual(parser.parse('ab+c').variables(), ['ab', 'c']); + assert.deepStrictEqual(parser.parse('a1').variables(), ['a1']); + assert.deepStrictEqual(parser.parse('a_1').variables(), ['a_1']); + assert.deepStrictEqual(parser.parse('a_').variables(), ['a_']); + assert.deepStrictEqual(parser.parse('a_c').variables(), ['a_c']); + assert.deepStrictEqual(parser.parse('A').variables(), ['A']); + assert.deepStrictEqual(parser.parse('ABC').variables(), ['ABC']); + assert.deepStrictEqual(parser.parse('A+B').variables(), ['A', 'B']); + assert.deepStrictEqual(parser.parse('AB+C').variables(), ['AB', 'C']); + assert.deepStrictEqual(parser.parse('A1').variables(), ['A1']); + assert.deepStrictEqual(parser.parse('A_1').variables(), ['A_1']); + assert.deepStrictEqual(parser.parse('A_C').variables(), ['A_C']); + assert.deepStrictEqual(parser.parse('abcdefg/hijklmnop+qrstuvwxyz').variables(), ['abcdefg', 'hijklmnop', 'qrstuvwxyz']); + assert.deepStrictEqual(parser.parse('ABCDEFG/HIJKLMNOP+QRSTUVWXYZ').variables(), ['ABCDEFG', 'HIJKLMNOP', 'QRSTUVWXYZ']); + assert.deepStrictEqual(parser.parse('abc123+def456*ghi789/jkl0').variables(), ['abc123', 'def456', 'ghi789', 'jkl0']); + assert.deepStrictEqual(parser.parse('_').variables(), ['_']); + assert.deepStrictEqual(parser.parse('_x').variables(), ['_x']); + assert.deepStrictEqual(parser.parse('$x').variables(), ['$x']); + assert.deepStrictEqual(parser.parse('$xyz').variables(), ['$xyz']); + assert.deepStrictEqual(parser.parse('$a_sdf').variables(), ['$a_sdf']); + assert.deepStrictEqual(parser.parse('$xyz_123').variables(), ['$xyz_123']); + assert.deepStrictEqual(parser.parse('_xyz_123').variables(), ['_xyz_123']); }); it('should not parse invalid variables', function () { @@ -351,14 +360,14 @@ describe('Parser', function () { add: true, sqrt: true, divide: true, - 'in': true, + in: true, assignment: true } }); assert.strictEqual(parser.evaluate('+(-1)'), -1); assert.strictEqual(parser.evaluate('sqrt(16)'), 4); assert.strictEqual(parser.evaluate('4 / 6'), 2 / 3); - assert.strictEqual(parser.evaluate('3 in array', { array: [ 1, 2, 3 ] }), true); + assert.strictEqual(parser.evaluate('3 in array', { array: [1, 2, 3] }), true); assert.strictEqual(parser.evaluate('x = 4', { x: 2 }), 4); }); }); @@ -431,23 +440,23 @@ describe('Parser', function () { it('should allow in operator to be enabled', function () { var parser = new Parser({ operators: { - 'in': true + in: true } }); assert.throws(function () { parser.parse('5 * in'); }, Error); - assert.strictEqual(parser.evaluate('5 in a', { a: [ 2, 3, 5 ] }), true); + assert.strictEqual(parser.evaluate('5 in a', { a: [2, 3, 5] }), true); }); it('should allow in operator to be disabled', function () { var parser = new Parser({ operators: { - 'in': false + in: false } }); assert.throws(function () { parser.parse('5 in a'); }, Error); - assert.strictEqual(parser.evaluate('5 * in', { 'in': 3 }), 15); + assert.strictEqual(parser.evaluate('5 * in', { in: 3 }), 15); }); it('should allow logical operators to be disabled', function () { @@ -507,7 +516,7 @@ describe('Parser', function () { it('should allow assignment operator to be enabled', function () { var parser = new Parser({ operators: { - 'assignment': true + assignment: true } }); @@ -518,7 +527,7 @@ describe('Parser', function () { it('should allow assignment operator to be disabled', function () { var parser = new Parser({ operators: { - 'assignment': false + assignment: false } }); @@ -539,7 +548,7 @@ describe('Parser', function () { }); assert.deepStrictEqual(parser.evaluate('[1, 2, 3]'), [1, 2, 3]); - assert.strictEqual(parser.evaluate('a[0]', { a: [ 4, 2 ] }), 4); + assert.strictEqual(parser.evaluate('a[0]', { a: [4, 2] }), 4); }); it('should allow arrays to be disabled', function () {
test/security.js+54 −0 added@@ -0,0 +1,54 @@ +/* global describe, it */ + +'use strict'; + +var assert = require('assert'); +var Parser = require('../dist/bundle').Parser; +var fs = require('fs'); +var childProcess = require('child_process'); + +/* A context of potential dangerous stuff */ +var context = { + write: (path, data) => fs.writeFileSync(path, data), + cmd: (cmd) => console.log('Executing:', cmd), + exec: childProcess.execSync +}; + +describe('Security tests', function () { + it('should fail on direct function call to an unallowed function', function () { + var parser = new Parser(); + assert.throws(() => { + parser.evaluate('write("pwned.txt","Hello!")', context); + }, Error); + }); + + it('should allow IFUNDEF but keep function calls safe', function () { + var parserWithFndef = new Parser({ + operators: { fndef: true } + }); + var safeExpr = '(f(x) = x * x)(5)'; + assert.strictEqual(parserWithFndef.evaluate(safeExpr), 25, + 'Should correctly evaluate an expression with an allowed IFUNDEF.'); + var dangerousExpr = '((h(x) = write("pwned.txt", x)) + h(5))'; + assert.throws(() => { + parserWithFndef.evaluate(dangerousExpr, context); + }, Error); + }); + + it('should fail when a variable is assigned a dangerous function', function () { + var parser = new Parser(); + + var dangerousContext = { ...context, evil: context.cmd }; + + assert.throws(() => { + parser.evaluate('evil("ls -lh /")', dangerousContext); + }, Error); + }); + + it('PoC provided by researcher VU#263614 deny child exec process', function () { + var parser = new Parser(); + assert.throws(() => { + parser.evaluate('exec("whoami")', context); + }, Error); + }); +});
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-jc85-fpwf-qm7xghsathird-party-advisoryADVISORY
- kb.cert.org/vuls/id/263614ghsathird-party-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-12735ghsaADVISORY
- github.com/jorenbroekema/expr-eval/blob/460b820ba01c5aca6c5d84a7d4f1fa5d1913c67b/test/security.jsghsaWEB
- github.com/jorenbroekema/expr-eval/commit/1d71bb2ca8f98df8de00e9cc4de8fdd468a7ad43ghsaWEB
- github.com/silentmatt/expr-eval/pull/288ghsaWEB
- github.com/silentmatt/expr-eval/pull/289ghsaWEB
- www.kb.cert.org/vuls/id/263614ghsaWEB
- www.npmjs.com/package/expr-evalghsaWEB
- www.npmjs.com/package/expr-eval-forkghsaWEB
News mentions
0No linked articles in our index yet.