expr-eval-fork
A security-patched fork of expr-eval for safely parsing and evaluating math expressions in JavaScript without native eval().
Repository Health
Technical Analysis
expr-eval-fork parses a string like “2 * x + 1” into an Expression object that can be evaluated with different variable bindings, compiled into a native JavaScript function, simplified, or substituted into other expressions. It supports the usual arithmetic, comparison, and logical operators plus array literals, indexing, and functions like map, filter, fold, and join, with a configurable operator set so a consuming app can disable assignment or logical operators it doesn’t want to expose to user input.
The fork exists specifically to close a gap upstream: the original silentmatt/expr-eval had a prototype-pollution and arbitrary-function-execution vulnerability (CVE-2025-12735 / GHSA-jc85-fpwf-qm7x) that was fixed in source but never published to npm. expr-eval-fork ships that fix plus a stricter evaluate() that blocks proto/prototype/constructor member access and only allows calling functions that are explicitly registered on the parser, defined inline in the expression, or part of a small Math allow-list — with a dedicated security test suite (including researcher-submitted PoCs) guarding those regressions.
What You Get
- A compact expression parser exposed through Parser.parse / Parser.evaluate static helpers, or an instance API for repeated parsing with custom options
- Prebuilt CJS (dist/bundle.js) and ESM (dist/index.mjs) builds selected automatically via a modern package.json exports map, plus hand-maintained TypeScript typings (parser.d.ts)
- A hardened evaluate() path that blocks prototype-pollution member access and rejects calls to any function not explicitly registered, expression-defined, or on a small Math allow-list
- Expression-level utilities — simplify() for constant folding, substitute() for composing expressions, toJSFunction() for compiling to a native JS function, and variables()/symbols() for introspection
Common Use Cases
- Evaluating user-typed formulas in spreadsheet-like grid UIs
- Running configurable pricing or discount formulas stored as strings without deploying new code
- Letting non-technical users define derived-field formulas in a no-code form or report builder
- Powering a web-based scientific or engineering calculator that accepts free-text math expressions
Under The Hood
Architecture Parser (src/parser.js) builds its operator, function, and constant tables in the constructor and exposes parse(expr), which drives TokenStream (src/token-stream.js) and ParserState (src/parser-state.js) to produce a flat, postfix instruction array. Expression (src/expression.js) wraps that instruction list and delegates each capability to a standalone module — evaluate.js (a stack-machine interpreter over the tokens), simplify.js (constant folding), substitute.js (expression composition), expression-to-string.js (used by toJSFunction), and get-symbols.js (variable/symbol extraction) — rather than putting all behavior directly on the Expression prototype, keeping each concern independently testable. Security enforcement is centralized entirely inside evaluate.js’s isAllowedFunc gate, so that one module is the single point where the sandboxing model lives or breaks.
Tech Stack Pure JavaScript with zero runtime dependencies, built with Rollup (rollup.config.js, rollup-min.config.js, rollup-esm.config.js) into CJS and ESM bundles selected through package.json’s exports map, with TypeScript consumers served by a hand-written parser.d.ts. Tests run under Mocha with nyc for coverage, and linting uses ESLint with eslint-config-semistandard. CI is still configured via a Travis file targeting Node 6 through 12 — stale relative to the package’s own engines requirement of Node >=16.9.0, suggesting the fork inherited its parent’s tooling without fully modernizing it.
Code Quality
Test coverage is substantial: test/expression.js, test/parser.js, test/operators.js, and test/functions.js each run into the hundreds of lines, plus a dedicated test/security.js covering prototype-pollution and arbitrary-function-execution regressions with researcher-submitted proof-of-concept expressions tied to specific CVEs. Code is written in consistent ES5-style semistandard JavaScript (no classes, var-based constructor functions) rather than TypeScript source. Error handling is explicit — evaluate.js throws precise, specific messages (‘prototype access detected’, ‘Is not an allowed function’) at each unsafe branch rather than swallowing failures. There is no active CI gate running these tests automatically since the Travis configuration is defunct, leaving npm test as a manual pre-publish step.
API Design
Parser.evaluate(expr, values) and the static Parser.parse/Parser.evaluate shortcuts take a caller from a string to a result in one call, while new Parser(options) supports the common progression of starting simple and later disabling specific operators or enabling member access. Naming mirrors JavaScript’s own Math object (sin, sqrt, atan2), and toJSFunction/simplify/substitute/variables/symbols each read as verbs matching what they return. Documentation is strong for a project this size, with a full operator-precedence table and per-function reference in the README, but the fork’s actual security-relevant breaking change — that evaluate() no longer accepts arbitrary context functions the way it once did — is explained only in the CHANGELOG and linked CVE, not in the README itself, so an upgrading consumer has to dig through commit history to understand it.