PoC Archive PoC Archive
Critical CVE-2025-12735 unpatched

safe-expr-eval: Mitigation Library for the expr-eval Unsafe eval() RCE (CVE-2025-12735)

by Alejandro Castrillon (alecasg555) · 2026-07-06

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-12735
Category
misc
Affected product
expr-eval npm package (mathematical/logical expression evaluator)
Affected versions
All versions of expr-eval, per this repository's SECURITY.md
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherAlejandro Castrillon (alecasg555)
CVE / AdvisoryCVE-2025-12735
Categorymisc
SeverityCritical
CVSS Score9.8 (per NVD)
StatusPatched
Tagsexpr-eval, eval-injection, arbitrary-code-execution, rce, cwe-95, javascript, typescript, npm, expression-parser, mitigation-library, drop-in-replacement
RelatedN/A

Affected Target

FieldValue
Software / Systemexpr-eval npm package (mathematical/logical expression evaluator)
Versions AffectedAll versions of expr-eval, per this repository’s SECURITY.md
Language / PlatformJavaScript / TypeScript, Node.js and browser bundles that embed expr-eval
Authentication RequiredNo (library-level flaw — exploitability depends on the host app exposing an expression string to untrusted input)
Network Access RequiredNot required for the flaw itself; typically Yes in practice, since expr-eval is normally reached through a web-facing feature (formula fields, pricing rules, dynamic filters, etc.)

Summary

CVE-2025-12735 is a critical arbitrary code execution vulnerability in the expr-eval npm package: instead of tokenizing and walking expressions through a restricted interpreter, expr-eval’s evaluation path ultimately reaches JavaScript’s eval()/Function() machinery, so a string such as require("child_process").execSync("...") submitted as an “expression” is executed as real JavaScript rather than rejected as an invalid formula. Any application that lets users (directly or indirectly) supply expression strings to expr-eval’s Parser.evaluate() is therefore exposed to full RCE, filesystem access, and process control. This repository, safe-expr-eval, is not an attack PoC — it is a from-scratch, zero-dependency TypeScript reimplementation of the same public API (Parser, evaluate, compile) that never touches eval() or the Function constructor, instead tokenizing input (tokenizer.ts) and walking a recursive-descent parser/evaluator (parser.ts, evaluator.ts) over a closed set of arithmetic/comparison/logical operators and an explicit function/constant whitelist. It is published as a drop-in, API-compatible replacement so that vulnerable applications can migrate away from expr-eval without rewriting call sites.


Vulnerability Details

Root Cause

The vulnerable pattern, as documented in this repo’s SECURITY.md, is that expr-eval evaluates parsed expressions by ultimately invoking JavaScript’s dynamic-code-execution primitives on attacker-influenced input:

1
2
3
4
5
6
7
8
// Vulnerable code using expr-eval
const Parser = require('expr-eval').Parser;
const parser = new Parser();

// Attacker can execute arbitrary code
parser.evaluate('process.exit()');                                    // Crashes the application
parser.evaluate('require("fs").readFileSync("/etc/passwd")');          // Reads sensitive files
parser.evaluate('require("child_process").execSync("rm -rf /")');      // System commands

Because the “expression” string is not restricted to a safe grammar before being executed, any JavaScript expression — including require(...) calls that reach Node’s built-in modules — runs with the privileges of the host process (CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code).

safe-expr-eval avoids this entirely: evaluator.ts’s ExpressionEvaluator only recognizes a fixed token grammar (NUMBER, STRING, BOOLEAN, IDENTIFIER, OPERATOR, parens, commas) produced by tokenizer.ts, and function calls are resolved only against an explicit functions registry the host application populates (parser.functions.max = Math.max, etc.) — there is no code path that can reach require, process, or the Function constructor.

Attack Vector

Original expr-eval exploitation (what this library mitigates):

  1. A vulnerable application exposes a feature (e.g., a pricing/formula engine, custom filter, or rule builder) that passes user-controlled input as an “expression” string into expr-eval’s Parser.evaluate().
  2. The attacker submits a payload crafted to look like an expression but that actually resolves to executable JavaScript, e.g. require("child_process").execSync("id").
  3. expr-eval’s evaluation path invokes eval()/Function() semantics on this input, executing the attacker’s JavaScript in the Node.js process.
  4. The attacker achieves arbitrary command execution, file read/write, or full process compromise — no authentication is inherently required beyond whatever gate the host app puts in front of the expression feature.

