Introduction
A flaw in how the vm2 Node.js sandbox library handles JavaScript's Proxy set trap allows untrusted sandbox code to write properties directly onto host objects, breaking the fundamental isolation guarantee that vm2 exists to provide. With over 56 million total npm downloads and roughly 1 million weekly downloads, vm2 is one of the most widely deployed JavaScript sandboxing libraries, used in code execution platforms, online IDEs, and increasingly in AI agent frameworks that evaluate LLM generated JavaScript.
vm2 is an open source advanced sandbox for Node.js, maintained by Patrik Simek, that lets embedders run untrusted JavaScript with whitelisted Node built in modules. The library has accumulated 4,100+ GitHub stars and 324 forks on GitHub. Its relevance extends beyond traditional sandboxing: AI agent frameworks rely on vm2 to safely execute code produced by large language models, making any sandbox escape a potential vector for converting prompt injections into host level remote code execution.
CVE-2026-47209 carries a CVSS v3.1 score of 8.6 (HIGH) with a network attack vector, no privileges required, no user interaction needed, and a scope change indicating that the impact crosses the sandbox boundary. The vulnerability affects all vm2 versions from 3.0.0 through 3.11.3 and was patched in version 3.11.4.
Technical Information
Root Cause: The Ignored receiver Parameter
The core defect resides in the BaseHandler.set trap within lib/bridge.js at line 1231. To understand why this matters, we need to look at how JavaScript's [[Set]] internal method interacts with Proxy objects.
Per ECMA-262 §9.5.9, when a property write walks up a prototype chain and reaches a Proxy, the engine passes four arguments into the set trap: target, property, value, and receiver. The receiver is the original object on which the property assignment was initiated. When receiver !== proxy (for example, when a child object inherits from the proxy via Object.create), the specification requires that the property assignment create an own property on the receiver, not on the proxy's underlying target.
The vm2 implementation completely ignored this receiver parameter. Instead, it unconditionally called otherReflectSet(object, key, value) against the host target object. This means that any property write on an object that prototypically inherits from a sandboxed proxy leaks through to the host side target, violating the sandbox boundary.
Classification: CWE-693 Protection Mechanism Failure
This vulnerability is classified under CWE-693, defined by MITRE as a weakness where "the product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks." In this case, the receiver parameter handling in the set trap is an insufficient protection mechanism: the boundary check that should distinguish between writes to the proxy target versus writes to a child receiver is entirely absent.
Attack Flow
The exploitation path follows a clear sequence:
-
Create a prototype inheriting child: Sandbox code creates a child object via
Object.create(proxy), whereproxyis a bridged reference to a host object. -
Assign a property on the child: The sandbox code writes a property on the child object. Per the Proxy specification, because the
receiver(the child) is not the proxy itself, the write should create an own property on the child and never touch the host object. -
Property leaks to host target: Due to the bug, the
settrap ignores thereceiverand writes directly to the host target viaotherReflectSet(object, key, value). The inherited property write leaks through to the host object.
The specific danger materializes when writing dangerous cross realm Symbol keys, such as nodejs.util.promisify.custom, to host objects. These Symbol keyed properties cause semantic confusion attacks where the host runtime interprets the injected property in a way that changes the behavior of built in Node.js APIs. This bypasses any future isDangerousCrossRealmSymbol guard that might be implemented on the direct set path, because the inherited write path does not go through that guard.
Per Aikido Intel, this Protection Mechanism Failure can be chained to execute arbitrary code in the host Node.js process, escalating from a boundary violation to full remote code execution.
CVSS Scoring Breakdown
| CVSS Metric | v3.1 Value | Interpretation |
|---|---|---|
| Attack Vector | Network (AV:N) | Exploitable remotely over the network |
| Attack Complexity | Low (AC:L) | No specialized conditions required |
| Privileges Required | None (PR:N) | No authentication needed |
| User Interaction | None (UI:N) | No user action required |
| Scope | Changed (S:C) | Impact crosses sandbox boundary |
| Confidentiality | None (C:N) | No direct information disclosure |
| Integrity | High (I:H) | Full control over integrity of host objects |
| Availability | None (A:N) | No direct availability impact |
A CVSS v4.0 score has also been assigned: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:U/U:Amber, where E:U confirms exploit status is "Unreported" as of the assessment date.
Patch Information
The vulnerability was patched by maintainer Patrik Simek in commit 26d0318, committed on May 17, 2026, and shipped in vm2 v3.11.4 (released May 18, 2026) under advisory GHSA-c4cf-2hgv-2qv6. The release was a pure security patch with no API changes.
How the Fix Works
The pre-fix BaseHandler.set in lib/bridge.js completely ignored the receiver argument and unconditionally forwarded every write to the host object via otherReflectSet(object, key, value). The fix introduces a receiver identity check that gates host object writes on the receiver being the canonical bridge proxy, and routes all non-canonical receivers to a safe install-on-receiver path.
The core code change adds 24 lines to BaseHandler.set in lib/bridge.js. Placed after the existing __proto__ and constructor-on-array short circuit checks but before the host write forwarding path, the new logic is:
// SECURITY (GHSA-c4cf-2hgv-2qv6): honour ECMA-262 9.5.9 [[Set]] // receiver semantics. const canonicalProxy = thisReflectApply(thisWeakMapGet, mappingOtherToThis, [object]); if (receiver !== canonicalProxy) { return thisReflectDefineProperty(receiver, key, { __proto__: null, value: value, writable: true, enumerable: true, configurable: true, }) === true; }
Here is how this works step by step:
-
Canonical proxy lookup: The fix retrieves the bridge's own canonical sandbox facing proxy for the given host object from
mappingOtherToThis, aWeakMapthat vm2 already populates during proxy construction for both the!isHost(inner proxy) andisHost(outer proxy2) code paths. No new bridge state is needed. -
Identity gate: It then compares the trap's
receiverto this canonical proxy. Ifreceiver === canonicalProxy, we know the write came directly through the proxy (e.g.,hostProxy.x = vorReflect.set(hostProxy, k, v, hostProxy)), and the code falls through to the existing legitimateotherReflectSetforwarding path, preserving the embedder contract that direct writes to sandbox exposed host objects still work. -
Install on receiver fallback: When
receiver !== canonicalProxy, meaning the write originated from an inheriting sandbox side child (e.g.,Object.create(hostProxy).x = v) or a forgedReflect.setcall with a custom receiver, the property is installed on thereceiveritself viaReflect.defineProperty. Critically, the host object is never touched. The descriptor uses a null prototype (__proto__: null) to prevent prototype pollution of the property descriptor itself, and creates a standard writable/enumerable/configurable data property on the receiver.
Consistency Across Handler Types
This design mirrors how two other vm2 handlers already operated: ReadOnlyHandler.set uses the same install-on-receiver shape unconditionally (since its policy is "host is read only"), and ProtectedHandler.set does the same in its function value branch. ProtectedHandler.set also inherits this fix automatically through its super.set delegation for non-function values, so all three handler types are now consistent with the spec.
Test Coverage
The commit includes a comprehensive five variant test suite in test/ghsa/GHSA-c4cf-2hgv-2qv6/repro.js covering:
- Symbol key inherited write via
Object.create(hostFn)[Symbol.for(...)] = fn - Plain string key inherited write
- Forged
Reflect.setwith an explicit non-proxy receiver - Deep prototype chain write (
Object.create(Object.create(hostObj))) Object.assign(Object.create(hostObj), src)bulk pollution- A negative control test confirming that direct
proxy.x = vwrites still correctly mutate the host object
The fix restores what the commit message calls "Defense Invariant #1" with a new corollary: a sandbox originated write reaches a host realm object only when the spec [[Set]] receiver equals the canonical bridge proxy for that object's target. All other write paths are safely confined to the sandbox side receiver.
Affected Systems and Versions
All vm2 versions from 3.0.0 through 3.11.3 are affected. The vulnerability is patched in version 3.11.4.
| Version Range | Status |
|---|---|
| < 3.0.0 | Not affected (predates the vulnerable code) |
| 3.0.0 through 3.11.3 | Vulnerable |
| 3.11.4 and later | Patched |
The latest published version on npm is 3.11.5 as of the npm registry.
Any application that uses vm2 to execute untrusted or semi-trusted JavaScript is potentially affected. This includes code execution platforms, online IDEs, plugin sandboxing systems, and AI agent frameworks that evaluate LLM generated code.
Vendor Security History
vm2 has a well documented history of sandbox escape vulnerabilities. The following table summarizes known CVEs across the library's lifecycle:
| CVE ID | Affected Versions | Patched Version | Severity | Description |
|---|---|---|---|---|
| CVE-2023-29017 | Prior to 3.9.15 | 3.9.15 | High | Sandbox escape via Proxy handler exploitation |
| CVE-2023-30547 | Prior to 3.9.17 | 3.9.17 | Critical | Exception sanitization bypass |
| CVE-2023-37466 | Prior to 3.9.18 | 3.9.18 | High | Insufficient fix for prior CVEs |
| CVE-2026-22709 | <= 3.10.1 | 3.10.2 | Critical | Promise callback sanitization bypass enabling RCE |
| CVE-2026-24118 | < 3.11.0 | 3.11.0 | High | Sandbox escape |
| CVE-2026-24120 | < 3.10.5 | 3.10.5 | High | Insufficient fix for CVE-2023-37466 |
| CVE-2026-24781 | < 3.11.0 | 3.11.0 | Critical | Sandbox breakout via inspect function |
| CVE-2026-26332 | < 3.11.0 | 3.11.0 | High | Sandbox escape |
| CVE-2026-26956 | < 3.10.5 | 3.10.5 | Critical | Sandbox escape via TypeError / Symbol to string conversion |
| CVE-2026-43997 | < 3.11.0 | 3.11.0 | High | Sandbox escape |
| CVE-2026-43999 | < 3.11.0 | 3.11.0 | High | Sandbox escape |
| CVE-2026-44005 | < 3.11.0 | 3.11.0 | High | Sandbox escape |
| CVE-2026-44006 | < 3.11.0 | 3.11.0 | High | Sandbox escape |
| CVE-2026-44007 | < 3.11.1 | 3.11.1 | High | Sandbox escape |
| CVE-2026-44008 | < 3.11.2 | 3.11.2 | High | Sandbox escape |
| CVE-2026-44009 | < 3.11.2 | 3.11.2 | High | Sandbox escape |
| CVE-2026-47209 | < 3.11.4 | 3.11.4 | 8.6 (High) | Bridge Proxy set trap ignores receiver parameter |
At least 13 CVEs were disclosed in 2026 alone, revealing systemic weaknesses in vm2's Proxy based isolation model that cannot be addressed by individual patches. The project was deprecated between July 2023 and October 2025 due to critical security issues, and the maintainer has acknowledged that new bypasses will likely be discovered in the future. Endor Labs specifically recommends avoiding vm2 for attacker controlled code in production environments.
Kodem Security characterizes the 2026 wave of vm2 vulnerabilities as particularly concerning for AI agent frameworks, where sandbox escapes can transform prompt injections into host level RCE, a pattern Microsoft terms "Prompts Become Shells."
Behavioral Indicators of Compromise
For organizations monitoring for potential exploitation of vm2 sandbox escapes in general, Kodem Security identifies the following behavioral IOCs:
| IOC Category | Indicators |
|---|---|
| Process | node processes spawning child_process.exec or spawn from sandboxed contexts |
| Shell activity | Shell invocations (sh, bash, cmd.exe) parented to Node.js |
| Credential access | Filesystem reads of ~/.aws/credentials, ~/.npmrc, /proc/self/environ |
| Network | Unexpected outbound DNS or HTTPS from agent runtimes |
Strategic Recommendation
For organizations currently using vm2, the recommended path forward is dual track: apply the 3.11.4 patch immediately as a tactical measure, while strategically evaluating migration to alternative sandboxing approaches (such as process level isolation with Firecracker or gVisor) that do not share vm2's Proxy based architectural vulnerability pattern. Layering container level or process level isolation beneath vm2 provides defense in depth against the next inevitable bypass.
References
- NVD: CVE-2026-47209
- GitHub Security Advisory: GHSA-c4cf-2hgv-2qv6
- Patch Commit 26d0318
- vm2 v3.11.4 Release
- OSV: GHSA-c4cf-2hgv-2qv6
- GitLab Advisory: CVE-2026-47209
- Aikido Intel: AIKIDO-2026-10839
- Cybersecurity Help: VU131737
- Kodem Security: vm2 Sandbox Escape, AI Agent RCE Risk and IOCs
- Endor Labs: Critical Sandbox Escape in vm2 Enables RCE
- vm2 GitHub Repository
- vm2 on npm
- Miggo Vulnerability Database: CVE-2026-47209
- CWE-693: Protection Mechanism Failure
- CISA KEV Catalog Update (June 8, 2026)
- npm-stat: vm2 Download Statistics
- QuorumCyber: PoC Exploit Released for VM2 Vulnerability
- Semgrep: New Sandbox Escape Affecting Popular Node.js Sandbox Library vm2



