Skip to content

Latest commit

 

History

History
116 lines (74 loc) · 24.4 KB

File metadata and controls

116 lines (74 loc) · 24.4 KB

Changelog

[3.11.5]

Patch release — no API changes.

Fix

  • #566util.inspect of vm.run(...) results rendered as Proxy(Proxy({})) on Node 26+. Install nodejs.util.inspect.custom on host-side proxy targets so the inspect output reflects the underlying shape.
  • #567 — Array iteration methods on a vm.freeze()-d host array threw an 'isExtensible' on proxy invariant error (regression from the GHSA-grj5-jjm8-h35p species defense). Align the ReadOnly proxy target's extensibility with its trap result and skip species neutralization on the host→sandbox apply path.

[3.11.4]

Ten advisories closed. Patch release — no API changes for valid configurations.

Security fixes

  • GHSA-c4cf-2hgv-2qv6 — bridge escape via BaseHandler.set ignoring the ECMA-262 §9.5.9 Receiver argument; Object.create(hostProxy).x = v and Reflect.set(hostProxy, k, v, sandboxObj) wrote through to the host object instead of installing on the receiver, turning every embedder-exposed host object into a sandbox write channel. Receiver-gated install-on-receiver fix in lib/bridge.js mirroring ReadOnlyHandler.set. See ATTACKS.md Category 32 and test/ghsa/GHSA-c4cf-2hgv-2qv6/.
  • GHSA-m5q2-4fm3-vfqp — sandbox escape via unblocked cross-realm Symbol.for keys plus missing dangerous-symbol guards on the bridge's write traps. Two-layer structural fix: lib/setup-sandbox.js denies the entire nodejs. namespace at Symbol.for and aligns the read-side filters with the full 9-symbol cache, and lib/bridge.js extends isDangerousCrossRealmSymbol and applies it to the set/defineProperty/deleteProperty traps. See ATTACKS.md Category 8 / Category 20 (both extended) and test/ghsa/GHSA-m5q2-4fm3-vfqp/.
  • GHSA-v6mx-mf47-r5wg — host prototype mutation via apply-trap indirection. Sandbox code could reach host prototype-mutating setters (Object.prototype.__proto__, setPrototypeOf, defineProperty, __defineSetter__/__defineGetter__) through Function.prototype.{call,apply,bind} and Reflect.{apply,construct} indirection, sever a host intrinsic's prototype chain, and escape via the bridge's thisEnsureThis proto-walk fallthrough. Two-layer structural fix in lib/bridge.js (apply-trap blocklist + cache check before proto-walk). See ATTACKS.md Category 30 and test/ghsa/GHSA-v6mx-mf47-r5wg/.
  • GHSA-q3fm-4wcw-g57x — Defense Invariant #11 hardening for defaultSandboxPrepareStackTrace (second variant of GHSA-9qj6-qjgg-37qq in a different file). The sandbox stack-trace formatter accumulated frames in a sandbox-realm array and .join-ed them, so a sandbox-installed setter on Array.prototype[N] (or .join override) observed bridge-internal state — no host reference reachable today, but one enrichment away from regressing into the GHSA-9qj6 RCE shape. Fixed in lib/setup-sandbox.js by folding frames through a primitive string accumulator (no Array.prototype slot reachable) and converting makeCallSiteGetters to localReflectDefineProperty for symmetry. See ATTACKS.md Category 28 Variant B and test/ghsa/GHSA-q3fm-4wcw-g57x/.
  • GHSA-76w7-j9cq-rx2j — Promise species hijack in the localPromise swallow tail. The swallow-tail apply(globalPromisePrototypeThen, this, [...]) call inside localPromise's constructor invoked the cached host Promise.prototype.then without first calling resetPromiseSpecies(this), so a sandbox subclass overriding [Symbol.species] could redirect the downstream child constructor to a user function and capture V8's internal (resolve, reject) capability — delivering a raw host-realm error (RangeError from deep recursion + e.stack) to a sandbox collector and reaching the host Function constructor via .constructor.constructor. One-line fix in lib/setup-sandbox.js adds the missing resetPromiseSpecies(this) before the swallow-tail call, matching the pattern already used by the .then/.catch/Reflect.apply overrides. See ATTACKS.md Category 31 and test/ghsa/GHSA-76w7-j9cq-rx2j/.
  • GHSA-m4wx-m65x-ghrr — NodeVM constructor patch bypass of GHSA-8hg8-63c5-gwmx: a truthy nesting paired with anything other than a real require config object produced a NESTING_OVERRIDE-only resolver → inner NodeVM with attacker-chosen requirechild_process RCE. Structural fix in lib/nodevm.js: destructure first, then reject at construction whenever nesting is truthy and requireOpts is not a non-null object or Resolver. Supersedes GHSA-8hg8-63c5-gwmx. See ATTACKS.md Category 25 and test/ghsa/GHSA-m4wx-m65x-ghrr/.
  • GHSA-6j2x-vhqr-qr7q — sandbox escape via WebAssembly JSPI (Node 24 behind --experimental-wasm-jspi, Node 26+ default). WebAssembly.promising returns Promise objects whose [[Prototype]] chain points directly at the host realm's Promise.prototype with no bridge proxy in between, so p.finally() reaches host Promise.prototype.finally, V8's SpeciesConstructor reads an attacker-controlled p.constructor getter, and the eventual host-realm rejection is dispatched through the attacker's class with no bridge wrapping — e.constructor.constructor('return process')() then evaluates in the host realm. Structural fix in lib/setup-sandbox.js: delete WebAssembly.promising and WebAssembly.Suspending at sandbox bootstrap, mirroring the existing WebAssembly.JSTag removal. Adds Defense Invariant #12 (no sandbox-visible object may have a host-realm prototype chain without bridge interposition). See ATTACKS.md Category 33 and test/ghsa/GHSA-6j2x-vhqr-qr7q/.
  • GHSA-rp36-8xq3-r6c4 — NodeVM builtin denylist bypass via process and inspector/promises. The exact-match denylist in lib/builtin.js missed two host-passthrough families: process (whose getBuiltinModule(name) reloads any core module regardless of the embedder's allow/deny configuration) and inspector/promises (whose Session().post('Runtime.evaluate', ...) evaluates attacker JS in the host realm). Structural fix promotes the check to family-prefix via isDangerousBuiltin(key), strips the node: URL prefix, and adds process to the dangerous set — enforced at both BUILTIN_MODULES source and addDefaultBuiltin. Supersedes GHSA-947f-4v7f-x2v8. Adds Defense Invariant #13. See ATTACKS.md Category 21 (extended) and test/ghsa/GHSA-rp36-8xq3-r6c4/.
  • GHSA-r9pm-gxmw-wv6p — NodeVM builtin: ['*'] wildcard exposed Node's undocumented underscored network builtins (_http_client, _http_server, the _http_* / _tls_* / _stream_* siblings), letting sandbox code make outbound HTTP requests and open listening sockets even when the documented -http/-https/-net/-tls exclusions were used — SSRF-class capability bypass (CVSS 8.6). Structural fix in lib/builtin.js: BUILTIN_MODULES filter now excludes any name starting with _, so '*' expands only to documented public builtins; explicit opt-in, mock, and override paths remain functional. See ATTACKS.md Category 34 and test/ghsa/GHSA-r9pm-gxmw-wv6p/.
  • GHSA-9g8x-92q2-p28f — NodeVM builtin allowlist surfaced four process-wide observability builtins (diagnostics_channel, async_hooks, perf_hooks, v8) that read state from the entire host process rather than the sandbox: HTTP IncomingMessage headers (incl. auth tokens) via diagnostics_channel.subscribe, embedder AsyncLocalStorage context via async_hooks.executionAsyncResource, embedder performance.mark labels via perf_hooks, and the full V8 heap via v8.getHeapSnapshot / v8.queryObjects. Fix in lib/builtin.js: extends DANGEROUS_BUILTINS with the four names, reusing the existing two-layer enforcement (BUILTIN_MODULES filter + addDefaultBuiltin rejection, family-prefix and node:-normalised via isDangerousBuiltin). mock/override escape hatches preserved. See ATTACKS.md Category 35 and test/ghsa/GHSA-9g8x-92q2-p28f/.

Upgrade notes

  • If you constructed NodeVM({ nesting: <truthy> }) without an explicit require config object, new NodeVM(...) now throws (GHSA-m4wx-m65x-ghrr). This covers every shape that previously silently produced a vm2-only resolver: omitting require entirely, or setting it to any falsy value (false/undefined/null/0/'') or any truthy non-object value (true/number/string/symbol/function); and also any truthy nesting value, not only nesting: true (1/'yes'/{}/[]/function). Either drop nesting, or pass an explicit require config object (e.g. require: { builtin: [] }) to acknowledge that vm2 will be requireable from inside the sandbox. The error message is actionable and links to the README hardening section.

[3.11.3]

Patch release — no API changes.

Security fix

  • GHSA-248r-7h7q-cr24 — async generator yield*-return thenable exception capture. Calling i.return(thenable) on an async generator delegating to a no-return inner iterator let V8's PromiseResolveThenableJob capture synchronous throws from the thenable's .then and surface them to sandbox code as iterator results — bypassing both the transformer's catch instrumentation and the globalPromise.prototype.then rejection sanitiser. Two-layer defense on %AsyncGeneratorPrototype%.next/.return/.throw in lib/setup-sandbox.js: every iterator-result promise routes value and rejection through handleException, and every thenable argument is replaced with a sandbox-realm wrapper whose .then is a fixed safeThen that sanitises sync throws and recursively re-wraps any nested thenable handed to resolve(...). When safeThen reads value.then and it is non-function, the wrapper always resolves with a {__proto__: null} shadow so V8's re-read of .then cannot observe attacker-controlled values — closing every counting/self-replacing-getter TOCTOU variant. Trade-off: identity is not preserved for non-thenable values passed to i.return(x). ATTACKS.md Category 29.

[3.11.2]

Three advisories closed. Patch release — no API changes.

Security fixes

  • GHSA-2cm2-m3w5-gp2f — Internal state reachable via computed property access on globalThis. The previous fix (GHSA-wp5r-2gw5-m7q7) tightened the transformer's identifier-rejection but left globalThis['VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL'] and every reflective probe of the global object (bracket access, Reflect.get, Object.getOwnPropertyDescriptor, Object.getOwnPropertyNames enumeration) returning the live state object — the transformer is a syntactic gate and cannot see through dynamic property keys. Structural fix: the bootstrap script (vm.js's setupSandboxScript source) now declares let VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL at the script's top level, which lands the binding in the context's [[GlobalLexicalEnvironment]] — reachable as a bare identifier from every script (so transformer-emitted catch handlers still resolve), but absent from globalThis's own-property table (so every computed-key probe returns undefined). The defineProperty install in setup-sandbox.js is removed entirely; the bootstrap IIFE assigns into the outer let instead. Supersedes GHSA-wp5r-2gw5-m7q7's identifier-only mitigation by closing the entire computed-key class. ATTACKS.md Category 27.
  • GHSA-9vg3-4rfj-wgcm — Sandbox breakout via null-proto throw / handleException. The post-GHSA-mpf8 hardening switched handleException and globalPromise.prototype.then onFulfilled to wrap caught/resolved values with bridge.from() for "symmetry". from() builds a sandbox-side proxy whose target the bridge treats as host-realm; calling it on a sandbox-realm null-proto value ({__proto__: null} thrown or Promise.resolve-d by sandbox JS) produced a proxy whose set trap unwrapped sandbox proxies of host references (e.g. Buffer.prototype.inspect) back to their raw host originals and stored them on the underlying sandbox object — readable via the original sandbox reference and pivot to host Function constructor → RCE. Three callsites in lib/setup-sandbox.js reverted to ensureThis() semantics; the host-Promise rejection sanitizer composes from() outside handleException so the GHSA-mpf8 invariant (host null-proto rejection values must reach sandbox callbacks bridge-wrapped) is preserved. ATTACKS.md Category 26.
  • GHSA-9qj6-qjgg-37qq — sandbox breakout via the species-defense helper neutralizeArraySpeciesBatch. The helper appended saved-state records to a fresh [] literal that — being allocated by the sandbox-side bridge closure — inherited sandbox Array.prototype. A sandbox-installed setter on Array.prototype[N] therefore captured the next saved[saved.length] = c write and exposed c.arr (a host-realm proxy) directly to attacker code, leading to host Function extraction and RCE. Fixed in lib/bridge.js by writing every saved-state entry through thisReflectDefineProperty so the appended slot is an own data property and no Array.prototype[N] setter is ever invoked while the bridge holds raw saved state. ATTACKS.md gains a new Defense Invariant ("Bridge-internal containers must not invoke sandbox code") codifying the cross-cutting principle.

