CVE-2026-47210 Overview
CVE-2026-47210 is a sandbox escape vulnerability in vm2, an open source virtual machine and sandbox library for Node.js. The flaw affects all versions prior to 3.11.4 when untrusted code runs on Node.js runtimes that expose the WebAssembly JavaScript Promise Integration (JSPI) API. A JSPI-backed Promise reaches Promise.prototype.finally() in a way that bypasses the expected Promise-species hardening. This exposes a host-originated rejection object to attacker-controlled species logic, breaking the sandbox boundary and enabling arbitrary code execution in the host process. The issue is classified under [CWE-913: Improper Control of Dynamically-Managed Code Resources].
Critical Impact
Untrusted JavaScript executed inside vm2 can escape the sandbox and run arbitrary code in the host Node.js process when JSPI is available.
Affected Products
- vm2 versions prior to 3.11.4
- Node.js 24+ environments running vm2 with --experimental-wasm-jspi
- Node.js 26+ environments running vm2 (JSPI enabled by default)
Discovery Timeline
- 2026-06-12 - CVE-2026-47210 published to NVD
- 2026-06-17 - Last updated in NVD database
Technical Details for CVE-2026-47210
Vulnerability Analysis
The vulnerability resides in lib/setup-sandbox.js. The WebAssembly JSPI API, exposed through WebAssembly.promising and WebAssembly.Suspending, produces Promise objects whose prototype chain points directly at the host realm's Promise.prototype. This bypasses every sandbox-side defense vm2 layers on top of standard Promises.
When the sandbox accesses a property on a JSPI promise, the lookup walks the cross-realm prototype chain and resolves to host-realm native methods. The sandbox-side globalPromise.prototype.then|catch overrides are never reached because they live on a different prototype object. The resetPromiseSpecies routine, invoked only from those overrides, is similarly skipped. The bridge apply-trap callback wrapping is also bypassed because JSPI promises are not proxied.
Root Cause
The root cause is missing isolation of the WebAssembly JSPI surface inside the sandbox. JSPI promises retain a direct reference to the host Promise.prototype and are not wrapped by vm2's bridge proxy. Any species-related logic that depends on prototype interception silently fails.
Attack Vector
An attacker supplies untrusted code to a vm2 instance that supports async execution. The attacker obtains a JSPI promise, installs a custom constructor getter on it, and triggers Promise.prototype.finally(). The host's finally reads p.constructor for SpeciesConstructor and uses the attacker-supplied function F as the result capability. When the JSPI promise rejects with a host-realm error, V8 delivers the raw host error into sandbox code, where e.constructor.constructor('return process')() yields the host process object and arbitrary code execution.
// Source: https://github.com/patriksimek/vm2/commit/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6
// Security patch in lib/setup-sandbox.js — removes WebAssembly JSPI surface from sandbox
localReflectDeleteProperty(WebAssembly, 'JSTag');
/*
* WebAssembly JSPI protection (GHSA-6j2x-vhqr-qr7q)
*
* The WebAssembly JavaScript Promise Integration (JSPI) API — `WebAssembly.promising`
* and `WebAssembly.Suspending` (Node 24+ behind --experimental-wasm-jspi, Node 26+
* by default) — produces Promise objects whose prototype chain points DIRECTLY at
* the host realm's `Promise.prototype` without going through any bridge proxy.
* Sandbox property access on a JSPI promise (e.g. `p.then`, `p.finally`) walks the
* cross-realm prototype chain and resolves to host-realm native methods, completely
* bypassing:
* - the sandbox-side `globalPromise.prototype.then|catch` overrides (different
* prototype object, so the overrides are never reached),
* - `resetPromiseSpecies` (only called from those overrides),
* - the bridge `apply`-trap callback wrapping for host Promise methods (only
* fires for *bridge-proxied* host promises; JSPI promises aren't proxied).
*
* Consequences:
* 1. An attacker can install `Object.defineProperty(p, 'constructor', { get(){return F}})`
* directly on the JSPI promise (no proxy intercepts it).
* 2. Host's `Promise.prototype.finally` reads `p.constructor` for SpeciesConstructor,
* gets the attacker's F (sandbox class), builds a result capability whose
* `[[Resolve]]` / `[[Reject]]` are the *raw* sandbox closures F supplied in its
* executor — with no bridge wrapping.
* 3. When the JSPI promise rejects (e.g. with a host-realm TypeError thrown by
* `WebAssembly.compileStreaming` on a non-Response input), V8 dispatches the
* rejection through F's reject closure, delivering the raw host error into
* sandbox code. `e.constructor.constructor('return process')()` then evaluates
*/
Source: GitHub Commit 6915fa4
Detection Methods for CVE-2026-47210
Indicators of Compromise
- Sandbox child processes spawning unexpected binaries such as /bin/sh, cmd.exe, or node with attacker-controlled arguments.
- Outbound network connections originating from the Node.js host process immediately after sandboxed code execution.
- Use of WebAssembly.promising or WebAssembly.Suspending inside untrusted scripts executed by vm2.
- Application logs showing host-realm TypeError objects surfacing inside sandboxed exception handlers.
Detection Strategies
- Inventory all Node.js applications using vm2 and identify versions less than 3.11.4 via npm ls vm2 or software composition analysis tooling.
- Flag any Node.js process launched with --experimental-wasm-jspi or running on Node.js 26+ while loading vulnerable vm2.
- Monitor for WebAssembly.compileStreaming calls and Promise finally invocations on objects whose constructor has been redefined.
Monitoring Recommendations
- Track process creation, file writes, and outbound network activity from Node.js application hosts that execute untrusted code.
- Alert on unexpected access to the process global, child_process, or fs modules following sandbox execution.
- Forward Node.js application telemetry and dependency manifests to a centralized analytics platform for continuous vulnerability and behavioral monitoring.
How to Mitigate CVE-2026-47210
Immediate Actions Required
- Upgrade vm2 to version 3.11.4 or later across all dependent applications.
- Audit dependency trees with npm audit and npm ls vm2 to find transitive uses of vulnerable releases.
- Restrict the Node.js runtime to versions that do not expose JSPI when an upgrade cannot be performed immediately.
- Treat vm2 as a defense-in-depth control only and isolate sandboxed workloads in dedicated processes or containers.
Patch Information
The maintainer released the fix in vm2 v3.11.4. The corrective commit removes the JSPI surface from the sandbox by deleting WebAssembly.promising, WebAssembly.Suspending, and related members before user code executes. Full advisory details are available in GHSA-6j2x-vhqr-qr7q.
Workarounds
- Disable WebAssembly JSPI by avoiding the --experimental-wasm-jspi flag on Node.js 24 and 25.
- Manually delete WebAssembly.promising and WebAssembly.Suspending from the sandbox context before evaluating untrusted code.
- Execute untrusted JavaScript in a hardened, isolated process or container rather than relying on vm2 alone.
# Upgrade vm2 to the patched release
npm install vm2@3.11.4 --save
# Verify installed version
npm ls vm2
# Optional runtime mitigation on Node.js <26 — avoid enabling JSPI
node --no-experimental-wasm-jspi app.js
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

