← Back to PullLight
CVE-2024-21534 CVSS 9.8 Critical

VM Sandbox Escape in jsonpath-plus

npm package jsonpath-plus allowed arbitrary Node.js code execution via an "unsafe" vm sandbox

Affected < 10.0.0
Fixed October 2024
// what happened
  • Affected versions: jsonpath-plus < 10.0.0 (July 2024 to October 2024)
  • Attack vector: Filter expressions with @constructor payload escaped the vm.Script sandbox
  • Impact: Remote Code Execution (RCE) — any user-provided JSONPath query could run arbitrary Node.js code on the server
  • Fix: AST-based SafeScript class replaced vm.Script entirely. The "safeVm" property was literally the raw Node.js vm module.

src/jsonpath-node.js import vm from 'vm'; import {JSONPath} from './jsonpath.js'; JSONPath.prototype.vm = vm; JSONPath.prototype.safeVm = vm; // <-- VULNERABLE: named "safe" but IS the raw vm module
src/jsonpath.js — _eval method v9.0.0 — The "safe" default leads to raw vm execution JSONPath.prototype.safeVm = vm; // line at top of module JSONPath.prototype._eval = function (code, _v, _vname, path, parent, parentPropName) { // ... if ( this.currEval === 'safe' || this.currEval === true || this.currEval === undefined // <-- DEFAULT is 'safe' ) { JSONPath.cache[scriptCacheKey] = new this.safeVm.Script(script); // <-- RCE: vm.Script is NOT a sandbox } // ... return JSONPath.cache[scriptCacheKey].runInNewContext(this.currSandbox); };
src/jsonpath-node.js import vm from 'vm'; import {JSONPath} from './jsonpath.js'; JSONPath.prototype.vm = vm; // safeVm removed — evaluated via SafeScript class instead
src/jsonpath.js — _eval method v10.3.0 — safeVm now wraps SafeScript AST evaluator // safeVm is now an object wrapping SafeScript, not the vm module JSONPath.prototype.safeVm = { Script: SafeScript // <-- SAFE: AST-based evaluator, no vm.Script }; JSONPath.prototype._eval = function (code, _v, _vname, path, parent, parentPropName) { // ... if ( this.currEval === 'safe' || this.currEval === true || this.currEval === undefined ) { JSONPath.cache[scriptCacheKey] = new this.safeVm.Script(script); // <-- Fixed: SafeScript AST evaluator } // ... return JSONPath.cache[scriptCacheKey].runInNewContext(this.currSandbox); };
src/Safe-Script.js — key blocked operations v10.3.0 — New file, AST-based evaluation class SafeScript { constructor(expr) { this.code = expr; this.ast = jsep(this.code); // parse to AST, never eval a string } runInNewContext(context) { // Object.create(null) = no prototype chain const keyMap = Object.assign(Object.create(null), context); return SafeEval.evalAst(this.ast, keyMap); } } // BLOCKED: constructor, __proto__, __defineGetter__, __defineSetter__ const BLOCKED_PROTO_PROPERTIES = new Set([ 'constructor', '__proto__', '__defineGetter__', '__defineSetter__' ]);
// root cause
safeVm was literally the Node.js vm module. The variable name implied safety, but the vm module is explicitly documented NOT to provide a secure sandbox. The default eval mode is 'safe' (undefined), which causes the code to use this.safeVm.Script(script) — executing the JSONPath filter expression as raw Node.js code. An attacker can pass payloads like $[?(@.constructor)] to escape the sandbox and execute arbitrary code on the host system.

🔴 PullLight — Critical Finding
[CRITICAL] Code Injection — src/jsonpath.js:_eval (line ~267)

this.safeVm resolves to the raw vm module, NOT a sandbox. Node's vm.Script is documented as NOT a security boundary and can be escaped with constructor access. Payload: $[?(@.constructor)] can escape the context and call system-level functions.
→ Replace this.safeVm.Script with an AST-based evaluator (e.g., jsep) that explicitly denies access to Function, constructor, __proto__, require, and other escape vectors. Do NOT use vm.Script for user-controlled input regardless of the variable name.

Variable names carry false trust

  • CodeRabbit / Copilot: Surface-level diff review — safeVm looks safe because the name says so. The actual implementation (raw vm module) is invisible in a name-based review.
  • Greptile: Similar issue — reviews symbols and syntax, not behavior. A variable named safeVm containing the vm module won't trigger warnings.
  • Graphite / Qodo: Focus on formatting and readability. The security-relevant fact (vm.Script is NOT a sandbox) requires domain knowledge, not code style analysis.

What makes this hard to catch manually

  • Node.js documentation explicitly states vm.Script "does not constitute a security boundary" — this is buried in the API docs, not in code comments.
  • The safeVm naming implies safety by convention, contradicting the actual behavior.
  • The escape payload (@.constructor) looks like normal JSONPath syntax — it's the semantic evaluation that makes it dangerous.

Jul 2024 jsonpath-plus v10.0.0 introduced vm.Script for eval expressions
Oct 8, 2024 First security fix committed (v10.0.0, commit 6b2f1b4)
Oct 11, 2024 CVE-2024-21534 published by NVD (CVSS 9.8)
Oct 14, 2024 GitHub Advisory GHSA-pppg-cpfq-h7wr published
Oct 16–18, 2024 Further hardening (SafeScript class, BlockedProtoProperties)
v10.3.0 Final stable patched release (SafeScript replaces vm.Script entirely)
Feb 2025 CVE-2025-1302 filed for incomplete fix (CVSS 8.9) — see the follow-on advisory

Don't let this happen to your PRs