Mitigation path implemented by safe-expr-eval:

  1. The host application replaces require('expr-eval') with require('safe-expr-eval') (API-compatible: Parser, evaluate, compile).
  2. Expressions are tokenized (Tokenizer) and parsed into an AST-walking evaluation (ExpressionEvaluator.parseOr → parseAnd → parseNot → parseComparison → parseAdditive → parseMultiplicative → parsePrimary) that only supports arithmetic, comparison, and logical operators plus whitelisted variables/functions/constants explicitly registered by the host app.
  3. Because no eval()/Function() call ever occurs, a payload like require("child_process").execSync(...) is parsed as an undefined-identifier/function-call and throws Undefined function: require / Undefined variable: require instead of executing.

Impact

Unmitigated expr-eval usage allows an attacker to achieve arbitrary code execution in the context of the Node.js process handling expression evaluation — including reading/writing arbitrary files, spawning OS commands, and fully compromising the host application and any data it can reach. safe-expr-eval eliminates this impact class for applications that migrate to it.


Environment / Lab Setup

Vulnerable component: expr-eval (any version), Node.js host application exposing Parser.evaluate() to user input
Mitigation library:   safe-expr-eval v1.0.0+ (Node.js >= 14.0.0, TypeScript, zero runtime dependencies)
Install:              npm install safe-expr-eval
Build/test:           npm run build (tsc) / npm test (jest) / npm run test:coverage

Proof of Concept

PoC Script

This repository contains no offensive exploit script — see src/evaluator.ts, src/parser.ts, src/tokenizer.ts, and src/index.ts for the mitigation implementation, and SECURITY.md for the documented vulnerable pattern reproduced above.

1
2
npm uninstall expr-eval
npm install safe-expr-eval
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Before (vulnerable):
// const { Parser } = require('expr-eval');

// After (safe, same API):
import { Parser } from 'safe-expr-eval';

const parser = new Parser();
const expr = parser.parse('2 * x + 1');
console.log(expr.evaluate({ x: 3 })); // 7

// Attempting the same payload against safe-expr-eval throws instead of executing:
// parser.evaluate('require("child_process").execSync("id")')
// -> Error: Undefined variable: require

Detection & Indicators of Compromise

- Application logs showing expression strings containing require(, process., child_process, exec(, Function(
- Unexpected process spawns (child_process) originating from a Node.js process that only exposes a "formula"/"expression" feature
- Crashes or restarts coinciding with expression-field input such as process.exit()

Signs of compromise:

  • Expression/formula input fields receiving strings that reference require, process, child_process, or Function.
  • Unexplained child processes, file reads (e.g., /etc/passwd), or outbound connections spawned by the Node.js process handling expression evaluation.
  • package.json still listing expr-eval as a dependency in an internet-facing service without migration to a safe evaluator.

Remediation

ActionDetail
Primary fixRemove expr-eval and adopt a safe-eval alternative such as this repo’s safe-expr-eval v1.0.0+ (or an equivalent token/AST-based evaluator that never calls eval()/Function()) — the package’s own SECURITY.md frames it as “the official secure replacement for the vulnerable expr-eval library.”
Interim mitigationIf migration isn’t immediately possible: never pass fully untrusted strings to expr-eval, strip/deny any expression containing require, process, Function, or backticks, run the evaluation in a sandboxed/isolated worker with no access to Node built-ins, and rate-limit/validate expression length.

References


Notes

Mirrored from https://github.com/alecasg555/safe-expr-eval on 2026-07-06. This is genuine hand-written TypeScript defensive/mitigation code, not an attack PoC — it is a real, functional, zero-dependency safe-eval replacement library published in direct response to CVE-2025-12735 (an eval()-based RCE in the expr-eval package), not an exploit script against a live target. Status is marked “Patched” to reflect that this repo represents the fix/mitigation path rather than a weaponized reproduction of the vulnerability; the vulnerable expr-eval code path itself is only referenced (not included) in this repo’s SECURITY.md.

jest.config.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.test.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.test.ts',
    '!src/**/__tests__/**'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};