[3.11.1]

Single advisory closed plus prominent documentation of an existing escape hatch. Patch release — no API changes for valid configurations.

Security fix

  • GHSA-8hg8-63c5-gwmxnesting: true bypassed require: false, allowing sandbox-to-host RCE via inner NodeVM construction. The contradictory option pair { nesting: true, require: false } now throws VMError at new NodeVM(...) time citing the advisory. Same shape as the GHSA-cp6g eager FileSystem-contract probe — surface contradictory configuration at the API surface, not silently produce an unsandboxed sandbox. ATTACKS.md Category 25.

Documentation

  • New README section "nesting: true is an escape hatch" under Hardening recommendations. Explains that nesting: true lets sandbox code require('vm2') and construct nested NodeVMs whose require config is chosen by the sandbox (not constrained by the outer config — by design of nesting). Do not enable nesting: true for untrusted code.
  • JSDoc on the nesting option (lib/nodevm.js) upgraded to spell out the escape-hatch semantics and the GHSA-8hg8 contradictory-pair rejection.
  • ATTACKS.md gains Category 25 documenting the configuration trap and a matching row in the "How The Bridge Defends" table.

Upgrade notes

  • If you set { nesting: true, require: false } anywhere in your codebase, new NodeVM(...) now throws. Either drop nesting: true (if you wanted deny-all), or replace require: false with an explicit require config (e.g. require: { builtin: [] }) to acknowledge that vm2 will be requireable. The error message is actionable and links to the README section.
  • No other configurations are affected. Bare new NodeVM({ nesting: true }) continues to work as documented; this is the documented escape hatch and is not closed by this patch (out of scope — would change nesting: true semantics substantially).

