← 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
TL;DR
// 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.
Vulnerable Code — Before (v9.0.0)
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);
};
The Fix — After (v10.3.0)
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.
Why Competitors Miss This
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.
Timeline
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
this.safeVmresolves to the rawvmmodule, 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.this.safeVm.Scriptwith 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.