Patch Forecast
Red warning
Act within 24 hours.

CVE-2026-92956 · GHSA-wjwh-qqvp-g4p4

vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass

Risk score
84/100
CVSS
10.0 v3.1
Exploit chance (30 days)
0.6%
Published
5 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
There is a sandbox escape in vm2 3.11.5 / current HEAD when it is used on Node.js 26. The issue is reachable from a default new VM() sandbox. No NodeVM, require permission, host object injection, or intentionally unsafe configuration is required. The escape is a patch-bypass of the same security invariant addressed by…
Affected
vm2 (npm) >= 3.10.1, <= 3.11.6
Fix
Upgrade vm2 to 3.11.7 or later.
Exploitation
Public proof of concept

Story map

Story map: vm2THE FLAWSTHE FIXCVE-2026-92956Vm2 sandbox escape viaWebAssembly.compileStreaming Promise species bypassCVSS 10.0 · EPSS 1%CVE-2026-92946NodeVM `require.external`without an explicit`require.root` grants…CVSS 10.0 · EPSS 1%CVE-2026-92944Vm2 sandbox escape on Node.js26 through a stalePromiseThenLookupChain…CVSS 9.8 · EPSS 1%CVE-2026-92937GHSA-m283-3h24-438v fixbypass leads to host RCE viacall/apply indirectionCVSS 10.0 · EPSS 1%FixUpgrade vm2 to 3.11.7 or later.Who is exposedvm2 (npm) <= 3.11.6RED · ACT TODAYvm2NodeVM's require.externaloption lets sandboxed coderequire() local files and npmpackages. When require.externalis enabled and require.root is notexplicitly set to a path thatexcludes node_modules, two…Where it standsPublic proof of conceptCONNECTS TOSame weaknessCWE-693 (CWE-693)58 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

vm2: Red, act today. NodeVM's require.external option lets sandboxed code require() local files and npm packages. When require.external is enabled and require.root is not explicitly set to a path that excludes node_modules, two defaults combine to fully defeat the sandbox: require.root defaults to unrestricted — "if omitted every path is …

The flaws

  • CVE-2026-92956: Vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass (CVSS 10.0 · EPSS 1%)
  • CVE-2026-92946: NodeVM `require.external` without an explicit `require.root` grants unrestricted host filesystem access and full RCE (CVSS 10.0 · EPSS 1%)
  • CVE-2026-92944: Vm2 sandbox escape on Node.js 26 through a stale PromiseThenLookupChain protector (CVSS 9.8 · EPSS 1%)
  • CVE-2026-92937: GHSA-m283-3h24-438v fix bypass leads to host RCE via call/apply indirection (CVSS 10.0 · EPSS 1%)

The fix

Fix: Upgrade vm2 to 3.11.7 or later.

Who is exposed: vm2 (npm) <= 3.11.6

Where it stands

  • Public proof of concept

Connects to

Why it is Red

  • CVSS 10.0 critical
  • Public exploit or proof of concept referenced
  • Remotely exploitable without logging in
  • On your watchlist: npm

Affected packages

PackageEcosystemVulnerableFixed in
vm2npm>= 3.10.1, <= 3.11.63.11.7

References

Original description

