Patch release — no API changes.
- #566 —
util.inspectofvm.run(...)results rendered asProxy(Proxy({}))on Node 26+. Installnodejs.util.inspect.customon 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 proxyinvariant 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.
Ten advisories closed. Patch release — no API changes for valid configurations.
- GHSA-c4cf-2hgv-2qv6 — bridge escape via
BaseHandler.setignoring the ECMA-262 §9.5.9Receiverargument;Object.create(hostProxy).x = vandReflect.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 inlib/bridge.jsmirroringReadOnlyHandler.set. See ATTACKS.md Category 32 andtest/ghsa/GHSA-c4cf-2hgv-2qv6/. - GHSA-m5q2-4fm3-vfqp — sandbox escape via unblocked cross-realm
Symbol.forkeys plus missing dangerous-symbol guards on the bridge's write traps. Two-layer structural fix:lib/setup-sandbox.jsdenies the entirenodejs.namespace atSymbol.forand aligns the read-side filters with the full 9-symbol cache, andlib/bridge.jsextendsisDangerousCrossRealmSymboland applies it to theset/defineProperty/deletePropertytraps. See ATTACKS.md Category 8 / Category 20 (both extended) andtest/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__) throughFunction.prototype.{call,apply,bind}andReflect.{apply,construct}indirection, sever a host intrinsic's prototype chain, and escape via the bridge'sthisEnsureThisproto-walk fallthrough. Two-layer structural fix inlib/bridge.js(apply-trap blocklist + cache check before proto-walk). See ATTACKS.md Category 30 andtest/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 onArray.prototype[N](or.joinoverride) observed bridge-internal state — no host reference reachable today, but one enrichment away from regressing into the GHSA-9qj6 RCE shape. Fixed inlib/setup-sandbox.jsby folding frames through a primitive string accumulator (noArray.prototypeslot reachable) and convertingmakeCallSiteGetterstolocalReflectDefinePropertyfor symmetry. See ATTACKS.md Category 28 Variant B andtest/ghsa/GHSA-q3fm-4wcw-g57x/. - GHSA-76w7-j9cq-rx2j — Promise species hijack in the
localPromiseswallow tail. The swallow-tailapply(globalPromisePrototypeThen, this, [...])call insidelocalPromise's constructor invoked the cached hostPromise.prototype.thenwithout first callingresetPromiseSpecies(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 hostFunctionconstructor via.constructor.constructor. One-line fix inlib/setup-sandbox.jsadds the missingresetPromiseSpecies(this)before the swallow-tail call, matching the pattern already used by the.then/.catch/Reflect.applyoverrides. See ATTACKS.md Category 31 andtest/ghsa/GHSA-76w7-j9cq-rx2j/. - GHSA-m4wx-m65x-ghrr — NodeVM constructor patch bypass of GHSA-8hg8-63c5-gwmx: a truthy
nestingpaired with anything other than a realrequireconfig object produced a NESTING_OVERRIDE-only resolver → inner NodeVM with attacker-chosenrequire→child_processRCE. Structural fix inlib/nodevm.js: destructure first, then reject at construction whenevernestingis truthy andrequireOptsis not a non-null object orResolver. Supersedes GHSA-8hg8-63c5-gwmx. See ATTACKS.md Category 25 andtest/ghsa/GHSA-m4wx-m65x-ghrr/. - GHSA-6j2x-vhqr-qr7q — sandbox escape via WebAssembly JSPI (Node 24 behind
--experimental-wasm-jspi, Node 26+ default).WebAssembly.promisingreturns Promise objects whose[[Prototype]]chain points directly at the host realm'sPromise.prototypewith no bridge proxy in between, sop.finally()reaches hostPromise.prototype.finally, V8'sSpeciesConstructorreads an attacker-controlledp.constructorgetter, 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 inlib/setup-sandbox.js: deleteWebAssembly.promisingandWebAssembly.Suspendingat sandbox bootstrap, mirroring the existingWebAssembly.JSTagremoval. Adds Defense Invariant #12 (no sandbox-visible object may have a host-realm prototype chain without bridge interposition). See ATTACKS.md Category 33 andtest/ghsa/GHSA-6j2x-vhqr-qr7q/. - GHSA-rp36-8xq3-r6c4 — NodeVM builtin denylist bypass via
processandinspector/promises. The exact-match denylist inlib/builtin.jsmissed two host-passthrough families:process(whosegetBuiltinModule(name)reloads any core module regardless of the embedder's allow/deny configuration) andinspector/promises(whoseSession().post('Runtime.evaluate', ...)evaluates attacker JS in the host realm). Structural fix promotes the check to family-prefix viaisDangerousBuiltin(key), strips thenode:URL prefix, and addsprocessto the dangerous set — enforced at bothBUILTIN_MODULESsource andaddDefaultBuiltin. Supersedes GHSA-947f-4v7f-x2v8. Adds Defense Invariant #13. See ATTACKS.md Category 21 (extended) andtest/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/-tlsexclusions were used — SSRF-class capability bypass (CVSS 8.6). Structural fix inlib/builtin.js:BUILTIN_MODULESfilter now excludes any name starting with_, so'*'expands only to documented public builtins; explicit opt-in,mock, andoverridepaths remain functional. See ATTACKS.md Category 34 andtest/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: HTTPIncomingMessageheaders (incl. auth tokens) viadiagnostics_channel.subscribe, embedderAsyncLocalStoragecontext viaasync_hooks.executionAsyncResource, embedderperformance.marklabels viaperf_hooks, and the full V8 heap viav8.getHeapSnapshot/v8.queryObjects. Fix inlib/builtin.js: extendsDANGEROUS_BUILTINSwith the four names, reusing the existing two-layer enforcement (BUILTIN_MODULESfilter +addDefaultBuiltinrejection, family-prefix andnode:-normalised viaisDangerousBuiltin).mock/overrideescape hatches preserved. See ATTACKS.md Category 35 andtest/ghsa/GHSA-9g8x-92q2-p28f/.
- If you constructed
NodeVM({ nesting: <truthy> })without an explicitrequireconfig object,new NodeVM(...)now throws (GHSA-m4wx-m65x-ghrr). This covers every shape that previously silently produced avm2-only resolver: omittingrequireentirely, 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 truthynestingvalue, not onlynesting: true(1/'yes'/{}/[]/function). Either dropnesting, or pass an explicitrequireconfig 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.
Patch release — no API changes.
- GHSA-248r-7h7q-cr24 — async generator
yield*-return thenable exception capture. Callingi.return(thenable)on an async generator delegating to a no-returninner iterator let V8'sPromiseResolveThenableJobcapture synchronous throws from the thenable's.thenand surface them to sandbox code as iterator results — bypassing both the transformer'scatchinstrumentation and theglobalPromise.prototype.thenrejection sanitiser. Two-layer defense on%AsyncGeneratorPrototype%.next/.return/.throwinlib/setup-sandbox.js: every iterator-result promise routes value and rejection throughhandleException, and every thenable argument is replaced with a sandbox-realm wrapper whose.thenis a fixedsafeThenthat sanitises sync throws and recursively re-wraps any nested thenable handed toresolve(...). WhensafeThenreadsvalue.thenand it is non-function, the wrapper always resolves with a{__proto__: null}shadow so V8's re-read of.thencannot observe attacker-controlled values — closing every counting/self-replacing-getter TOCTOU variant. Trade-off: identity is not preserved for non-thenable values passed toi.return(x). ATTACKS.md Category 29.
Three advisories closed. Patch release — no API changes.
- 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 leftglobalThis['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.getOwnPropertyNamesenumeration) 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 declareslet VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAILat 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 fromglobalThis's own-property table (so every computed-key probe returnsundefined). ThedefinePropertyinstall insetup-sandbox.jsis removed entirely; the bootstrap IIFE assigns into the outerletinstead. 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 switchedhandleExceptionandglobalPromise.prototype.thenonFulfilled to wrap caught/resolved values withbridge.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 orPromise.resolve-d by sandbox JS) produced a proxy whosesettrap 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 hostFunctionconstructor → RCE. Three callsites inlib/setup-sandbox.jsreverted toensureThis()semantics; the host-Promise rejection sanitizer composesfrom()outsidehandleExceptionso 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 sandboxArray.prototype. A sandbox-installed setter onArray.prototype[N]therefore captured the nextsaved[saved.length] = cwrite and exposedc.arr(a host-realm proxy) directly to attacker code, leading to hostFunctionextraction and RCE. Fixed inlib/bridge.jsby writing every saved-state entry throughthisReflectDefinePropertyso the appended slot is an own data property and noArray.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.
Single advisory closed plus prominent documentation of an existing escape hatch. Patch release — no API changes for valid configurations.
- GHSA-8hg8-63c5-gwmx —
nesting: truebypassedrequire: false, allowing sandbox-to-host RCE via inner NodeVM construction. The contradictory option pair{ nesting: true, require: false }now throwsVMErroratnew 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.
- New README section "
nesting: trueis an escape hatch" under Hardening recommendations. Explains thatnesting: truelets sandbox coderequire('vm2')and construct nested NodeVMs whoserequireconfig is chosen by the sandbox (not constrained by the outer config — by design of nesting). Do not enablenesting: truefor untrusted code. - JSDoc on the
nestingoption (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.
- If you set
{ nesting: true, require: false }anywhere in your codebase,new NodeVM(...)now throws. Either dropnesting: true(if you wanted deny-all), or replacerequire: falsewith an explicitrequireconfig (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 changenesting: truesemantics substantially).
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.
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.
- Custom
fsadapters withrequire.rootmust implementrealpathSync(orrealpath()on a fully customFileSystemclass). Without it,new NodeVM({require: {root, fs: customAdapter}})now throws aVMErrorat construction, citing GHSA-cp6g-6699-wx9c. The eager probe converts what was previously silent deny-by-default at every laterrequire()into a single, clearly-labelled construction-time error. Defaultfsusers are unaffected —DefaultFileSystemandVMFileSystemshiprealpath()out of the box. - Embedders running untrusted async code should install a host-side
unhandledRejectionhandler. The GHSA-hw58 fix closes synchronous executor throws but cannot reach async-function / async-generator /await usingrejection paths (V8 creates rejection promises via the realm's intrinsicPromise). 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 remainsInfinityfor backwards compatibility.
- GHSA-grj5-jjm8-h35p — Array species self-return sandbox escape. Bridge
applyandconstructtraps now neutralise host-arrayconstructorandSymbol.speciesbefore every host call (and restore in afinallyblock). Direct write,Object.assign, non-configurable defineProperty, and prototype-level constructor variants all blocked. - GHSA-v37h-5mfm-c47c — Handler reconstruction via
util.inspectleak. Three-layer defense: closure-scoped construction token,getHandlerObjectWeakMap guard, and.constructorsentinel rebind on every handler-class prototype (includingBufferHandler). - GHSA-qcp4-v2jj-fjx8 — Trap method on leaked handler with forged target. New
handlerToTargetWeakMap pairs every handler with its canonical proxy target at construction;validateHandlerTarget(this, target)at the entry of every trap method rejects forged-thisand forged-targetinvocations withVMError(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.
handleExceptionnow recurses intoAggregateError.errors[](in addition toSuppressedError.error/.suppressed); the bridge-levelapply-trap recognises calls to hostPromise.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
protectedHostObjectsWeakMap is populated at bridge init with every entry inglobalsList+errorsList(includingAggregateError) plus each prototype's.constructor. The four write traps (set,defineProperty,deleteProperty,preventExtensions) reject withVMError(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 fromBUILTIN_MODULES(closes'*'wildcard expansion) AND rejected inaddDefaultBuiltin(closes explicit-name +makeBuiltins(...)paths).mock/overrideescape hatches preserved. - GHSA-hw58-p9xv-2mjh — Promise executor unhandled rejection host-process DoS.
localPromiseconstructor wraps the user-supplied executor in try/catch (synchronous throws routed throughhandleExceptionand rejected as sandbox-realm values) and attaches a benign swallow tail to every sandbox-constructed Promise so the host'sunhandledRejectionevent never fires. Known residual: async function / async generator /await usingpaths 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-sideunhandledRejectionhandler. See README's Hardening recommendations. - GHSA-6785-pvv7-mvg7 — Unbounded
Buffer.alloc(N)host-heap DoS. NewbufferAllocLimitoption (defaultInfinity— fully backwards-compatible) caps single allocations onBuffer.alloc,Buffer.allocUnsafe,Buffer.allocUnsafeSlow, deprecatedBuffer(N), andnew 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 intotimeout. Forwarded fromNodeVMto its parentVMviasuper(options). - GHSA-mpf8-4hx2-7cjg — Host Promise
.then(onFulfilled)/ sanitiser-callback null-proto unwrapping. Sandbox-sideglobalPromise.prototype.thenonFulfilled and the bridge-level host-Promise sanitiser now usefrom()(always wraps) instead ofensureThis()(proto-fallthrough on null-proto host objects).handleExceptionalso switched tofrom()for symmetry on the rejection path. - GHSA-v27g-jcqj-v8rw —
CallSitehost-frame information disclosure viaprepareStackTrace.applyCallSiteGettersredacts every metadata getter (getFileName,getLineNumber,getColumnNumber,getFunctionName,getMethodName,getTypeName, etc.) for host frames;getEvalOriginredacts unconditionally because its return string can embed a host path.Error.prepareStackTraceis initialised todefaultSandboxPrepareStackTraceat 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 containingcatch,import,async,with, theINTERNAL_STATE_NAMEsubstring, or any\uescape (identifiers likeVM2_INTERNAL_STATE_…are valid JS and would slip past a literal-string check). - GHSA-cp6g-6699-wx9c — NodeVM
require.rootsymlink bypass (path-check / use TOCTOU). Lexical prefix check onpath.resolve()-resolved candidates was bypassed by symlinks inside the allowed root pointing outside it (Node's nativerequire()follows symlinks; CWE-59). Especially severe with pnpm / npm-workspaces /npm linklayouts where everynode_modulesentry is a symlink by design. Fixed by canonicalising candidate paths viafs.realpathSyncbefore the prefix check and canonicalisingrootPathsat construction time.DefaultFileSystemandVMFileSystemgain arealpath()method; ifrealpaththrows at runtime (missing file, broken link) the check denies by default. An eager FileSystem-contract probe atnew NodeVM(...)time throwsVMErrorimmediately ifrequire.rootis set and the adapter cannot dereference symlinks (missingrealpath()method, orVMFileSystemwrapping anfswithoutrealpathSync) — see Upgrade notes. ATTACKS.md Category 24.
bufferAllocLimit(VM, NodeVM) — non-negative number orInfinity. Caps individualBuffer.allocfamily requests from inside the sandbox. Default:Infinity. See README's "Hardening recommendations".
trace_eventshost-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 toDANGEROUS_BUILTINS.wasiadded to the denylist — experimental syscall surface (filesystempreopens, host clock/random, network) too broad for default'*'exposure.
- New README "Hardening recommendations" section covering
bufferAllocLimitusage,unhandledRejectionhandler shape (mitigates async-fn residual above),--max-old-space-sizecomplement, 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 qcp4validateHandlerTarget, the wp5r unicode-escape hardening, the GHSA-hw58 async-fn known residual, and the new cp6g symlink-bypass mitigation.
scripts/legacy-test-runner.jsnow supportsthis.skip()for runtime-conditional skipping and Promise-returning async tests (length-0async function () {}).