v3.12.1
Four advisories closed. Patch release — no API changes, with one deliberate behaviour change for NodeVM embedders who granted child_process (see Upgrade Notes).
What's Changed
Security fixes
- GHSA-6454-5x88-m6jw — an embedder-exposed host
Promisecould be turned into a sandbox escape. Sandbox code wroteconstructor[Symbol.species]onto the raw host promise and then called.then/.catch/.finallywith the settlement-direction handler omitted; V8 built the result capability throughSpeciesConstructoroff the raw object (no bridge trap on that read) and its internal Thrower/Identity reaction delivered the raw host settlement — e.g.process— to a sandbox-captured capability, with no callback slot for the rejection sanitizer to wrap. Host RCE. The bridge now shadows the host promise'sconstructorfor the duration of every sandbox-initiatedthen/catch/finallycall, so the result capability is always a genuine host%Promise%; the indirection peel also covers hostReflect.applyand.finally. - GHSA-j89j-5m6r-cr2q — any ordinary sloppy-mode host function exposed to the sandbox leaked the host realm's global object when called with a nullish receiver (
greet(),.call(null),Reflect.apply(fn, undefined, []),bind(null)()): V8'sOrdinaryCallBindThissubstituted the host global forthis, and the bridge wrapped and delivered it —greet().process.getBuiltinModule('child_process')was host RCE. The host global is now cached at bridge init and refused at every host→sandbox coercion chokepoint, delivered asundefined, which is exactly what a strict-mode function'sthisalready looked like. - GHSA-x3v6-43hc-82mc — a
NodeVMthat allowlisted thecryptobuiltin let guest code callcrypto.setFips, flipping the FIPS mode of the entire host process; the read-only wrap stops property writes but forwards host calls with full authority. Same process-wide-mutator class ascrypto.setEngine(GHSA-46pr) andtls.setDefaultCACertificates(GHSA-98xx).setFipsis now replaced with a throwing stub alongsidesetEngine;getFips()and the rest ofcryptoare unchanged. Configuration-integrity issue, not RCE. - GHSA-pq68-rvw4-xp4r —
NodeVM's hard denylist blocked every other host-process launcher (cluster,worker_threads,node:test) but omittedchild_process, sorequire: { builtin: ['*'] },['*', '-fs'], an explicit['child_process'], and thenode:spellings all handed the sandbox the real host module — oneexecSynccall was host RCE.child_processnow joinsDANGEROUS_BUILTINS: filtered from the wildcard, refused on explicit request, and covered by thenode:/ family normalization.
Documentation
- The attack catalog is now split by mechanism family:
docs/ATTACKS.mdis the index and common material, and each family lives underdocs/attacks/. Category numbers are permanent and unchanged.test/docs-catalog.jsfails the suite on a renumbering, a dead link, or a missing metadata line. - New Categories 53 (host-Promise
@@specieshijack with a missing handler) and 54 (host global leak via a sloppy function's nullishthis), plus extensions to Categories 21 (child_processjoins the denylist, with the upgrade path) and 40 (crypto.setFips), and matching rows in the "How The Bridge Defends" table.
Upgrade Notes
child_processcan no longer be granted viarequire.builtin— not by'*'and not by name, same ascluster/worker_threads/node:test. Trusted-script embedders that need it re-expose it throughrequire.mock, preferably as a narrow facade:require: { mock: { child_process: { execFileSync } } }.crypto.setFipsthrows in the sandbox. Everything else incrypto, includinggetFips(), is unchanged.- A sloppy host function called with no receiver now sees
undefinedwhere it previously got the host global. Method calls, explicit receivers, and strict / ESM functions are unaffected. - No other valid configurations are affected.
Full Changelog: v3.12.0...v3.12.1