GitHub Advisoryadvisory · npm
GHSA-27g9-p43v-cw3v vm2 sandbox escape on Node.js 26 through a stale PromiseThenLookupChain protector
CRITICAL
Official page ↗
- Aliases
- CVE-2026-92944
- Ecosystem
- npm
- Packages
- vm2
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-693, CWE-913
- Reviewed
- 2026-10-01
Affected packages
• npm vm2: >= 3.10.2, < 3.11.7 → fixed in 3.11.7
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`: `[email protected]`
• `[email protected]` 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:
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 [email protected]
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 [email protected] >/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 `proto_assign_seq_opt` optimization, combining this assignment sequence into `SetPrototypeProperties`.
4. In the Node.js 26 implementation, the existing-data-property branch calls `Object::SetDataProperty(&it, value)` without first calling `it.UpdateProtector()`.
5. The JavaScript property now contains vm2's wrapper, but the V8 `PromiseThenLookupChain` protector incorrectly remains valid.
6. `Promise.prototype.finally()` performs an internal `InvokeThen`. Because the protector appears valid, V8 directly selects the native Promise `then` instead of performing the observable property lookup that would reach vm2's wrapper.
7. The wrapper never executes, so the attacker's own `constructor[Symbol.species]` is not reset before the native `then` creates its result capability.
8. A native Promise reaction calls the attacker-provided capability resolve function. At a calibrated stack boundary, V8 creates a host-realm `RangeError` and passes it to the attacker-provided capability reject function without vm2 conversion.
9. `error.constructor.constructor` is consequently the host `Function`, even when the VM was configured with `eval:false`.
The root-cause control is strong:
```sh
node --no-proto-assign-seq-opt poc.js
```
On Node.js 26.7.0 this changes the result from `HOST` to `SAFE` (3/3). A separate semantic probe also changes from `wrapperCalls=0` to `wrapperCalls=1`.
Relationship to previous advisories
This report intentionally discloses the overlap with prior work:
CVE-2026-22709 / GHSA-99p7-6v5w-7xg8
That issue concerned bypassing callback sanitization on intrinsic Promises returned by async functions. vm2's fix added/strengthened the `globalPromise.prototype.then` and `catch` wrappers. The current issue is different: on V8 14.6, `finally()` trusts a stale protector and bypasses those installed wrappers at the engine fast path.
CVE-2026-47208 / GHSA-76w7-j9cq-rx2j
That issue used a missing `resetPromiseSpecies` call in `localPromise`'s rejection-swallowing tail. It is marked fixed in 3.11.4. …
Related
Alias · CVE (1)
- CVE-2026-92944 vm2 versions 3.10.2 through 3.11.6 contain a sandbox escape vulnerability on Node.js 26 where Promise.prototype.finally() bypasses vm2's… vulnerability
References
- https://github.com/patriksimek/vm2/security/advisories/GHSA-27g9-p43v-cw3v web
- https://nvd.nist.gov/vuln/detail/CVE-2026-92944 advisory
- https://github.com/patriksimek/vm2/commit/c7df2e21d2c74038a1f1750f40f9fe603ec69c6a web
- https://github.com/patriksimek/vm2 package
- https://github.com/patriksimek/vm2/releases/tag/v3.11.7 web
- https://www.vulncheck.com/advisories/vm2-3.10.2-through-3.11.6-sandbox-escape-via-promise-protector web