What this fix does NOT close

nesting: true itself remains an escape hatch for any non-trivial require config. The fix closes the specific contradictory pair flagged by the advisory; the broader recommendation is in the new README section: do not enable nesting: true when running untrusted code. Constraint propagation from outer to inner NodeVM (where the outer's require config would constrain inner construction) was considered and deferred — it would change the documented semantics of nesting: true and is a major-version-shaped change.

[3.11.0]

Coordinated security release closing 13 advisories, plus a new bufferAllocLimit option and a realpath() method on the FileSystem adapter contract. Minor version bump because of the new public option and the FileSystem contract addition; no incompatible changes to the existing public API surface. Embedders running untrusted code in memory-constrained environments should review the new bufferAllocLimit option and the README's Hardening recommendations section.

Upgrade notes

  • Custom fs adapters with require.root must implement realpathSync (or realpath() on a fully custom FileSystem class). Without it, new NodeVM({require: {root, fs: customAdapter}}) now throws a VMError at construction, citing GHSA-cp6g-6699-wx9c. The eager probe converts what was previously silent deny-by-default at every later require() into a single, clearly-labelled construction-time error. Default fs users are unaffected — DefaultFileSystem and VMFileSystem ship realpath() out of the box.
  • Embedders running untrusted async code should install a host-side unhandledRejection handler. The GHSA-hw58 fix closes synchronous executor throws but cannot reach async-function / async-generator / await using rejection paths (V8 creates rejection promises via the realm's intrinsic Promise). See README's Hardening recommendations and ATTACKS.md Category 22.
  • Embedders running untrusted code in memory-constrained environments should opt into a finite bufferAllocLimit (e.g. 32 * 1024 * 1024) as part of layered DoS defense. Default remains Infinity for backwards compatibility.

Security fixes

  • GHSA-grj5-jjm8-h35p — Array species self-return sandbox escape. Bridge apply and construct traps now neutralise host-array constructor and Symbol.species before every host call (and restore in a finally block). Direct write, Object.assign, non-configurable defineProperty, and prototype-level constructor variants all blocked.
  • GHSA-v37h-5mfm-c47c — Handler reconstruction via util.inspect leak. Three-layer defense: closure-scoped construction token, getHandlerObject WeakMap guard, and .constructor sentinel rebind on every handler-class prototype (including BufferHandler).
  • GHSA-qcp4-v2jj-fjx8 — Trap method on leaked handler with forged target. New handlerToTarget WeakMap pairs every handler with its canonical proxy target at construction; validateHandlerTarget(this, target) at the entry of every trap method rejects forged-this and forged-target invocations with VMError(OPNA).
  • GHSA-47x8-96vw-5wg6 — Cross-realm symbol extraction from host objects. Two-layer defense: dangerous cross-realm symbols (nodejs.util.inspect.custom, nodejs.rejection, nodejs.util.promisify.custom) are filtered at the bridge boundary; structural identity collapse pre-populates the bridge identity caches for every built-in intrinsic prototype + constructor pair so prototype walks land on sandbox primordials.
  • GHSA-55hx-c926-fr95 — Promise structural-leak / SuppressedError / AggregateError sanitisation. handleException now recurses into AggregateError.errors[] (in addition to SuppressedError.error/.suppressed); the bridge-level apply-trap recognises calls to host Promise.prototype.{then,catch,finally} by cached identity and pipes every sandbox callback through the same sanitiser.
  • GHSA-vwrp-x96c-mhwq — Host intrinsic prototype pollution via bridge write traps. Closure-scoped protectedHostObjects WeakMap is populated at bridge init with every entry in globalsList + errorsList (including AggregateError) plus each prototype's .constructor. The four write traps (set, defineProperty, deleteProperty, preventExtensions) reject with VMError(OPNA) when targeting a protected intrinsic.
  • GHSA-947f-4v7f-x2v8 — NodeVM builtin allowlist bypass via host-passthrough builtins. DANGEROUS_BUILTINS = ['module', 'worker_threads', 'cluster', 'vm', 'repl', 'inspector', 'trace_events', 'wasi']. Two-layer enforcement: filtered from BUILTIN_MODULES (closes '*' wildcard expansion) AND rejected in addDefaultBuiltin (closes explicit-name + makeBuiltins(...) paths). mock / override escape hatches preserved.
  • GHSA-hw58-p9xv-2mjh — Promise executor unhandled rejection host-process DoS. localPromise constructor wraps the user-supplied executor in try/catch (synchronous throws routed through handleException and rejected as sandbox-realm values) and attaches a benign swallow tail to every sandbox-constructed Promise so the host's unhandledRejection event never fires. Known residual: async function / async generator / await using paths bypass the executor wrap (V8 creates rejection promises via the realm's intrinsic Promise). Documented in ATTACKS.md Category 22 — embedders should install a host-side unhandledRejection handler. See README's Hardening recommendations.
  • GHSA-6785-pvv7-mvg7 — Unbounded Buffer.alloc(N) host-heap DoS. New bufferAllocLimit option (default Infinity — fully backwards-compatible) caps single allocations on Buffer.alloc, Buffer.allocUnsafe, Buffer.allocUnsafeSlow, deprecated Buffer(N), and new Buffer(N). Embedders running untrusted code should opt into a finite cap (e.g. 32 * 1024 * 1024) as part of layered DoS defense, the same way they opt into timeout. Forwarded from NodeVM to its parent VM via super(options).
  • GHSA-mpf8-4hx2-7cjg — Host Promise .then(onFulfilled) / sanitiser-callback null-proto unwrapping. Sandbox-side globalPromise.prototype.then onFulfilled and the bridge-level host-Promise sanitiser now use from() (always wraps) instead of ensureThis() (proto-fallthrough on null-proto host objects). handleException also switched to from() for symmetry on the rejection path.
  • GHSA-v27g-jcqj-v8rwCallSite host-frame information disclosure via prepareStackTrace. applyCallSiteGetters redacts every metadata getter (getFileName, getLineNumber, getColumnNumber, getFunctionName, getMethodName, getTypeName, etc.) for host frames; getEvalOrigin redacts unconditionally because its return string can embed a host path. Error.prepareStackTrace is initialised to defaultSandboxPrepareStackTrace at sandbox bootstrap so V8 never falls through to Node's host-side formatter (which throws on Symbol-named errors and emits absolute host paths).
  • GHSA-wp5r-2gw5-m7q7 — Transformer fast-path bypass via with/INTERNAL_STATE_NAME/unicode-escape identifier. Fast-path bailout now triggers AST instrumentation for any source containing catch, import, async, with, the INTERNAL_STATE_NAME substring, or any \u escape (identifiers like VM2_INTERNAL_STATE_… are valid JS and would slip past a literal-string check).
  • GHSA-cp6g-6699-wx9c — NodeVM require.root symlink bypass (path-check / use TOCTOU). Lexical prefix check on path.resolve()-resolved candidates was bypassed by symlinks inside the allowed root pointing outside it (Node's native require() follows symlinks; CWE-59). Especially severe with pnpm / npm-workspaces / npm link layouts where every node_modules entry is a symlink by design. Fixed by canonicalising candidate paths via fs.realpathSync before the prefix check and canonicalising rootPaths at construction time. DefaultFileSystem and VMFileSystem gain a realpath() method; if realpath throws at runtime (missing file, broken link) the check denies by default. An eager FileSystem-contract probe at new NodeVM(...) time throws VMError immediately if require.root is set and the adapter cannot dereference symlinks (missing realpath() method, or VMFileSystem wrapping an fs without realpathSync) — see Upgrade notes. ATTACKS.md Category 24.

New options

  • bufferAllocLimit (VM, NodeVM) — non-negative number or Infinity. Caps individual Buffer.alloc family requests from inside the sandbox. Default: Infinity. See README's "Hardening recommendations".

Other security improvements

  • trace_events host-process abort DoS — surfaced during pre-tag red-team. trace_events.createTracing({categories: [Proxy<Array>]}) triggered a C++ IsArray() assertion failure that aborted the host process. Added to DANGEROUS_BUILTINS.
  • wasi added to the denylist — experimental syscall surface (filesystem preopens, host clock/random, network) too broad for default '*' exposure.

Documentation

  • New README "Hardening recommendations" section covering bufferAllocLimit usage, unhandledRejection handler shape (mitigates async-fn residual above), --max-old-space-size complement, and '*' allowlist semantics.
  • ATTACKS.md updated for Categories 4, 9, 12, 19, 20, 21, 22, 24 to reflect the deployed defenses, the v27g getEvalOrigin/Path A hardening, the qcp4 validateHandlerTarget, the wp5r unicode-escape hardening, the GHSA-hw58 async-fn known residual, and the new cp6g symlink-bypass mitigation.

Test infrastructure

  • scripts/legacy-test-runner.js now supports this.skip() for runtime-conditional skipping and Promise-returning async tests (length-0 async function () {}).