{"id":"GHSA-jf8q-945g-9q4c","summary":"vm2: Incomplete nodejs.* symbol filtering lets sandbox override host WebStream state checks","details":"## Summary\n\nvm2 current head (`v3.11.5`, commit `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`) still exposes registered Node.js internal symbols from host WebStream prototypes to sandbox code.\n\nThe prior `nodejs.*` symbol hardening blocks `Symbol.for('nodejs.\u003cname\u003e')` 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:\n\n- `nodejs.stream.disturbed`\n- `nodejs.stream.errored`\n\nSandbox 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.\n\n## Technical Details\n\n`lib/setup-sandbox.js` correctly blocks future `nodejs.*` keys at the `Symbol.for()` source:\n\n```js\nif (apply(localStringStartsWith, keyStr, ['nodejs.'])) {\n  ...\n  return fresh;\n}\n```\n\nHowever, 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.\n\n`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.\n\nCurrent-head source references:\n\n- `lib/setup-sandbox.js:180-196` denies `Symbol.for('nodejs.*')` by namespace.\n- `lib/setup-sandbox.js:214-230` uses a fixed `realDangerousSymbols` list for extraction filtering; the two reported symbols are absent.\n- `lib/bridge.js:187-199` uses a fixed `isDangerousCrossRealmSymbol()` list; the two reported symbols are absent.\n- `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.\n\n## Impact\n\nSandbox 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()`.\n\nThis 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.\n\nThis 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.\n\n## Affected Package/Versions\n\nConfirmed affected on Node.js `v25.8.0`:\n\n- `v3.11.4`\n- `v3.11.5`\n- current head `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`\n\n`v3.11.3` is also affected, but it predates the broader `nodejs.*` symbol fix. For this incomplete-fix report, the suggested affected range is `\u003e= 3.11.4, \u003c= 3.11.5` on Node.js versions where these stream symbols exist.\n\nNo patched version is known.\n\n## Configuration Required\n\nThe PoV uses a `VM` where the embedder exposes a host WebStream object and the host `stream/web` module object to sandbox code:\n\n```js\nconst vm = new VM({ sandbox: { rs, streamWeb } });\n```\n\nThis 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.\n\nThe PoV is local-only. It does not require network access, a public target, `NodeVM` builtin access, `process`, filesystem access, or child-process access.\n\n## Local Proof of Concept\n\nRun from the `oss-zero-day-harness` directory:\n\n```sh\nnode submission-bundle/vm2-pov-test-incomplete-nodejs-stream-symbol-filter/pov-nodejs-stream-symbol-incomplete-fix.js\n```\n\nThe PoV:\n\n1. The host creates a `ReadableStream`.\n2. The host reads one chunk so `stream.Readable.isDisturbed(rs)` is `true`.\n3. Sandbox code confirms `Symbol.for('nodejs.stream.disturbed')` is blocked and returns a sandbox-local symbol.\n4. Sandbox code extracts the real registered `nodejs.stream.disturbed` / `nodejs.stream.errored` symbols from the host `ReadableStream.prototype`.\n5. Sandbox code writes an own `nodejs.stream.disturbed` property onto the host stream with value `false`.\n6. The host calls `stream.Readable.isDisturbed(rs)` again and receives `false`.\n\nObserved result on current head:\n\n```json\n{\n  \"beforeHostDisturbed\": true,\n  \"controls\": {\n    \"symbolForDisturbedIsRegistered\": false,\n    \"symbolForErroredIsRegistered\": false\n  },\n  \"extracted\": [\n    {\n      \"description\": \"nodejs.stream.disturbed\",\n      \"keyFor\": \"nodejs.stream.disturbed\"\n    },\n    {\n      \"description\": \"nodejs.stream.errored\",\n      \"keyFor\": \"nodejs.stream.errored\"\n    }\n  ],\n  \"overrideDisturbedOk\": true,\n  \"afterHostDisturbed\": false\n}\n```\n\n## Official Disclosure Policy Fit\n\nvm2'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:\n\n- Policy: https://github.com/patriksimek/vm2/blob/main/SECURITY.md\n- Private report route: https://github.com/patriksimek/vm2/security/advisories/new\n\nThis bundle is formatted for that private GitHub report flow and should not be posted publicly before maintainer triage and a fixed release.\n\n## Suggested Fix Direction\n\nMake the dangerous-symbol checks namespace-based instead of list-based:\n\n- In `setup-sandbox.js`, make `isDangerousSymbol(sym)` return true for any registered symbol whose `Symbol.keyFor(sym)` starts with `nodejs.`.\n- In `bridge.js`, make `isDangerousCrossRealmSymbol(key)` do the same for any symbol key crossing the bridge.\n- In host-result scrubbing, delete all own symbol keys whose registered key starts with `nodejs.` instead of iterating a hard-coded list.\n- Keep the current explicit list only as regression documentation, not as the complete security boundary.\n\nRegression tests should include:\n\n- `Object.getOwnPropertySymbols(ReadableStream.prototype)` must not expose `nodejs.stream.disturbed` or `nodejs.stream.errored`.\n- A sandbox-local `Symbol.for('nodejs.stream.disturbed')` write must not affect host `stream.Readable.isDisturbed()`.\n- Even if the real symbol is passed into the sandbox by a host test harness, `set`, `defineProperty`, and `deleteProperty` traps must reject writes and deletes against host objects.\n\n## Why This Is Not Intended Behavior\n\nThe hardening comments and tests establish the intended invariant:\n\n- any `nodejs.*` internal symbol should be sandbox-local when requested through `Symbol.for()`;\n- dangerous registered symbols should not be enumerable/extractable from host objects;\n- even if a sandbox obtains one, bridge write traps should reject writes using that key.\n\nThis report shows that the source-side rule is active, but the extraction and write-trap rules are incomplete for newer registered `nodejs.stream.*` symbols.\n\nNode's stream state helpers consult these symbols directly. For example, `stream.Readable.isDisturbed()` reads the internal disturbed symbol before falling back to public state. After sandbox writes an own property under the extracted symbol, the host helper returns attacker-controlled state.\n\nNode's public documentation describes `stream.isErrored(stream)` as reporting whether a stream has encountered an error, and `stream.Readable.isDisturbed(stream)` as reporting whether the stream has been read from or cancelled:\n\n- https://nodejs.org/api/stream.html#streamiserroredstream\n- https://nodejs.org/api/stream.html#streamreadableisdisturbedstream","aliases":["CVE-2026-92952"],"modified":"2026-10-01T16:00:05.084630653Z","published":"2026-10-01T15:42:15Z","database_specific":{"cwe_ids":["CWE-669"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-10-01T15:42:15Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/patriksimek/vm2/security/advisories/GHSA-jf8q-945g-9q4c"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-92952"},{"type":"WEB","url":"https://github.com/patriksimek/vm2/commit/a45444d5abbdfa59055528f584df5f21cc7c42da"},{"type":"PACKAGE","url":"https://github.com/patriksimek/vm2"},{"type":"WEB","url":"https://github.com/patriksimek/vm2/releases/tag/v3.11.7"},{"type":"WEB","url":"https://www.vulncheck.com/advisories/vm2-3.11.4-through-3.11.6-sandbox-symbol-filtering-bypass"}],"affected":[{"package":{"name":"vm2","ecosystem":"npm","purl":"pkg:npm/vm2"},"ranges":[{"type":"SEMVER","events":[{"introduced":"3.11.4"},{"fixed":"3.11.7"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 3.11.6","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-jf8q-945g-9q4c/GHSA-jf8q-945g-9q4c.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:H/A:N"}]}