CVE-2026-92952 · GHSA-jf8q-945g-9q4c
vm2: Incomplete nodejs.* symbol filtering lets sandbox override host WebStream state checks
- Risk score
- 59/100
- CVSS
- 6.8 v3.1
- Exploit chance (30 days)
- 0.5%
- Published
- 1 Oct 2026
Open in the live forecast Today's forecast
What to know
- What it is
- vm2 current head (v3.11.5, commit 7a1f5100b96f48d34e0fe104ab37c0acc5944f92) still exposes registered Node.js internal symbols from host WebStream prototypes to sandbox code. The prior nodejs.* symbol hardening blocks Symbol.for('nodejs.<name>') at the source, but the extraction filters and bridge write traps still enu…
- Affected
- vm2 (npm) >= 3.11.4, <= 3.11.6
- Fix
- Upgrade vm2 to 3.11.7 or later.
- Exploitation
- Public proof of concept
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-92952: Incomplete nodejs.* symbol filtering lets sandbox override host WebStream state checks (CVSS 6.8 · EPSS 0%)
- 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-92956: Vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass (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-669 (CWE-669). 8 other warnings in 30 days
Why it is Yellow
- CVSS 6.8 medium
- Public exploit or proof of concept referenced
- On your watchlist: npm
Affected packages
| Package | Ecosystem | Vulnerable | Fixed in |
|---|---|---|---|
| vm2 | npm | >= 3.11.4, <= 3.11.6 | 3.11.7 |
References
- https://github.com/advisories/GHSA-jf8q-945g-9q4c
- https://github.com/patriksimek/vm2/security/advisories/GHSA-jf8q-945g-9q4c
- https://nvd.nist.gov/vuln/detail/CVE-2026-92952
- https://github.com/patriksimek/vm2/commit/a45444d5abbdfa59055528f584df5f21cc7c42da
- https://github.com/patriksimek/vm2/releases/tag/v3.11.7
- https://www.vulncheck.com/advisories/vm2-3.11.4-through-3.11.6-sandbox-symbol-filtering-bypass
- https://github.com/advisories/GHSA-jf8q-945g-9q4c
Original description
## Summary vm2 current head (`v3.11.5`, commit `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`) still exposes registered Node.js internal symbols from host WebStream prototypes to sandbox code. The prior `nodejs.*` symbol hardening blocks `Symbol.for('nodejs.<name>')` at the source, but the extraction filters and bridge write traps still enumerate a fixed set of known registered symbols. On Node.js `v25.8.0`, `stream/web` exposes two additional registered symbols: - `nodejs.stream.disturbed` - `nodejs.stream.errored` Sandbox code can extract those real host symbols with `Object.getOwnPropertySymbols(streamWeb.ReadableStream.prototype)` and then use them as write keys on host objects. On a real host `ReadableStream`, an attacker can make `stream.Readable.isDisturbed(stream)` return `false` after the stream has already been read. ## Technical Details `lib/setup-sandbox.js` correctly blocks future `nodejs.*` keys at the `Symbol.for()` source: ```js if (apply(localStringStartsWith, keyStr, ['nodejs.'])) { ... return fresh; } ``` However, the extraction filters are still driven by a fixed `realDangerousSymbols` list. That list does not include `nodejs.stream.disturbed` or `nodejs.stream.errored`, so `Object.getOwnPropertySymbols()` and related paths can still return those real host symbols. `lib/bridge.js` has the same fixed-list problem in `isDangerousCrossRealmSymbol()` and in the host-result scrub list. Because the two new symbols are not recognized, the `set` and `defineProperty` traps allow sandbox-originated writes using those keys. Current-head source references: - `lib/setup-sandbox.js:180-196` denies `Symbol.for('nodejs.*')` by namespace. - `lib/setup-sandbox.js:214-230` uses a fixed `realDangerousSymbols` list for extraction filtering; the two reported symbols are absent. - `lib/bridge.js:187-199` uses a fixed `isDangerousCrossRealmSymbol()` list; the two reported symbols are absent. - `lib/bridge.js:1499-1509` treats the write trap as the last line of defense, but it only rejects keys recognized by that fixed list. ## Impact Sandbox code can corrupt host-visible WebStream state checks for host WebStream objects that cross into the sandbox. In the validated PoV, a stream that the host has already consumed is made to appear undisturbed to `stream.Readable.isDisturbed()`. This can bypass host logic that relies on Node's public stream-state helpers to enforce one-shot body consumption, reject errored streams, or decide whether a host WebStream is safe to hand to another component. This is not a host-code-execution primitive in the current PoV. The report is an incomplete-fix / guard-coverage gap in the same symbol-boundary family as the prior `nodejs.*` symbol advisory. ## Affected Package/Versions Confirmed affected on Node.js `v25.8.0`: - `v3.11.4` - `v3.11.5` - current head `7a1f5100b96f48d34e0fe104ab37c0acc5944f92` `v3.11.3` is also affected, but it predates the broader `nodejs.*` symbol fix. For this incomplete-fix report, the suggested affected range is `>= 3.11.4, <= 3.11.5` on Node.js versions where these stream symbols exist. No patched version is known. ## Configuration Required The PoV uses a `VM` where the embedder exposes a host WebStream object and the host `stream/web` module object to sandbox code: ```js const vm = new VM({ sandbox: { rs, streamWeb } }); ``` This matches the same trust boundary as the earlier cross-realm symbol-write class: sandbox code must not be able to obtain registered Node.js internal symbols and write them back onto host objects. The PoV is local-only. It does not require network access, a public target, `NodeVM` builtin access, `process`, filesystem access, or child-process access. ## Local Proof of Concept Run from the `oss-zero-day-harness` directory: ```sh node submission-bundle/vm2-pov-test-incomplete-nodejs-stream-symbol-filter/pov-nodejs-stream-symbol-incomplete-fix.js ``` The PoV: 1. The host creates a `ReadableStream`. 2. The host reads one chunk so `stream.Readable.isDisturbed(rs)` is `true`. 3. Sandbox code confirms `Symbol.for('nodejs.stream.disturbed')` is blocked and returns a sandbox-local symbol. 4. Sandbox code extracts the real registered `nodejs.stream.disturbed` / `nodejs.stream.errored` symbols from the host `ReadableStream.prototype`. 5. Sandbox code writes an own `nodejs.stream.disturbed` property onto the host stream with value `false`. 6. The host calls `stream.Readable.isDisturbed(rs)` again and receives `false`. Observed result on current head: ```json { "beforeHostDisturbed": true, "controls": { "symbolForDisturbedIsRegistered": false, "symbolForErroredIsRegistered": false }, "extracted": [ { "description": "nodejs.stream.disturbed", "keyFor": "nodejs.stream.disturbed" }, { "description": "nodejs.stream.errored", "keyFor": "nodejs.stream.errored" } ], "overrideDisturbedOk": true, "afterHostDisturbed": false } ``` ## Official Disclosure Policy Fit vm2's `SECURITY.md` asks reporters not to create a public issue and to submit It asks for reproduction steps, affected versions, environment/configuration details, and potential impact: - Policy: https://github.com/patriksimek/vm2/blob/main/SECURITY.md - Private report route: https://github.com/patriksimek/vm2/security/advisories/new This bundle is formatted for that private GitHub report flow and should not be posted publicly before maintainer triage and a fixed release. ## Suggested Fix Direction Make the dangerous-symbol checks namespace-based instead of list-based: - In `setup-sandbox.js`, make `isDangerousSymbol(sym)` return true for any registered symbol whose `Symbol.keyFor(sym)` starts with `nodejs.`. - In `bridge.js`, make `isDangerousCrossRealmSymbol(key)` do the same for any symbol key crossing the bridge. - In host-result scrubbing, delete all own symbol keys whose registered key starts with `nodejs.` instead of iterating a hard-coded list. - Keep the current explici
Last activity 1 Oct 2026. Patch Forecast updates every hour.