Skip to content

v3.12.1

Choose a tag to compare

@patriksimek patriksimek released this 03 Sep 15:21
· 8 commits to main since this release

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 Promise could be turned into a sandbox escape. Sandbox code wrote constructor[Symbol.species] onto the raw host promise and then called .then / .catch / .finally with the settlement-direction handler omitted; V8 built the result capability through SpeciesConstructor off 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's constructor for the duration of every sandbox-initiated then / catch / finally call, so the result capability is always a genuine host %Promise%; the indirection peel also covers host Reflect.apply and .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's OrdinaryCallBindThis substituted the host global for this, 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 as undefined, which is exactly what a strict-mode function's this already looked like.
  • GHSA-x3v6-43hc-82mc — a NodeVM that allowlisted the crypto builtin let guest code call crypto.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 as crypto.setEngine (GHSA-46pr) and tls.setDefaultCACertificates (GHSA-98xx). setFips is now replaced with a throwing stub alongside setEngine; getFips() and the rest of crypto are unchanged. Configuration-integrity issue, not RCE.
  • GHSA-pq68-rvw4-xp4rNodeVM's hard denylist blocked every other host-process launcher (cluster, worker_threads, node:test) but omitted child_process, so require: { builtin: ['*'] }, ['*', '-fs'], an explicit ['child_process'], and the node: spellings all handed the sandbox the real host module — one execSync call was host RCE. child_process now joins DANGEROUS_BUILTINS: filtered from the wildcard, refused on explicit request, and covered by the node: / family normalization.

Documentation

  • The attack catalog is now split by mechanism family: docs/ATTACKS.md is the index and common material, and each family lives under docs/attacks/. Category numbers are permanent and unchanged. test/docs-catalog.js fails the suite on a renumbering, a dead link, or a missing metadata line.
  • New Categories 53 (host-Promise @@species hijack with a missing handler) and 54 (host global leak via a sloppy function's nullish this), plus extensions to Categories 21 (child_process joins the denylist, with the upgrade path) and 40 (crypto.setFips), and matching rows in the "How The Bridge Defends" table.

Upgrade Notes

  • child_process can no longer be granted via require.builtin — not by '*' and not by name, same as cluster / worker_threads / node:test. Trusted-script embedders that need it re-expose it through require.mock, preferably as a narrow facade: require: { mock: { child_process: { execFileSync } } }.
  • crypto.setFips throws in the sandbox. Everything else in crypto, including getFips(), is unchanged.
  • A sloppy host function called with no receiver now sees undefined where 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