Close NodeVM builtin denylist bypass via process/inspector subpaths

fix(GHSA-rp36-8xq3-r6c4): close NodeVM builtin denylist bypass via process/inspector subpaths
Close NodeVM builtin denylist bypass via process/inspector subpathsa1ed47-fix-close-nodevm-builtin-denylist-bypass-via-cve-2026-47140fixclose nodevm builtin denylist bypass via

Committed by patriksimek

commit message
fix(GHSA-rp36-8xq3-r6c4): close NodeVM builtin denylist bypass via process/inspector subpaths

The denylist in `lib/builtin.js` was exact-match, so two host-passthrough
families slipped through every enforcement layer:
 
  - `require('process').getBuiltinModule(name)` (Node 22+) reloads ANY core
    module — including `child_process` — regardless of the embedder's
    allow/deny configuration. Also exposes `binding`, `dlopen`,
    `_linkedBinding`, and the raw host `process.env`.
  - `require('inspector/promises').Session().post('Runtime.evaluate', ...)`
    evaluates attacker JS in the host realm. The subpath shape
    `inspector/promises` does not match the `inspector` denylist entry.
 
Both reached host RCE under the otherwise-correct configs
`builtin: ['*', '-child_process']` and `builtin: ['*', '-child_process',
'-inspector']`.
 
Structural fix in `lib/builtin.js`:
 
  - `DANGEROUS_BUILTINS` extended with `process`.
  - New `isDangerousBuiltin(key)` promotes the check to family-prefix
    matching: any `<family>/...` whose family is in the set is blocked. The
    `node:` URL-style prefix is stripped before matching so
    `node:process` and `node:inspector/promises` share fate with their
    canonical spellings.
  - Enforced at both `BUILTIN_MODULES` (closes the `'*'` wildcard expansion
    path) and `addDefaultBuiltin` (closes the explicit-allowlist path and
    the low-level `makeBuiltins` API).
  - `SPECIAL_MODULES` / `mocks` / `overrides` escape hatches preserved —
    embedders who genuinely need a stub for any blocked name can register a
    sandbox-safe replacement.
 
Supersedes GHSA-947f-4v7f-x2v8 (whose exact-match denylist missed both
families). Restores and tightens Defense Invariant #13 (NodeVM builtin
allowlist is a closed system) — added in this commit.
 
Tests: `test/ghsa/GHSA-rp36-8xq3-r6c4/repro.js` (175 lines) covers
`require('process').getBuiltinModule('child_process')`,
`require('inspector/promises').Session().post('Runtime.evaluate', ...)`,
the `node:` URL-style spellings, the explicit-allowlist path
(`builtin: ['process']` and `builtin: ['inspector/promises']`), the
low-level `makeBuiltins` API, and confirms `SPECIAL_MODULES` overrides
still work.
 
Docs: `docs/ATTACKS.md` Category 21 extended with the new attack flow,
both canonical PoCs, and the three-layer mitigation. New Defense
Invariant #13 added under the existing #12 (WebAssembly JSPI). New row
in the "How The Bridge Defends" table.
 
Version bump deferred — this lands alongside seven other advisories in
the pre-existing [3.11.4] release slot.

Editor assessment

This commit patches CVE-2026-47140. The body documents two host-passthrough families that beat the exact-match denylist: process.getBuiltinModule reloading child_process and inspector/promises reaching Runtime.evaluate, including the node: spellings and both bypass configurations. It then lists the structural fix—family-prefix matching via isDangerousBuiltin, enforced on the wildcard and explicit paths—and enumerates a 175-line repro plus docs updates. That is thorough, maybe excessively so, and the closing note that the version bump is deferred is honest. The header is the weak point: GHSA-rp36-8xq3-r6c4 as scope, and a subject that only gestures at subpaths. Acceptable.

Browse categories

More fix examples · Back to search