From 432900f73f1660c6fa9c25b90f3c8f2939b749f6 Mon Sep 17 00:00:00 2001 From: Renae Metcalf Date: Tue, 4 Nov 2025 14:44:39 -0500 Subject: [PATCH 01/17] Remove user-defined function ability and member access --- src/evaluate.js | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/evaluate.js b/src/evaluate.js index f23cef7e..af15b521 100644 --- a/src/evaluate.js +++ b/src/evaluate.js @@ -1,4 +1,4 @@ -import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY } from './instruction'; +import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IEXPR, IEXPREVAL, IENDSTATEMENT, IARRAY } from './instruction'; export default function evaluate(tokens, expr, values) { var nstack = []; @@ -72,38 +72,10 @@ export default function evaluate(tokens, expr, values) { } else { throw new Error(f + ' is not a function'); } - } else if (type === IFUNDEF) { - // Create closure to keep references to arguments and expression - nstack.push((function () { - var n2 = nstack.pop(); - var args = []; - var argCount = item.value; - while (argCount-- > 0) { - args.unshift(nstack.pop()); - } - var n1 = nstack.pop(); - var f = function () { - var scope = Object.assign({}, values); - for (var i = 0, len = args.length; i < len; i++) { - scope[args[i]] = arguments[i]; - } - return evaluate(n2, expr, scope); - }; - // f.name = n1 - Object.defineProperty(f, 'name', { - value: n1, - writable: false - }); - values[n1] = f; - return f; - })()); } else if (type === IEXPR) { nstack.push(createExpressionEvaluator(item, expr, values)); } else if (type === IEXPREVAL) { nstack.push(item); - } else if (type === IMEMBER) { - n1 = nstack.pop(); - nstack.push(n1[item.value]); } else if (type === IENDSTATEMENT) { nstack.pop(); } else if (type === IARRAY) { From 146ff83a4fe31b8bf91bd74d54889405a5a23c49 Mon Sep 17 00:00:00 2001 From: Renae Metcalf Date: Tue, 4 Nov 2025 14:48:01 -0500 Subject: [PATCH 02/17] restrict functions to ./functions.js --- src/evaluate.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/evaluate.js b/src/evaluate.js index af15b521..caf6250f 100644 --- a/src/evaluate.js +++ b/src/evaluate.js @@ -67,6 +67,16 @@ export default function evaluate(tokens, expr, values) { args.unshift(resolveExpression(nstack.pop(), values)); } f = nstack.pop(); + var isAllowedFunc = false; + for (var key in expr.functions) { + if (expr.functions[key] === f) { + isAllowedFunction = true; + break; + } + } + if (!isAllowedFunc) { + throw new Error('Is not an allowed function.') + } if (f.apply && f.call) { nstack.push(f.apply(undefined, args)); } else { From 955f390dd395f51ecef7a12ae382042a3dac5b45 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 4 Nov 2025 18:00:00 -0500 Subject: [PATCH 03/17] Updated 2.0.3 for secure expr-eval --- src/evaluate.js | 85 +++++++++++++++++++++++++++++++++++++++-------- test/operators.js | 8 ++--- test/parser.js | 2 +- test/security.js | 68 +++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 test/security.js diff --git a/src/evaluate.js b/src/evaluate.js index caf6250f..f9aa992e 100644 --- a/src/evaluate.js +++ b/src/evaluate.js @@ -1,4 +1,4 @@ -import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IEXPR, IEXPREVAL, IENDSTATEMENT, IARRAY } from './instruction'; +import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY } from './instruction'; export default function evaluate(tokens, expr, values) { var nstack = []; @@ -9,6 +9,30 @@ export default function evaluate(tokens, expr, values) { return resolveExpression(tokens, values); } + /** + * Checks if a function reference 'f' is explicitly allowed to be executed. + * This logic is the core security allowance gate. + */ + var isAllowedFunc = function (f) { + if (typeof f !== 'function') return true; + + for (var key in expr.functions) { + if (expr.functions[key] === f) return true; + } + + if (f.__expr_eval_safe_def) return true; + + for (var key in values) { + if (typeof values[key] === 'object' && values[key] !== null) { + for (var subKey in values[key]) { + if (values[key][subKey] === f) return true; + } + } + } + return false; + }; + /* --- END: LOCAL HELPER FUNCTION FOR SECURITY --- */ + var numTokens = tokens.length; for (var i = 0; i < numTokens; i++) { @@ -50,7 +74,11 @@ export default function evaluate(tokens, expr, values) { nstack.push(expr.unaryOps[item.value]); } else { var v = values[item.value]; - if (v !== undefined) { + if (v !== undefined) { + if (typeof v === 'function' && !isAllowedFunc(v)) { + /* 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); @@ -66,26 +94,57 @@ export default function evaluate(tokens, expr, values) { while (argCount-- > 0) { args.unshift(resolveExpression(nstack.pop(), values)); } - f = nstack.pop(); - var isAllowedFunc = false; - for (var key in expr.functions) { - if (expr.functions[key] === f) { - isAllowedFunction = true; - break; - } - } - if (!isAllowedFunc) { - throw new Error('Is not an allowed function.') - } + f = nstack.pop(); + + // --- FINAL SECURITY CHECK --- + if (!isAllowedFunc(f)) { + throw new Error('Is not an allowed function.'); + } + // --- END FINAL SECURITY CHECK --- + if (f.apply && f.call) { nstack.push(f.apply(undefined, args)); } else { throw new Error(f + ' is not a function'); } + } else if (type === IFUNDEF) { + // Create closure to keep references to arguments and expression + nstack.push((function () { + var n2 = nstack.pop(); + var args = []; + var argCount = item.value; + while (argCount-- > 0) { + args.unshift(nstack.pop()); + } + var n1 = nstack.pop(); + var f = function () { + var scope = Object.assign({}, values); + for (var i = 0, len = args.length; i < len; i++) { + scope[args[i]] = arguments[i]; + } + return evaluate(n2, expr, scope); + }; + // f.name = n1 + Object.defineProperty(f, 'name', { + value: n1, + writable: false + }); + // *** MARK AS SAFE FOR SECURITY CHECK *** + Object.defineProperty(f, '__expr_eval_safe_def', { + value: true, + writable: false + }); + // *************************************** + values[n1] = f; + return f; + })()); } else if (type === IEXPR) { nstack.push(createExpressionEvaluator(item, expr, values)); } else if (type === IEXPREVAL) { nstack.push(item); + } else if (type === IMEMBER) { + n1 = nstack.pop(); + nstack.push(n1[item.value]); } else if (type === IENDSTATEMENT) { nstack.pop(); } else if (type === IARRAY) { diff --git a/test/operators.js b/test/operators.js index 07d15aa5..d6656983 100644 --- a/test/operators.js +++ b/test/operators.js @@ -162,17 +162,17 @@ describe('Operators', function () { assert.strictEqual(Parser.evaluate('1 and 1 and 0'), false); }); - it('skips rhs when lhs is false', function () { + it('skips rhs when lhs is false for AND operator', function () { var notCalled = spy(returnFalse); assert.strictEqual(Parser.evaluate('false and notCalled()', { notCalled: notCalled }), false); assert.strictEqual(notCalled.called, false); }); - it('evaluates rhs when lhs is true', function () { + it('evaluates rhs when lhs is true for AND operator', 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); }); }); diff --git a/test/parser.js b/test/parser.js index bf4459d2..c0d1bdc7 100644 --- a/test/parser.js +++ b/test/parser.js @@ -234,7 +234,7 @@ 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 ]); +/* assert.deepStrictEqual(parser.parse('f(sin, sqrt)').evaluate({ f: function (a, b) { return [ a, b ]; }}), [ 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); diff --git a/test/security.js b/test/security.js new file mode 100644 index 00000000..1ebd573b --- /dev/null +++ b/test/security.js @@ -0,0 +1,68 @@ +'use strict'; +var assert = require('assert'); +var Parser = require('../dist/bundle').Parser; +var fs = require('fs'); +var child_process = require('child_process'); + +// 1. Setup the Dangerous Context +var context = { + write: (path, data) => fs.writeFileSync(path, data), + exec: (cmd) => console.log('Executing:', cmd) +}; + +// --- Test Case 1: Direct Call to a Dangerous Global/Context Function --- +it('should fail on direct function call to an unallowed function', function () { + var parser = new Parser(); + + // The evaluator should throw an error because 'write' is not an allowed function + assert.throws(() => { + parser.evaluate('write("pwned.txt","Hello!")', context); + }, Error); +}); + +// --- Test Case 2: Function Definition (IFUNDEF) should still be safe --- +it('should allow IFUNDEF but keep function calls safe', function () { + // Enable IFUNDEF + var parserWithFndef = new Parser({ + operators: { fndef: true } + }); + + // Expression: define an expression-internal function 'g' and call it. + // 'g' itself is safe because it only uses safe operators. + var safeExpr = '(f(x) = x * x)(5)'; + + assert.equal(parserWithFndef.evaluate(safeExpr), 25, + 'Should correctly evaluate an expression with an allowed IFUNDEF.'); + + // Expression: Define a function 'h' that calls the *unallowed* function 'write' + var dangerousExpr = '((h(x) = write("pwned.txt", x)) + h(5))'; + + // The call to 'write' inside the expression-defined function 'h' should still fail + assert.throws(() => { + parserWithFndef.evaluate(dangerousExpr, context); + }, Error); +}); + +// --- Test Case 3: Variable Assignment to a Dangerous Function --- +it('should fail when a variable is assigned a dangerous function', function () { + var parser = new Parser(); + + // The variable 'evil' points to the 'exec' function from the context + var dangerousContext = { ...context, evil: context.exec }; + + // The evaluator should throw an error because 'evil' is a variable, + // but the final call attempts to execute a function that is not in expr.functions. + 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(); + var context = { + exec: child_process.execSync + }; + assert.throws(() => { + parser.evaluate('exec("whoami")', context); + }, Error); +}); From 084b27cbef138ecffee0bf4069e7d779a247cd96 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 4 Nov 2025 18:09:04 -0500 Subject: [PATCH 04/17] Update the parser tests with parser.functions --- test/parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parser.js b/test/parser.js index c0d1bdc7..1a3c1511 100644 --- a/test/parser.js +++ b/test/parser.js @@ -234,7 +234,7 @@ 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 ]); */ + 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); From 0cf073c21237b7e7cd5d371a382f8f7011e3fcd8 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 4 Nov 2025 18:15:21 -0500 Subject: [PATCH 05/17] Updates to security.js --- test/security.js | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/test/security.js b/test/security.js index 1ebd573b..d688d488 100644 --- a/test/security.js +++ b/test/security.js @@ -4,64 +4,45 @@ var Parser = require('../dist/bundle').Parser; var fs = require('fs'); var child_process = require('child_process'); -// 1. Setup the Dangerous Context +/* A context of potential dangerous stuff */ var context = { write: (path, data) => fs.writeFileSync(path, data), - exec: (cmd) => console.log('Executing:', cmd) + cmd: (cmd) => console.log('Executing:', cmd), + exec: child_process.execSync }; -// --- Test Case 1: Direct Call to a Dangerous Global/Context Function --- it('should fail on direct function call to an unallowed function', function () { var parser = new Parser(); - - // The evaluator should throw an error because 'write' is not an allowed function assert.throws(() => { parser.evaluate('write("pwned.txt","Hello!")', context); }, Error); }); -// --- Test Case 2: Function Definition (IFUNDEF) should still be safe --- it('should allow IFUNDEF but keep function calls safe', function () { - // Enable IFUNDEF var parserWithFndef = new Parser({ operators: { fndef: true } }); - - // Expression: define an expression-internal function 'g' and call it. - // 'g' itself is safe because it only uses safe operators. var safeExpr = '(f(x) = x * x)(5)'; - - assert.equal(parserWithFndef.evaluate(safeExpr), 25, - 'Should correctly evaluate an expression with an allowed IFUNDEF.'); - - // Expression: Define a function 'h' that calls the *unallowed* function 'write' + assert.equal(parserWithFndef.evaluate(safeExpr), 25, + 'Should correctly evaluate an expression with an allowed IFUNDEF.'); var dangerousExpr = '((h(x) = write("pwned.txt", x)) + h(5))'; - - // The call to 'write' inside the expression-defined function 'h' should still fail assert.throws(() => { parserWithFndef.evaluate(dangerousExpr, context); }, Error); }); -// --- Test Case 3: Variable Assignment to a Dangerous Function --- it('should fail when a variable is assigned a dangerous function', function () { var parser = new Parser(); - - // The variable 'evil' points to the 'exec' function from the context - var dangerousContext = { ...context, evil: context.exec }; - - // The evaluator should throw an error because 'evil' is a variable, - // but the final call attempts to execute a function that is not in expr.functions. + + 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() { +it('PoC provided by researcher VU#263614 deny child exec process', function() { var parser = new Parser(); - var context = { - exec: child_process.execSync - }; assert.throws(() => { parser.evaluate('exec("whoami")', context); }, Error); From ae20d0a59fce5b82ec97da641ca9da528f70e55f Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 4 Nov 2025 18:16:25 -0500 Subject: [PATCH 06/17] Updated expr-eval package --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 36260e00..1dc5ca19 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "expr-eval", - "version": "2.0.2", - "description": "Mathematical expression evaluator", + "version": "2.0.3", + "description": "Mathematical expression evaluator with security", "main": "dist/bundle.js", "module": "dist/index.mjs", "typings": "parser.d.ts", From 6a2bd624ace8a515a5459bd48f52d63a3c9776e9 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 4 Nov 2025 18:34:25 -0500 Subject: [PATCH 07/17] npm updates were run --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 1dc5ca19..48177249 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,17 @@ }, "dependencies": {}, "devDependencies": { - "eslint": "^6.3.0", - "eslint-config-semistandard": "^15.0.0", - "eslint-config-standard": "^13.0.1", - "eslint-plugin-import": "^2.15.0", - "eslint-plugin-node": "^9.2.0", - "eslint-plugin-promise": "^4.0.1", - "eslint-plugin-standard": "^4.0.0", - "mocha": "^6.2.0", - "nyc": "^14.1.1", - "rollup": "^1.20.3", - "rollup-plugin-uglify": "^6.0.3" + "eslint": "^9.39.1", + "eslint-config-semistandard": "^17.0.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-standard": "^5.0.0", + "mocha": "^11.7.4", + "nyc": "^17.1.0", + "rollup": "^4.52.5", + "rollup-plugin-uglify": "^6.0.4" }, "scripts": { "test": "npm run build && mocha", From 1af79833e89a6f60b7ab0ce4893b1250d75e3c3e Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 4 Nov 2025 18:58:14 -0500 Subject: [PATCH 08/17] Updates devDependencies --- package.json | 7 +++---- rollup-esm.config.js | 22 +++++++++++++++++----- rollup-min.config.js | 24 +++++++++++++++++++----- rollup.config.js | 3 ++- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 48177249..215f00ba 100644 --- a/package.json +++ b/package.json @@ -8,19 +8,18 @@ "directories": { "test": "test" }, - "dependencies": {}, "devDependencies": { - "eslint": "^9.39.1", + "eslint": "^8.57.0", "eslint-config-semistandard": "^17.0.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-promise": "^6.0.0", "eslint-plugin-standard": "^5.0.0", "mocha": "^11.7.4", "nyc": "^17.1.0", "rollup": "^4.52.5", - "rollup-plugin-uglify": "^6.0.4" + "@rollup/plugin-terser": "^0.4.4" }, "scripts": { "test": "npm run build && mocha", diff --git a/rollup-esm.config.js b/rollup-esm.config.js index d530a43f..d4b84db2 100644 --- a/rollup-esm.config.js +++ b/rollup-esm.config.js @@ -1,7 +1,19 @@ -import rollupConfig from './rollup.config'; +// rollup-esm.config.js (Converted to CommonJS) -rollupConfig.plugins = []; -rollupConfig.output.file = 'dist/index.mjs'; -rollupConfig.output.format = 'esm'; +// 1. Use require() to import the base config +const rollupConfig = require('./rollup.config.js'); -export default rollupConfig; +// Create a copy of the base configuration object to avoid modifying it +// This is critical since 'rollupConfig' is a shared object. +const esmConfig = { ...rollupConfig }; + +// 2. Apply ESM-specific changes to the copied object +esmConfig.plugins = []; // No minification for the base ESM file +esmConfig.output = { + ...rollupConfig.output, + file: 'dist/index.mjs', + format: 'esm' +}; + +// 3. Use module.exports to export the configuration +module.exports = esmConfig; diff --git a/rollup-min.config.js b/rollup-min.config.js index 222fafa3..fb397603 100644 --- a/rollup-min.config.js +++ b/rollup-min.config.js @@ -1,7 +1,21 @@ -import rollupConfig from './rollup.config'; -import { uglify } from 'rollup-plugin-uglify'; +// rollup-min.config.js (Correct CommonJS Syntax) -rollupConfig.plugins = [ uglify() ]; -rollupConfig.output.file = 'dist/bundle.min.js'; +// 1. Use require() to import the base config and the terser plugin +const rollupConfig = require('./rollup.config.js'); +const terser = require('@rollup/plugin-terser'); -export default rollupConfig; +// Create a shallow copy of the base configuration to avoid side effects +const minifiedConfig = { + ...rollupConfig, + // Ensure the output property is also copied, if it exists + output: { ...rollupConfig.output } +}; + +// 2. Set the plugins to ONLY include the terser plugin +minifiedConfig.plugins = [ terser() ]; + +// 3. Update the file name +minifiedConfig.output.file = 'dist/bundle.min.js'; + +// 4. Use module.exports to export the configuration +module.exports = minifiedConfig; diff --git a/rollup.config.js b/rollup.config.js index 1c55fc75..6d2d7b65 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,5 @@ -export default { +// rollup.config.js (CommonJS syntax) +module.exports = { input: 'index.js', output: { file: 'dist/bundle.js', From bb9394ee086799eb8ced42b30b6343b282034f60 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Thu, 6 Nov 2025 11:00:08 -0500 Subject: [PATCH 09/17] Updates to README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 1472abbe..41937d32 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,23 @@ JavaScript Expression Evaluator [![CDNJS version](https://img.shields.io/cdnjs/v/expr-eval.svg?maxAge=3600)](https://cdnjs.com/libraries/expr-eval) [![Build Status](https://travis-ci.org/silentmatt/expr-eval.svg?branch=master)](https://travis-ci.org/silentmatt/expr-eval) +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. +If expr-eval ever gets released, raise an issue and we'll deprecate this fork. + +This fork addresses a security vulnerability identified by CVE-2025-12735. +An important update with a strict allow-list security model to prevent +Code Injection (CWE-94) and Prototype Pollution (CWE-1321) vulnerabilities. +To achieve this, the expression evaluator no longer allows arbitrary +functions to be passed directly into the evaluation context +`(.evaluate({ myFunc: function() { ... } }))`. Any external function that +is intended for use in an expression must be explicitly registered with +the Parser instance via the parser.functions map prior to evaluation. This +impacts very few test cases that have been updated in test/*.js. A new +`test/security.js` highlights the attacks against vulnerbaility +CVE-2025-12735 that will be prevented. + + Description ------------------------------------- From bf1123166bffd04c43ee94aa11e0bb7804259ada Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Thu, 6 Nov 2025 11:06:20 -0500 Subject: [PATCH 10/17] Added notes to test/parser.js --- test/parser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/parser.js b/test/parser.js index 1a3c1511..bd3831c9 100644 --- a/test/parser.js +++ b/test/parser.js @@ -234,6 +234,7 @@ 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))'); + /* After this update, to pass a test function f to be used within an expression, you must now register it. The old, 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 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); From 46b2b83a103a3539faf9e515c014fc624c926343 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Thu, 6 Nov 2025 11:09:42 -0500 Subject: [PATCH 11/17] Bumped version big jump and small updates from Gemini feedback --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 215f00ba..847cca04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "expr-eval", - "version": "2.0.3", + "version": "3.0.1", "description": "Mathematical expression evaluator with security", "main": "dist/bundle.js", "module": "dist/index.mjs", From 85b96bdebd33ac2d7177cd78344cb29ae7bc3d0a Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Mon, 17 Nov 2025 23:25:17 -0500 Subject: [PATCH 12/17] Vijay's temp fix for MEMBER vulnerability --- src/evaluate.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/evaluate.js b/src/evaluate.js index f9aa992e..746debc6 100644 --- a/src/evaluate.js +++ b/src/evaluate.js @@ -14,22 +14,29 @@ export default function evaluate(tokens, expr, values) { * This logic is the core security allowance gate. */ var isAllowedFunc = function (f) { - if (typeof f !== 'function') return true; + if (typeof f !== 'function') return 1; for (var key in expr.functions) { - if (expr.functions[key] === f) return true; + if (expr.functions[key] === f) return 2; } - if (f.__expr_eval_safe_def) return true; + if (f.__expr_eval_safe_def) return 3; for (var key in values) { if (typeof values[key] === 'object' && values[key] !== null) { for (var subKey in values[key]) { - if (values[key][subKey] === f) return true; + console.log(f,values); + if (values[key][subKey] === f) { + const tf = values[key][subKey]; + for (var key in expr.functions) { + if (expr.functions[key] === tf) return 4; + } + } } } } - return false; + + return 0; }; /* --- END: LOCAL HELPER FUNCTION FOR SECURITY --- */ @@ -143,7 +150,17 @@ export default function evaluate(tokens, expr, values) { } else if (type === IEXPREVAL) { nstack.push(item); } else if (type === IMEMBER) { - n1 = nstack.pop(); + n1 = nstack.pop(); + if (/^__proto__|prototype|constructor$/.test(item.value)) { + throw new Error('prototype access detected in MEMBER'); + } + console.log(typeof(n1[item.value])); + console.log(isAllowedFunc(n1[item.value])); + if(typeof(n1) === "object" && (typeof(n1[item.value]) === "function") + && (!isAllowedFunc(n1[item.value]))) { + console.log(n1[item.value]); + throw new Error('Is not an allowed function in MEMBER.'); + } nstack.push(n1[item.value]); } else if (type === IENDSTATEMENT) { nstack.pop(); From 1a3b379d1e540b74799a275baf922cfbb51a1f4b Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 18 Nov 2025 00:53:43 -0500 Subject: [PATCH 13/17] Almost fixed second vul --- src/evaluate.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/evaluate.js b/src/evaluate.js index 746debc6..b7ee6125 100644 --- a/src/evaluate.js +++ b/src/evaluate.js @@ -15,27 +15,26 @@ export default function evaluate(tokens, expr, values) { */ var isAllowedFunc = function (f) { if (typeof f !== 'function') return 1; - for (var key in expr.functions) { if (expr.functions[key] === f) return 2; } - - if (f.__expr_eval_safe_def) return 3; + if (f.__expr_eval_safe_def) return 4; for (var key in values) { if (typeof values[key] === 'object' && values[key] !== null) { for (var subKey in values[key]) { - console.log(f,values); if (values[key][subKey] === f) { const tf = values[key][subKey]; for (var key in expr.functions) { - if (expr.functions[key] === tf) return 4; + if (expr.functions[key] === tf) return 5; + } + for (var key of Object.getOwnPropertyNames(Math)) { + if(Math[key] === tf) return 6; } } } } } - return 0; }; /* --- END: LOCAL HELPER FUNCTION FOR SECURITY --- */ @@ -136,6 +135,9 @@ export default function evaluate(tokens, expr, values) { value: n1, writable: false }); + if (f.__expr_eval_safe_def) { + throw new Error("User self-defined safe definition found"); + } // *** MARK AS SAFE FOR SECURITY CHECK *** Object.defineProperty(f, '__expr_eval_safe_def', { value: true, @@ -154,8 +156,6 @@ export default function evaluate(tokens, expr, values) { if (/^__proto__|prototype|constructor$/.test(item.value)) { throw new Error('prototype access detected in MEMBER'); } - console.log(typeof(n1[item.value])); - console.log(isAllowedFunc(n1[item.value])); if(typeof(n1) === "object" && (typeof(n1[item.value]) === "function") && (!isAllowedFunc(n1[item.value]))) { console.log(n1[item.value]); From 039e419610f8b07846c9a9f4f1a7171bd34c2953 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 18 Nov 2025 01:11:10 -0500 Subject: [PATCH 14/17] A new fix for MEMBER sub property vul --- src/evaluate.js | 1 - test/operators.js | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/evaluate.js b/src/evaluate.js index b7ee6125..cee26a9d 100644 --- a/src/evaluate.js +++ b/src/evaluate.js @@ -158,7 +158,6 @@ export default function evaluate(tokens, expr, values) { } if(typeof(n1) === "object" && (typeof(n1[item.value]) === "function") && (!isAllowedFunc(n1[item.value]))) { - console.log(n1[item.value]); throw new Error('Is not an allowed function in MEMBER.'); } nstack.push(n1[item.value]); diff --git a/test/operators.js b/test/operators.js index d6656983..507ef9ab 100644 --- a/test/operators.js +++ b/test/operators.js @@ -171,8 +171,9 @@ describe('Operators', function () { it('evaluates rhs when lhs is true for AND operator', function () { var called = spy(returnFalse); - - assert.strictEqual(Parser.evaluate('true and spies.called()', { spies: {called: called }}), false); + var parser = new Parser; + parser.functions = {f: called}; + assert.strictEqual(parser.evaluate('true and spies.called()', { spies: {called: called }}), false); assert.strictEqual(called.called, true); }); }); @@ -211,8 +212,9 @@ describe('Operators', function () { it('evaluates rhs when lhs is false', function () { var called = spy(returnTrue); - - assert.strictEqual(Parser.evaluate('false or spies.called()', { spies: {called: called }}), true); + var parser = new Parser(); + parser.functions = {f: called}; + assert.strictEqual(parser.evaluate('false or spies.called()', { spies: {called: called }}), true); assert.strictEqual(called.called, true); }); }); From 25cf147b13d233925b552b0e34e4c1c2a8d0fd12 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 18 Nov 2025 17:18:03 -0500 Subject: [PATCH 15/17] Updated test cases to include @baoquanh Issue #289 --- test/security.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/security.js b/test/security.js index d688d488..d61ba0e8 100644 --- a/test/security.js +++ b/test/security.js @@ -47,3 +47,18 @@ it('PoC provided by researcher VU#263614 deny child exec process', function() { parser.evaluate('exec("whoami")', context); }, Error); }); +it('PoC provided by researcher https://github.com/silentmatt/expr-eval/issues/289 by gitHub @baoquanh', function() { + var context = { + write: (path, data) => fs.writeFileSync(path, data), + cmd: (cmd) => console.log('Executing:', cmd), + exec: child_process.execSync + }; + + var baoquanh = { + test: context + } + var parser = new Parser(); + assert.throws(() => { + parser.evaluate('test.write("pwned.txt","Hello!")', baoquanh) + }, Error); +}) From e312ed551667fed61def43007e51822c34341402 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 18 Nov 2025 17:28:39 -0500 Subject: [PATCH 16/17] Update CHANGELOG.md with newer fix --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d026c779..cede8856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,22 @@ # Changelog +## [3.0.1] - 2025-11-18 +- Incomplete fix to [CVE-2025-12735](https://github.com/advisories/GHSA-jc85-fpwf-qm7x) has been updated to address the issue identified by @baoquanh in silentmatt#289 + +## [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 From 3e468c2088f4a37dff9b74c53b1a80f6df524fe6 Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Tue, 18 Nov 2025 17:48:20 -0500 Subject: [PATCH 17/17] Switched states to true/false --- src/evaluate.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/evaluate.js b/src/evaluate.js index cee26a9d..51eb02a4 100644 --- a/src/evaluate.js +++ b/src/evaluate.js @@ -14,11 +14,11 @@ export default function evaluate(tokens, expr, values) { * This logic is the core security allowance gate. */ var isAllowedFunc = function (f) { - if (typeof f !== 'function') return 1; + if (typeof f !== 'function') return true; for (var key in expr.functions) { - if (expr.functions[key] === f) return 2; + if (expr.functions[key] === f) return true; } - if (f.__expr_eval_safe_def) return 4; + if (f.__expr_eval_safe_def) return true; for (var key in values) { if (typeof values[key] === 'object' && values[key] !== null) { @@ -26,16 +26,16 @@ export default function evaluate(tokens, expr, values) { if (values[key][subKey] === f) { const tf = values[key][subKey]; for (var key in expr.functions) { - if (expr.functions[key] === tf) return 5; + if (expr.functions[key] === tf) return true; } for (var key of Object.getOwnPropertyNames(Math)) { - if(Math[key] === tf) return 6; + if(Math[key] === tf) return true; } } } } } - return 0; + return false; }; /* --- END: LOCAL HELPER FUNCTION FOR SECURITY --- */ @@ -135,9 +135,6 @@ export default function evaluate(tokens, expr, values) { value: n1, writable: false }); - if (f.__expr_eval_safe_def) { - throw new Error("User self-defined safe definition found"); - } // *** MARK AS SAFE FOR SECURITY CHECK *** Object.defineProperty(f, '__expr_eval_safe_def', { value: true,