Introduction
A flaw in how the vm2 Node.js sandbox library handles JavaScript's Proxy set trap lets untrusted sandbox code write properties directly onto host objects. That breaks the isolation guarantee 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. It runs in code execution platforms, online IDEs, and, increasingly, 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 repository has 4,100+ stars and 324 forks on GitHub. AI agent frameworks rely on vm2 to execute code produced by large language models safely, so a sandbox escape there can turn a prompt injection into host-level remote code execution.
CVE-2026-47209 has a CVSS v3.1 score of 8.6 (HIGH). The attack vector is network, no privileges or user interaction are required, and the scope is changed, meaning 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 defect is in the BaseHandler.set trap in lib/bridge.js, at line 1231. It comes down to 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 object the assignment started on. When receiver !== proxy (for example, when a child object inherits from the proxy via Object.create), the specification requires the assignment to create an own property on the receiver, not on the proxy's underlying target.
vm2 ignored the receiver parameter completely. It always called otherReflectSet(object, key, value) against the host target object. Any property write on an object that prototypically inherits from a sandboxed proxy therefore leaks through to the host-side target and crosses 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." Here the protection mechanism is the set trap's handling of receiver, and it is insufficient. The trap has no boundary check at all to tell a write to the proxy target apart from a write to a child receiver.
Attack Flow
Exploitation takes three steps:
-
Sandbox code creates a child object via
Object.create(proxy), whereproxyis a bridged reference to a host object. -
The sandbox code writes a property on the child. Because the
receiver(the child) is not the proxy itself, the Proxy specification says the write should create an own property on the child and never touch the host object. -
Because of the bug, the
settrap ignores thereceiverand callsotherReflectSet(object, key, value), so the inherited write lands on the host object.
The write becomes dangerous when the key is a cross-realm Symbol such as nodejs.util.promisify.custom. Symbol-keyed properties like this cause semantic confusion attacks, where the host runtime interprets the injected property in a way that changes how built-in Node.js APIs behave. The inherited write path would also bypass any isDangerousCrossRealmSymbol guard added to the direct set path in the future, because it never goes through that guard.
According to Aikido Intel, this protection mechanism failure can be chained to execute arbitrary code in the host Node.js process, which escalates a boundary violation into 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 |
The CVE also has a CVSS v4.0 score, 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. Its E:U value records the exploit status as "Unreported" on the assessment date.
Patch Information
Maintainer Patrik Simek patched the vulnerability in commit 26d0318 on May 17, 2026. The fix 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
Before the fix, BaseHandler.set in lib/bridge.js ignored the receiver argument and forwarded every write to the host object via otherReflectSet(object, key, value). The fix adds a receiver identity check. A write reaches the host object only when the receiver is the canonical bridge proxy, and every other receiver goes to a safe install-on-receiver path.
The change adds 24 lines to BaseHandler.set in lib/bridge.js. The new logic sits after the existing __proto__ and constructor-on-array short-circuit checks and before the host-write forwarding path:
// 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; }
The logic runs in three steps:
-
The fix looks up the bridge's canonical sandbox-facing proxy for the given host object in
mappingOtherToThis. vm2 already populates thisWeakMapduring proxy construction on both the!isHost(inner proxy) andisHost(outer proxy2) code paths, so the fix needs no new bridge state. -
It compares the trap's
receiverto that canonical proxy. Ifreceiver === canonicalProxy, 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. This preserves the embedder contract that direct writes to sandbox-exposed host objects still work. -
When
receiver !== canonicalProxy, the write came from an inheriting sandbox-side child (e.g.,Object.create(hostProxy).x = v) or from a forgedReflect.setcall with a custom receiver. In that case the trap installs the property on thereceiveritself viaReflect.defineProperty, and the host object is not touched at all. The descriptor has a null prototype (__proto__: null), which prevents prototype pollution of the descriptor itself, and it creates a standard writable/enumerable/configurable data property on the receiver.
Consistency Across Handler Types
Two other vm2 handlers already worked this way. 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. For non-function values, ProtectedHandler.set delegates to super.set and so inherits this fix automatically. All three handler types now follow the spec.
Test Coverage
The commit includes a 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 commit message says the fix restores "Defense Invariant #1" and adds 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. Every other write path stays 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 npm registry lists 3.11.5 as the latest published version.
Any application that uses vm2 to execute untrusted or semi-trusted JavaScript may be affected. That 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 table below lists the 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. That count shows systemic weaknesses in vm2's Proxy-based isolation model, and individual patches cannot fix them. The project was deprecated from July 2023 to October 2025 because of critical security issues, and the maintainer has acknowledged that new bypasses will likely be discovered. Endor Labs recommends against using vm2 for attacker-controlled code in production environments.
Kodem Security calls the 2026 wave of vm2 vulnerabilities particularly concerning for AI agent frameworks, where a sandbox escape can turn a prompt injection into host-level RCE. Microsoft's name for this pattern is "Prompts Become Shells."
Behavioral Indicators of Compromise
For teams monitoring for exploitation of vm2 sandbox escapes in general, Kodem Security lists these 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
Organizations using vm2 should work on two tracks. In the short term, apply the 3.11.4 patch immediately. Over the longer term, evaluate a move to sandboxing approaches that do not share vm2's Proxy-based architecture and its pattern of vulnerabilities, such as process-level isolation with Firecracker or gVisor. Running vm2 on top of container-level or process-level isolation adds defense in depth for when the next, inevitable bypass arrives.
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



