CVE-2026-92944 · GHSA-27g9-p43v-cw3v
vm2 sandbox escape on Node.js 26 through a stale PromiseThenLookupChain protector
- Risk score
- 85/100
- CVSS
- 9.8 v3.1
- Exploit chance (30 days)
- 0.8%
- Published
- 1 Oct 2026
Open in the live forecast Today's forecast
What to know
- What it is
- Name or handle: [YMsora] Report date: 2026-08-14 The latest published vm2 release, 3.11.5, and the current main branch are vulnerable to a sandbox escape when used on Node.js 26. An ordinary fulfilled Promise created by an async function can retain an attacker-controlled constructor[Symbol.species] across Promise.prot…
- Affected
- vm2 (npm) >= 3.10.2, <= 3.11.6
- Fix
- Upgrade vm2 to 3.11.7 or later.
- Exploitation
- No known exploitation
Story map
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-92944: Vm2 sandbox escape on Node.js 26 through a stale PromiseThenLookupChain protector (CVSS 9.8 · 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-92956: Vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass (CVSS 10.0 · 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
- Same weakness: CWE-693 (CWE-693). 58 other warnings in 30 days
Why it is Red
- CVSS 9.8 critical
- Remotely exploitable without logging in
- On your watchlist: node.js, npm
Affected packages
| Package | Ecosystem | Vulnerable | Fixed in |
|---|---|---|---|
| vm2 | npm | >= 3.10.2, <= 3.11.6 | 3.11.7 |
References
- https://github.com/advisories/GHSA-27g9-p43v-cw3v
- https://github.com/patriksimek/vm2/security/advisories/GHSA-27g9-p43v-cw3v
- https://nvd.nist.gov/vuln/detail/CVE-2026-92944
- https://github.com/patriksimek/vm2/commit/c7df2e21d2c74038a1f1750f40f9fe603ec69c6a
- https://github.com/patriksimek/vm2/releases/tag/v3.11.7
- https://www.vulncheck.com/advisories/vm2-3.10.2-through-3.11.6-sandbox-escape-via-promise-protector
- https://github.com/advisories/GHSA-27g9-p43v-cw3v
Original description
## Reporter - Name or handle: `[YMsora]` - Report date: 2026-08-14 ## Summary The latest published vm2 release, **3.11.5**, and the current `main` branch are vulnerable to a sandbox escape when used on Node.js 26. An ordinary fulfilled Promise created by an async function can retain an attacker-controlled `constructor[Symbol.species]` across `Promise.prototype.finally()`. vm2 installs wrappers on the intrinsic `Promise.prototype.then` and `catch` methods. On Node.js 26 / V8 14.6, V8's `SetPrototypeProperties` path updates these existing data properties without invalidating the `PromiseThenLookupChain` protector. `Promise.prototype.finally()` subsequently trusts the stale protector and uses an internal `InvokeThen` fast path that calls the original native `then`, bypassing vm2's wrapper and its `resetPromiseSpecies(this)` hardening. The attacker-controlled species constructor therefore supplies the resolve and reject functions used by a native Promise reaction. A calibrated stack overflow at that native reaction boundary yields a raw host-realm `RangeError` to the attacker-controlled reject function. Its constructor chain reaches the host `Function` constructor and consequently the host `process` object. The attached proof is non-destructive: it reads only `process.version`. It does not execute commands, access files, or perform network requests. ## Latest-version status Verified on 2026-08-14: - npm `latest`: `vm2@3.11.5` - `vm2@3.11.5` publication time: 2026-05-18T15:37:26.138Z - npm `gitHead`: `7a1f5100b96f48d34e0fe104ab37c0acc5944f92` - npm tarball: `https://registry.npmjs.org/vm2/-/vm2-3.11.5.tgz` - npm integrity: `sha512-RSrkBiwrj6FRU+QdqNs6KG0XdlvJCjpQ4GXiqmMbrhmwfu5k/XIMpAer0L8f6iuf0uJ3a4T1xJN126Q8yf0VIA==` - GitHub default branch `main` HEAD: the same commit, also tagged `v3.11.5` - latest formal Node.js release: `v26.7.0`, V8 `14.6.202.34` The npm tarball and current GitHub `main` contain the same relevant `lib/setup-sandbox.js` Git blob. There is no unpublished fix on `main` at the time of this report. Permanent source reference: <https://github.com/patriksimek/vm2/blob/7a1f5100b96f48d34e0fe104ab37c0acc5944f92/lib/setup-sandbox.js#L345-L390> ## Affected configurations ### vm2 versions tested on Node.js 26.7.0 | vm2 version | Result | | --- | --- | | 3.10.2 through 3.10.5 | host realm reached, 3/3 per release | | 3.11.0 | host realm reached, 3/3 | | 3.11.1 | host realm reached, 3/3 | | 3.11.2 | host realm reached, 5/5 | | 3.11.3 | host realm reached, 3/3 | | 3.11.4 | host realm reached, 5/5 | | 3.11.5 | host realm reached, 5/5 | | current `main` (`7a1f5100`) | host realm reached, 5/5 | This report therefore confirms **vm2 3.10.2 through 3.11.5** as affected when used with the vulnerable runtime. Earlier vm2 versions are not claimed: they may be independently vulnerable through older published issues, which would not establish this root cause. ### Node.js versions tested with stock vm2 3.11.5 | Node.js | V8 | Result | | --- | --- | --- | | 22.23.2 | 12.4.254.21 | safe, 5/5 | | 24.19.0 | 13.6.233.17 | safe, 5/5 | | 25.9.0 | 14.1.146.11 | safe, 5/5 | | 26.0.0 | 14.6.202.33 | host realm reached, 5/5 | | every formal release from 26.1.0 through 26.7.0 | 14.6.202.34 | host realm reached, 5/5 per release | | v27.0.0-nightly202608131b2de5e052 | 14.6.202.34 | host realm reached | | v27.0.0-v8-canary20260812f3fe529a1e | 15.3.55 | safe | The vulnerable behavior was reproduced on Linux/musl, Linux/glibc, and Windows x64. It is not Alpine-specific. ## Configuration requirements The default configuration is affected: ```js const vm = new VM(); ``` Default `new VM()` was verified 3/3 on Node.js 26.7.0 and vm2 3.11.5 with the operating system's default Node stack size. The escape also remains reproducible with stronger settings: ```js const vm = new VM({ allowAsync: true, eval: false, wasm: false, timeout: 5000 }); ``` Therefore the exploit does **not** require: - WebAssembly or JSPI; - dynamic evaluation being enabled; - Buffer; - an exposed host object; - a custom Promise supplied by the embedder; - a specific operating system; or - any challenge-specific code. The demonstrated producer uses an async function, so async support must be available. This is vm2's default. The V8 `PromiseThenLookupChain` protector must still be intact when vm2 installs its wrappers. A fresh/default Node.js process satisfies this condition. An unrelated earlier mutation that correctly invalidates the protector can make this exact path safe, but that is not a documented vm2 mitigation and is not present in the default setup. ## Reproduction The attachment `poc.js` uses only the published npm package and prints a benign host-version marker. ```sh mkdir vm2-node26-repro cd vm2-node26-repro npm init -y npm install --ignore-scripts --no-audit --no-fund vm2@3.11.5 cp /path/to/poc.js . node poc.js ``` Alternatively, with the official Node.js 26.7.0 container: ```sh docker run --rm -v "$PWD:/poc:ro" -w /tmp node:26.7.0-bookworm-slim sh -lc ' npm init -y >/dev/null 2>&1 && npm install --ignore-scripts --no-audit --no-fund vm2@3.11.5 >/dev/null 2>&1 && cp /poc/poc.js . && node poc.js ' ``` Representative output: ```json { "node": "v26.7.0", "v8": "14.6.202.34-node.28", "vm2": "3.11.5", "config": "default", "result": "HOST", "hostVersion": "v26.7.0", "speciesCalls": 1, "localError": false, "localRangeError": false } ``` The calibrated depth varies with the platform and process layout; this is expected. The PoC searches the boundary instead of relying on a fixed depth. To verify that disabling eval and WebAssembly is not sufficient: ```sh STRICT=1 node poc.js ``` ## Technical root cause 1. vm2 replaces the intrinsic `Promise.prototype.then` and `catch` methods with wrappers that sanitize callbacks and execute `resetPromiseSpecies(this)`. 2. These two prototype writes are consecutive direct assignments in `lib/setup-sandbox.js`. 3. V8 14.6 enables the `p
Last activity 1 Oct 2026. Patch Forecast updates every hour.