## Summary There is a sandbox escape in vm2 `3.11.5` / current HEAD when it is used on Node.js 26. The issue is reachable from a default `new VM()` sandbox. No `NodeVM`, `require` permission, host object injection, or intentionally unsafe configuration is required. The escape is a patch-bypass of the same security invariant addressed by GHSA-6j2x-vhqr-qr7q. The earlier fix removed the JSPI entry points `WebAssembly.promising` and `WebAssembly.Suspending`, because those APIs exposed a Promise path whose host-realm `Promise.prototype` was not intercepted by vm2's Promise hardening or bridge layer. The same unsafe class remains reachable through `WebAssembly.compileStreaming` and `WebAssembly.instantiateStreaming`. On Node 26, these streaming APIs can produce a raw host-realm Promise path that rejects with a host-realm error. By controlling `Symbol.species` through `Promise.prototype.finally`, sandbox code can receive that raw host error object, walk from the host error constructor to the host `Function` constructor, and recover the real host `process` object. The proof of concept demonstrates this by first showing that direct access to `process`, `require`, and constructor-based escapes are blocked in the same default VM. It then reaches the host `process`, verifies that the recovered pid matches the parent Node process, and writes a harmless marker file through host `fs`. ## Impact This is a sandbox escape. In applications that expose vm2 execution to attacker-controlled JavaScript, the issue can become host code execution in the context of the Node.js process running the sandbox. This is not remote code execution by default. The remote aspect depends on the embedding application. The accurate framing is: > An attacker who can supply JavaScript to a vm2 `VM` sandbox on Node 26 can break out of the vm2 boundary and obtain host Node.js capabilities. This matters for services that use vm2 as a security boundary for untrusted JavaScript, including plugin runners, workflow engines, automation platforms, online code runners, browser-automation sandboxes, and AI-agent or user-script execution environments. The PoC only writes a marker file under the OS temporary directory. That marker is intentionally harmless. The security impact is not the marker itself; the security impact is that sandbox-controlled code obtains the real host `process` and host modules after the control case proves those capabilities are normally blocked. ## Tested versions and configuration Tested vm2 `3.11.5` at commit `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`. The escape reproduced on Node.js `26.2.0`. I also tested the same PoC on Node.js `20.20.2`, `22.22.3`, and `24.15.0`; those versions did not recover the host process through this path. The test case uses the default `VM` boundary only: ```js const { VM } = require('./lib/main'); const vm = new VM({ timeout: 8000 }); vm.run(attackerControlledJavaScript); ``` No `NodeVM` was used. The sandbox was not given `require`, `process`, `fs`, `child_process`, host callbacks, or host objects. The PoC only controls the JavaScript string passed to `vm.run()`. The version dependency appears to be in the final delivery step: on Node 26 the host-realm rejection from `WebAssembly.compileStreaming` reaches the attacker-controlled `finally`/`Symbol.species` capability, while the same path did not become exploitable in my Node 20/22/24 tests. I would still treat the fix as version-independent: sandbox code should not be able to receive this raw host-Promise path from the WebAssembly streaming APIs at all. ## Root cause The security boundary relies on vm2 keeping Promise objects that are reachable from sandbox code in one of two safe shapes: 1. **Sandbox-realm Promise.** The Promise uses the sandbox's Promise prototype chain, so vm2's Promise hardening can pin species and sanitize callbacks. 2. **Bridge-proxied host Promise.** The Promise is a host object crossing through vm2's membrane, so bridge traps and callback sanitizers apply. `WebAssembly.compileStreaming` and `WebAssembly.instantiateStreaming` introduce a third shape: a raw host-realm Promise path that is directly reachable from sandbox code and is not bridge-proxied. This is the same class of object that made the JSPI issue dangerous. The exploitability depends on two facts being true at the same time: - the streaming WebAssembly API returns a Promise path whose relevant Promise machinery is host-realm rather than sandbox-realm; and - the rejection generated by passing an invalid streaming source is a host-realm `TypeError` from Node's WebAssembly streaming implementation. Once that host error is delivered to attacker-controlled code, the usual host-realm constructor walk becomes possible: ```js hostError.constructor.constructor('return process')() ``` That expression resolves through the host `Function` constructor, not the sandbox one, because the error object is host-realm. ## Exploit flow The exploit flow is small, but the realm boundary is the important part: 1. Sandbox code calls `WebAssembly.compileStreaming(0)`. 2. The argument is not a valid `Response` or Promise resolving to a `Response`, so the returned Promise rejects. 3. On Node 26, the rejection value is a host-realm error object. 4. The attacker installs a controlled `constructor` accessor on the returned Promise and provides a custom `Symbol.species` constructor. 5. Calling `p.finally(() => {})` reaches the species path used by `Promise.prototype.finally`. 6. `NewPromiseCapability(F)` invokes the attacker-controlled constructor `F` and exposes the result capability functions. 7. When the Promise rejects, the host-realm error is delivered to the attacker-controlled rejection path. 8. The attacker uses the host error's constructor chain to recover host `process`. 9. The PoC loads host `fs` through `process.mainModule.require('fs')` and writes a harmless marker file. `Promise.prototype.finally` is important because it is the r

Last activity 5 Oct 2026. Patch Forecast updates every hour.