- GHSA-5h3f-q97h-ccvc — NodeVM custom-resolver authorization admitted prefix-sharing siblings: resolving an allowlisted package recorded a raw
^<path>prefix, so.../node_modules/fooauthorized.../node_modules/foo2/index.js(and the{module, path}shape authorized the whole search directory), running the sibling's top-level code in the host realm undercontext: 'host'. Structural fix inlib/resolver-compat.js: resolver answers are recorded as boundary-matched base paths (plus exact extension spellings), the object shape authorizes only the resolved package directory, and a failed load withdraws its authorization. Behavior change: a{module, path}answer whosemoduleis absolute, relative or contains..is now refused. See ATTACKS.md Category 46 andtest/ghsa/GHSA-5h3f-q97h-ccvc/. - GHSA-2v2p-6j97-cjg9 — a host promise delivered into the sandbox through a constructor return, a host getter or data property, or a callback argument carried no rejection reaction, so sandbox code that dropped it terminated the host process under Node's
unhandledRejectionpolicy (GHSA-gjq8-xm47-88rc covered onlyapplyreturns). Structural fix inlib/bridge.js: every host promise is marked handled once at the delivery chokepoint (prototype brand check, cross-realm aware), and theconstructtrap gains the same unconditional mark asapply. Behavior change: embedders no longer seeunhandledRejectionfor host promises handed to the sandbox on any route; debug rejections need an explicit.catch(). See ATTACKS.md Category 22 andtest/ghsa/GHSA-2v2p-6j97-cjg9/. - GHSA-489w-w794-jq94 — host memory disclosure and corruption: a host-allocated
Buffer(a builtin's return value such aszlib.deflateSync, an embedder-supplied buffer, a callback argument) exposed Node's shared 64 KiB pool through.buffer/.parent, letting the sandbox read and overwrite unrelated host buffers. Structural fix inlib/bridge.js: the GHSA-fcqc backing-store ownership rule now applies at the bridge for every host view, keyed on the delivered value's identity (so every alias of the store is covered), with the rawbuffer/parent/offsetgetters undeliverable and a fail-closed gate. Behavior change: for a host view that does not own its whole store,.buffer/.parentis a bounded copy (not identity-stable, not write-through),byteOffset/offsetread as0, sandbox-created sub-views lose.bufferaliasing with their parent, andSharedArrayBuffersub-views are delivered as copies; owning buffers are unchanged. See ATTACKS.md Category 41 andtest/ghsa/GHSA-489w-w794-jq94/.
- Single-file bundlers (Bun compile, esbuild, pkg, ...) can now ship vm2. The sandbox bootstrap files (
bridge.js,setup-sandbox.js,setup-node-sandbox.js,events.js) must reach the sandbox realm as source text and were read from disk at runtime withfs.readFileSync(\${__dirname}/...`), which a bundler cannot follow — a compiled binary failed withENOENTas soon as the package directory was not on disk. They are now embedded as string literals in the generatedlib/sources.js(npm run build:sources, regenerated bypretest/prepublishOnlyand guarded by a staleness test). The sandbox-compiled scripts use the fixed virtual filename/vm2/lib/` instead of the host install path, so bootstrap frames stay redacted from sandbox-visible stack traces.
- Dev dependency
@humanfs/nodebumped from 0.16.6 to 0.16.8.
- GHSA-6454-5x88-m6jw — sandbox-to-host RCE through an embedder-exposed host
Promise. Writingconstructor[Symbol.species]on the raw host promise and calling.then/.catch/.finallywith the settlement-direction handler omitted made V8 deliver the raw host settlement (e.g.process) to a sandbox-captured capability, with no callback slot for the rejection sanitizer to wrap. Structural fix inlib/bridge.js:neutralizeHostPromiseSpeciesOnshadows the host promise'sconstructoracross the call so the result capability is always a genuine host%Promise%, and the indirection peel now also covers hostReflect.applyand.finally. See ATTACKS.md Category 53 andtest/ghsa/GHSA-6454-5x88-m6jw/. - GHSA-j89j-5m6r-cr2q — sandbox escape to host RCE through any embedder-exposed sloppy-mode host function. Calling it with a nullish receiver (
greet(),.call(null),Reflect.apply(fn, undefined, []),bind(null)()) makes V8 bindthisto the host realm's global object, which the bridge then wrapped and delivered to the sandbox (greet().process.getBuiltinModule('child_process')). Structural fix inlib/bridge.js: the host global is cached at bridge init and refused at the three host→sandbox coercion chokepoints, returningundefinedso strict-function semantics are preserved. See ATTACKS.md Category 54 andtest/ghsa/GHSA-j89j-5m6r-cr2q/. - GHSA-x3v6-43hc-82mc — a NodeVM that allowlists the
cryptobuiltin 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, the same process-wide-mutator class ascrypto.setEngineandtls.setDefaultCACertificates. Fix inlib/builtin.js:sanitizeCryptoModulereplacessetFipswith a throwing stub alongsidesetEngine;getFips()and the rest ofcryptoare untouched. Configuration-integrity issue, not RCE. See ATTACKS.md Category 40 andtest/ghsa/GHSA-x3v6-43hc-82mc/. - GHSA-pq68-rvw4-xp4r — NodeVM's hard denylist omitted
child_process, sorequire: { builtin: ['*'] },['*', '-fs'], an explicit['child_process'], and thenode:spellings all handed the sandbox the real host module and oneexecSynccall was host RCE. Fix inlib/builtin.js:child_processjoinsDANGEROUS_BUILTINS, denied under the wildcard and on explicit request likecluster/worker_threads/node:test. Behavior change: embedders running trusted scripts that need it re-expose it throughrequire.mock(the real module or a narrower facade); a barebuiltin: ['child_process']no longer grants it. See ATTACKS.md Category 21 andtest/ghsa/GHSA-pq68-rvw4-xp4r/.
- Experimental Bun support (test suite and CI only). The suite now runs
under Bun, with engine-keyed assertion messages so patterns stay exactly as
strict on Node, a central
test/bun-skips.jslisting every JavaScriptCore divergence, and a non-blocking CI job whose output is verified complete before it is believed. Bun is not a supported security boundary — vm2's threat model is derived from V8 internals and JavaScriptCore has not been audited against the bridge. See the README Runtimes section.
global.Proxyinstall guarded for JavaScriptCore, and the sealed-slot attributes left implicit —lib/setup-sandbox.jsinstalls the handler-sanitisingProxywith a bare assignment to a slot theObject.defineProperties(global, ...)block above declares asundefined. What that write does is engine-specific: on Node >= 10 the slot is genuinely sealed and the write is a silent no-op, so the sandbox has noProxyat all; on Node 8 the old V8 global proxy leaves the slot writable and the write is live, which is what gives that sandbox itsProxy; and JavaScriptCore (Bun) implements the strict-mode write correctly and throws, aborting sandbox setup before the firstrun(). The write is now wrapped intry/catch— the failure outcome is "noProxyin the sandbox", which is strictly more restrictive, never less.test/vm.jsgains an AST-based guard (viaacorn, already a runtime dependency) that fails if any write toError,PromiseorProxyis left outside atryblock, including through a local alias, and pins the sealed-slot descriptors on Node >= 10 where the seal actually takes. Correction to 3.11.8's entry: that release described the assignment as dead code and removed it, and separately spelled outwritable: false, configurable: falseon the three sealed slots for readability, claiming no behaviour change. The second change was the damaging one — those attributes are the spec defaults, so the forms are equivalent on a current V8 but not on the Node 8 global proxy, where only the explicit form actually seals the slot. That silently removedProxyfrom Node 8 sandboxes and broke six tests. The attributes are omitted again, with a comment saying why they must stay that way, and the assignment is restored.
- GHSA-3vgf-8m4q-q4qr (dup: GHSA-59g5-pmg6-5gr4) — default
VMhost intrinsic prototype pollution of the binary-data and iterator families. The protected inventory omittedArrayBuffer/SharedArrayBuffer/DataView/ everyTypedArray/ the abstract%TypedArray%.prototype, and the array/string/map/set/regexp-string iterator prototypes plus the shared%IteratorPrototype%. BecauseBufferextendsUint8Array, the Category 20 proto-walk from a hostBufferreached those unprotected host prototypes andReflect.definePropertypolluted them globally, corrupting every host-realm typed array and iterator.lib/bridge.jsnow lists the binary-data globals inglobalsListand resolves the abstract intrinsic prototypes structurally intothisGlobalPrototypes, routing all of them intoprotectedHostObjects, the proto-mapping table, and the identity map so the write traps refuse sandboxset/defineProperty. See ATTACKS.md Category 20 (extended) andtest/ghsa/GHSA-3vgf-8m4q-q4qr/. - GHSA-88hf-g992-jg85 — NodeVM default-config (
console: 'inherit') sandbox escape. The sandbox extracted the raw hostObject.prototype.__proto__getter (viaBuffer.call.call(__lookupGetter__, …, '__proto__'), the GHSA-v6mx/cfcw primitive) and, because that getter was never classified dangerous like the setter, climbedconsole._stdout's host prototype chain to the non-intrinsicEventEmitter.prototype, overwroteemit, and had the host invoke it withthis === process→ RCE. Closed with two independent layers inlib/bridge.js: (1) the raw host proto-readers (__proto__getter,Object.getPrototypeOf,Reflect.getPrototypeOf) are denied delivery atthisFromOtherWithFactory/thisEnsureThis/thisFromOtherForThrowand the apply trap, so the sandbox can no longer climb host chains; (2) host[[Prototype]]objects are marked at delivery and sandbox function/accessor writes to them are diverted off the raw host object inBaseHandler.set/defineProperty. LegitimateObject.getPrototypeOfon host proxies and data/leaf writes are unchanged. See ATTACKS.md Category 50 andtest/ghsa/GHSA-88hf-g992-jg85/. - GHSA-f8gf-w286-fmq2 —
allowAsync: falseasync-execution boundary bypassed via Promise thenable assimilation. BlockingPromise.prototype.thenleft every native resolve capability open:Promise.resolve/all/race/any/allSettled/try,new Promise(r => r(thenable)),withResolvers().resolve,Array.fromAsync, and the realm-intrinsic base reached viaObject.getPrototypeOf(Promise)all let V8'sPromiseResolveThenableJobrun an attacker.thenin a microtask afterrun()returned, outside the configuredtimeout. Structural fix inlib/setup-sandbox.js, gated entirely toallowAsync: false: a TOCTOU-safe resolve-capability guard (refuses object/function values without ever reading.then), synchronous throws on the assimilating static methods, a non-configurable throwingArray.fromAsyncstub, and a construct-guard Proxy onlocalPromise's prototype that makes the native base un-constructable from the sandbox.allowAsync: trueis untouched. See ATTACKS.md Category 51 andtest/ghsa/GHSA-f8gf-w286-fmq2/. - GHSA-gjq8-xm47-88rc — an embedder-exposed host function (or a host builtin such as
events.once) that returns a rejected hostPromisecrashed the entire host process when sandbox code called it and ignored the result. The bridge handed the sandbox a wrapped promise but left the underlying host promise without a rejection reaction of its own, so Node's defaultunhandledRejectionpolicy (Node 15+) tore the process down — a sandbox-triggered host DoS from a single line of untrusted code. Sibling of the parent advisory GHSA-hw58-p9xv-2mjh, which hardened the opposite (sandbox→host) direction.lib/bridge.jsnow attaches a benign no-op reaction to the underlying host promise on the host side, at the apply-trap boundary (markHostPromiseHandled). Promises multicast, so the sandbox's own GHSA-55hx-sanitized.then/.catchstill fires and still observes the sanitized rejection; the no-op onRejected returnsundefined, so it never creates a new unhandled rejection; fulfilled promises are untouched. See ATTACKS.md Category 22 (extended) andtest/ghsa/GHSA-gjq8-xm47-88rc/. - GHSA-r273-hxvj-fxhp — NodeVM exposed host
utilto the sandbox as an unfilteredObject.assign({}, util), soutil.getCallSites()(Node >= 22.9) handed sandboxed code the host process call stack — absolute paths including vm2's ownlib/and the embedder entrypoint — bypassing the GHSA-v27g host-frame redaction, which only covers sandbox-realm Error stacks.getCallSite/setTraceSigInt/ private internals rode the same wholesale copy, and thesysalias leaked identically via the generic loader.lib/builtin.jsnow builds the exposedutilfrom a vetted, forward-safe allowlist (SAFE_UTIL_MEMBERS, presence-gated Node 8→26) routed through theBUILTIN_MEMBER_SANITIZERSchokepoint for bothutilandsys, so no unreviewed host member reaches the sandbox. Information disclosure only. See ATTACKS.md Category 52 andtest/ghsa/GHSA-r273-hxvj-fxhp/. - GHSA-x965-fc75-jpqh — incomplete-fix bypass of GHSA-m283-3h24-438v: a host-wrapped
AggregateError/SuppressedErrorrevisited within onehandleExceptiontraversal (self-cycleagg.errors=[agg], duplicate[shared,shared], mutual cycle) returned the raw host carrier from the cycle memo, re-embedding a live host proxy into the rebuilterrors[]→ host RCE on the caught-exception channel.lib/setup-sandbox.jsnow memoizes each carrier to exactly what a revisit must return (itself when sealed in place, its sandbox-realm replacement when rebuilt), builds host-wrapped aggregate/suppressed replacements in two phases so every cycle terminates on the replacement, memoizes thesanitizeHostOwnPropsrebuild, and adds a_blockHostWrappedbackstop at embed sites. See ATTACKS.md Category 49 andtest/ghsa/GHSA-x965-fc75-jpqh/.
- GHSA-27g9-p43v-cw3v — VM sandbox escape on Node 26 via a stale
PromiseThenLookupChainprotector. vm2 installed itsPromise.prototype.then/catchwrappers by plain assignment; on V8 14.6 that updates the data property without invalidating the protector, soPromise.prototype.finallytook an internalInvokeThenfast path straight to the original nativethenand vm2's wrapper — and itsresetPromiseSpecies— never ran. An attackerconstructor[Symbol.species]on an ordinary async-function Promise therefore survivedp.finally()and took control of a native reaction, which a calibrated stack overflow turned into a raw host-realmRangeError(e.constructor.constructor→ hostFunction→ hostprocess), reachable witheval: falseandwasm: false.lib/setup-sandbox.jsnow installs thethen/catchwrappers throughReflect.defineProperty(which does invalidate the protector) and wrapsPromise.prototype.finallyto runresetPromiseSpecies(this)before delegating to the cached native implementation. See ATTACKS.md Category 43 andtest/ghsa/GHSA-27g9-p43v-cw3v/. - GHSA-46pr-c5wc-xffx —
crypto.setEngine(path)handed a sandbox-supplied path to OpenSSL's ENGINE loader, and the OS dynamic loader ran the named library's constructor as native code before OpenSSL rejected the file — native RCE from a NodeVM allowing onlycrypto.lib/builtin.jsnow neutralizes the member before the read-only wrap, so no library is ever loaded; the rest ofcryptois untouched. See ATTACKS.md Category 40 andtest/ghsa/GHSA-46pr-c5wc-xffx/. - GHSA-633r-hq9m-c4ff —
vm.freeze()/vm.readonly()read-only bypass: a frozen host object's accessorsetwas still reachable from the sandbox viaObject.getOwnPropertyDescriptor(...).set,__lookupSetter__,Reflect.getOwnPropertyDescriptorandObject.getOwnPropertyDescriptors, letting sandbox code mutate host state through a view the embedder declared read-only. Fixed inlib/bridge.jsby strippingsetinReadOnlyHandler.getOwnPropertyDescriptorDescand routingdoPreventExtensionsdescriptors through that same hook; getter reads are preserved and non-frozen host objects are unaffected. See ATTACKS.md Category 44 andtest/ghsa/GHSA-633r-hq9m-c4ff/. - GHSA-647f-g98j-qq25 — patch bypass of the GHSA-m283-3h24-438v host-Promise rejection sanitizer, allowing sandbox-to-host RCE. The apply-trap gate identity-checked only the direct apply target, so registering an
onRejectedhandler throughFunction.prototype.call/.applyindirection skipped the capability-stripping rebuild and delivered the raw host rejection to sandbox code.lib/bridge.jsnow peelscall/applyindirection to the effective hostthen/catchand wraps the callbacks regardless of invocation shape, snapshotting.applyargument arrays getter-free and failing closed past a bounded peel depth. ATTACKS.md Category 39; tests intest/ghsa/GHSA-647f-g98j-qq25/. - GHSA-6rh5-qq4q-97xh — NodeVM builtin deny tokens did not cover subpath siblings:
fsandfs/promisesare separatebuiltinModulesentries, sobuiltin: ['*', '-fs']removed onlyfsand left the full hostfs/promisesAPI (host filesystem writes viawriteFile) exposed. The same gap affected every subpath family (-path→path/posix,-stream→stream/*,-timers→timers/promises). The'*'deny check inlib/builtin.jsnow routes throughisBuiltinDenied, which treats<family>/<sub>as denied whenever-<family>is present, in eithernode:spelling; undenied families keep their subpaths. See ATTACKS.md Category 21 andtest/ghsa/GHSA-6rh5-qq4q-97xh/. - GHSA-6w8r-xxw2-g3hx —
node:sqlite'sDatabaseSync(':memory:', { allowExtension: true }).loadExtension(path)loaded a native SQLite extension into the host process — native RCE from a NodeVM allowing only that builtin. The exposedDatabaseSyncnow forcesallowExtensionoff (for object- and function-typed options alike, since Node accepts a function there), so Node throwsERR_INVALID_STATEfrom bothloadExtension()andenableLoadExtension()while ordinary SQL keeps working.lib/setup-node-sandbox.jsadditionally rejects repeatednode:prefixes, closing therequire('node:node:sqlite')second spelling and making the canonicalrequire('node:sqlite')resolve. See ATTACKS.md Category 40 andtest/ghsa/GHSA-6w8r-xxw2-g3hx/. - GHSA-7q3f-wx44-378m — NodeVM external allowlist bypass: a prefix-sharing sibling package loaded as allowlisted.
LegacyResolver.isPathAllowedForModuleauthorized a require from an allowlisted module with a rawpath.startsWith(mod.path)test, so.../node_modules/foo2/index.jspassed for allowlisted.../node_modules/foo.lib/resolver-compat.jsnow requires a path boundary aftermod.path(exact match / trailing separator / next char a separator), sofoo2no longer matchesfoo. Scoped names (@scope/pkgvs@scope/pkg-evil) were affected identically and are covered. See ATTACKS.md Category 46 andtest/ghsa/GHSA-7q3f-wx44-378m/. - GHSA-8686-vhfx-7r3j — NodeVM's
builtin: ['*', '-node:child_process']deny token was a silent no-op: the wildcard expansion matched deny tokens by exact string, so thenode:-prefixed spelling never matched the canonicalchild_processname and the host module (RCE viaexecSync/spawn) stayed exposed. The'*'deny check inlib/builtin.jsnow matches both-${name}and-node:${name}, mirroring thenode:normalization the resolver already applies on the require side. See ATTACKS.md Category 21 andtest/ghsa/GHSA-8686-vhfx-7r3j/. - GHSA-8hr7-r645-pc6w — patch-bypass of the GHSA-m4wx-m65x-ghrr NodeVM nesting guard. The guard accepted any
typeof requireOpts === 'object'value as a realrequireconfig, so{ nesting: true, require: [] }(an array — and likewiseDate/RegExp/Map/boxed primitives) passed it, destructured to all-undefined, and produced aNESTING_OVERRIDE-only resolver that exposes hostvm2to the sandbox with no restriction → nestedNodeVM→child_process→ host RCE. The guard now accepts only aResolveror a plain config object (Object.prototype/null prototype, not an array) via a sharedisPlainConfigObjectpredicate, enforced at two layers: thelib/nodevm.jsconstructor throws for non-config shapes undernesting, andlib/resolver-compat.jsmakeResolverFromLegacyOptionsfail-closed strips the nesting override for any non-plainoptionsso no alternate caller can re-open it. The documented escape hatch ({ nesting: true, require: {} }/{ builtin: [...] }) is unchanged. See ATTACKS.md Category 25 andtest/ghsa/GHSA-8hr7-r645-pc6w/. - GHSA-98xx-8mx4-x7cm —
tls.setDefaultCACertificates()let sandbox code replace the host thread's process-wide default CA trust store, so subsequent host TLS clients accepted attacker-signed certificates. Argument-side defenses were insufficient (the required host array is forgeable throughURLSearchParams.getAll()), so the member itself is now neutralized inlib/builtin.js; the rest oftlsis unaffected. See ATTACKS.md Category 40 andtest/ghsa/GHSA-98xx-8mx4-x7cm/. - GHSA-c48m-32m9-vx93 — NodeVM
require.externalallowlist bypass when a customrequire.resolveis configured. The bare-specifier pre-check inLegacyResolver.customResolvematched by substring, soexternal: ['left-pad']also admittedevil-left-pad/left-pad-evil; anchoring that matcher then left a second route, since the permitted subpath tail accepted..segments (left-pad/../evil-package). Either way the resolver located an un-allowlisted host package and, under the defaultcontext: 'host', ran its top-level code in host context. Two composed layers inlib/resolver-compat.js: theexternalCachematcher is anchored to the whole specifier (wildcard segment semantics preserved), and any bare specifier carrying a..path segment is rejected before the custom resolver is consulted. See ATTACKS.md Category 45 andtest/ghsa/GHSA-c48m-32m9-vx93/. - GHSA-fcqc-726x-5wfc — sandbox read/write of host-realm memory through Node's shared small-buffer pool. Node serves small
Buffer.from(...)/Buffer.concat(...)/Buffer.of(...)allocations out of one shared 64 KiB backingArrayBuffer, and a pooled buffer's.buffergetter exposed that whole pool — soBuffer.from(Buffer.from([0]).buffer, 0, 65536)inside the sandbox could disclose and corrupt any host buffer (secrets, tokens, DB rows) sharing it.lib/setup-sandbox.jsnow enforces a backing-store ownership invariant (byteOffset === 0 && buffer.byteLength === length): every pooling factory (Buffer.fromnon-ArrayBuffer overloads,concat,of,copyBytesFrom, and the deprecatedBuffer(...)/new Buffer(...)forms) copies a pool-backed result into a standalone non-pooled buffer, while the documentedBuffer.from(arrayBuffer, byteOffset, length)sharing overload is preserved via a spoof-proof brand test. Independent ofbufferAllocLimit. See ATTACKS.md Category 41 andtest/ghsa/GHSA-fcqc-726x-5wfc/. - GHSA-h85j-hv3c-qfgq —
http.globalAgent/https.globalAgenthanded the sandbox the real shared host singleton, so a.on('free')listener received live host request options (includingAuthorizationheaders) and released sockets from unrelated host requests. The sandbox now sees a dedicatedAgent, and the module'srequest()/get()default to it soreq.agentcannot re-expose the host singleton; a caller-suppliedagentis preserved. See ATTACKS.md Category 40 andtest/ghsa/GHSA-h85j-hv3c-qfgq/. - GHSA-j3hm-6rg5-mchv (dup: GHSA-w9c4-gw9x-53mq) — sandboxed code under
require.externalcouldrequire('vm2')from disk and construct an unrestricted nestedNodeVM, defeating the guarantee that nesting is off by default and reachingchild_process.lib/resolver-compat.jsnow denies a sandboxrequire()of vm2's ownlib/directory and package main entry, matched by realpath so a symlinked candidate cannot dodge it;nesting: trueis unaffected, as it uses the builtin-override mechanism rather than a file require. Note thatrequire.externalwithoutrequire.rootstill host-requires any named path — the documented meaning of that option — and now warns once; tightening it to deny-by-default is a breaking change deferred to the next major. See ATTACKS.md Category 47 andtest/ghsa/GHSA-j3hm-6rg5-mchv/. - GHSA-jf8q-945g-9q4c — incomplete
nodejs.*symbol filtering let sandbox code corrupt host-visible WebStream state. The dangerous cross-realm symbol checks were fixed lists that omittednodejs.stream.disturbed/nodejs.stream.errored, so sandbox code could extract those real host symbols from aReadableStream.prototypereachable through the sandbox anddefinePropertythem onto a host stream, flippingstream.Readable.isDisturbed()/isErrored()host-side on an already-consumed stream.isDangerousSymbol(lib/setup-sandbox.js) andisDangerousCrossRealmSymbol(lib/bridge.js) now flag any registered symbol whoseSymbol.keyForis in the reservednodejs.namespace, covering the extraction filter, thegetOwnPropertyDescriptorsscrub and theset/defineProperty/deletePropertywrite traps, so the guard no longer goes stale as Node addsnodejs.*symbols; well-known symbols and benign registered symbols still cross. See ATTACKS.md Category 8 (extended) andtest/ghsa/GHSA-jf8q-945g-9q4c/. - GHSA-jxxv-8r27-vm4p — the shipped CLI (
npx vm2 ./script.js) ran the target script with no sandbox boundary.lib/cli.jsbuiltNodeVM.file(path, {require: {external: true}})with norequire.rootand the defaultrequire.context: 'host', so the script couldrequire(__filename)— or any absolute path — and execute it in the host realm with full host authority. The CLI now confines requires to the script's own directory (root: pa.dirname(script)) and loads them inside the sandbox (context: 'sandbox'). See ATTACKS.md Category 47 andtest/ghsa/GHSA-jxxv-8r27-vm4p/. - GHSA-qhwx-74w5-xhxq — NodeVM
builtin: ['node:test'](and['*']) exposed the real hostnode:test, whoserun({ execArgv: ['--eval=<js>'] })spawns a separate host Node process running attacker code with full host authority — RCE from a sandbox with no fs orchild_processof its own.lib/builtin.jsaddstesttoDANGEROUS_BUILTINS, so the family (includingnode:test/reporters) is dropped from wildcard expansion, refused on explicit request, and never enters the builtins map;isDangerousBuiltinnow strips repeatednode:prefixes sonode:node:testnormalizes too. See ATTACKS.md Category 21 andtest/ghsa/GHSA-qhwx-74w5-xhxq/. - GHSA-r4fx-v8hh-22mv — bypass of the documented
timeoutcontrol via aFinalizationRegistrycleanup callback.timeoutbounds only the synchronous body ofrun(); a cleanup callback is invoked by the garbage collector afterrun()has returned, so sandbox code inside it ran with no timeout accounting at all and could block the host event loop indefinitely — and unlike Promise continuations,allowAsync: falsedid not close it.lib/setup-sandbox.jsnow removesFinalizationRegistryandWeakReffrom the default sandbox globals, the way timers already are;NodeVMinherits the removal, and embedders who need them for trusted code can re-expose them through thesandboxoption. This restores thetimeoutguarantee for the default configuration; it is not a general DoS-prevention claim (see the README Hardening recommendations). See ATTACKS.md Category 42 andtest/ghsa/GHSA-r4fx-v8hh-22mv/. - GHSA-wjwh-qqvp-g4p4 (dup: GHSA-m3pp-qgq7-gwm6) — VM sandbox escape via
WebAssembly.compileStreaming/instantiateStreaming. On Node 26 both return a Promise whose[[Prototype]]chain reaches the host realm'sPromise.prototype, so vm2's sandbox-side Promise overrides andresetPromiseSpeciesnever run on it; an attackerconstructor[Symbol.species]plusp.finally()then delivered the raw host rejection into sandbox code (e.constructor.constructor("return globalThis")()→ hostprocess).lib/setup-sandbox.jsnow removes both streaming-compile APIs alongside the JSPI constructors (GHSA-6j2x-vhqr-qr7q); the non-streamingWebAssembly.compile/instantiateare unaffected. See ATTACKS.md Category 33 (extended) andtest/ghsa/GHSA-wjwh-qqvp-g4p4/. - GHSA-x6m4-chr9-cg97 — host filesystem path disclosure to sandbox code via host-realm error stacks, a patch bypass of GHSA-v27g-jcqj-v8rw. v27g redacts host frames only from stacks formatted in the sandbox realm, but a host-realm
Errorarrives with.stackalready formatted host-side — absolute paths,node:/internal/frames, vm2's ownlib/*.js, and the embedding application's source — and crossed to the sandbox verbatim; reachable with no special configuration viaeval('@@@ catch'), which makes vm2's host-side transformer throw, and generally via any embedder-exposed host function or host builtin that throws. Redacted at three chokepoints, each preserving the message and clean sandbox frames:lib/bridge.js(get+getOwnPropertyDescriptortraps, gated on a severance-robust host-error brand check, covering both the data-property and the Node 22+ accessor shape ofError#stack),lib/setup-sandbox.js(sanitizeHostOwnProps, covering the GHSA-m283 rebuild path that never crosses a bridge trap), andlib/vm.js(sandbox-destined compile errors truncated pre-bridge). Information disclosure only. See ATTACKS.md Category 48 andtest/ghsa/GHSA-x6m4-chr9-cg97/.
Five advisories closed. Patch release — no API changes for valid configurations.
- GHSA-cfcw-xp6x-25gj — stacked-indirection bypass of the GHSA-v6mx-mf47-r5wg apply-trap peel: the peel inspected one layer of
Function.prototype.{call,apply,bind}indirection, so two layers slipped the host prototype mutator past it, and a follow-up variant laundered the severance entirely host-side. Closed at two independent chokepoints —lib/bridge.jsrefuses to deliver host prototype mutators, and both it andhandleExceptionreject any host object whose prototype chain reachesnullwithout passing through the sandboxObject.prototype. See ATTACKS.md Category 37 andtest/ghsa/GHSA-cfcw-xp6x-25gj/. - GHSA-gmc2-2x9w-cgh9 —
bufferAllocLimit(GHSA-6785-pvv7-mvg7) bypass viaBuffer.concat(list, totalLength)andBuffer.from(arrayLike), whose host implementations reach the C++ allocator without traversing the sandbox-sideallocUnsafewrapper.lib/setup-sandbox.jsnow capsconcat,from, andcopyBytesFrom, and a fail-closed enumeration ofhost.Buffer's own keys turns any future uncapped allocator into an explicit error rather than a silent bypass. See ATTACKS.md Category 23 (extended) andtest/ghsa/GHSA-gmc2-2x9w-cgh9/. - GHSA-m283-3h24-438v — host errors leaked live host references into sandbox
catchblocks through four channels:Error.cause, theSuppressedError/AggregateErrorsub-error slots, arbitrary own properties (err.detail = process), and — beyond the reach of own-key enumeration — the carrier's own prototype chain.handleExceptionnow seals the spec-defined slots and rebuilds every host-wrapped carrier as a fresh sandbox-realm error carrying only its primitive properties, discarding the host prototype chain entirely. See ATTACKS.md Category 38 andtest/ghsa/GHSA-m283-3h24-438v/. - GHSA-m5w8-4gq2-6f8x — sibling of GHSA-9g8x: NodeVM
builtin: ['*']still surfacedosanddns, the last two process-wide builtins. Beyond host-identity and network-topology reads, both carry writes reachable in one line of sandbox code —dns.setServers()hijacks the host's DNS resolver,os.setPriority()renices the host process.lib/builtin.jsadds both toDANGEROUS_BUILTINS, coveringnode:spellings anddns/promisesautomatically. See ATTACKS.md Category 35 (extended) andtest/ghsa/GHSA-m5w8-4gq2-6f8x/. - GHSA-v836-6xw4-9cx3 —
bufferAllocLimitbypass viaArrayBuffer/SharedArrayBuffer/ TypedArray /WebAssembly.Memory, which reach the same synchronous, timeout-immune V8 backing-store allocator uncapped. When a finite limit is set,lib/setup-sandbox.jswraps each constructor with aconstructtrap capping the ToIndex-coerced byte count, and pinsprototype.constructorso the uncapped intrinsic cannot be recovered by a constructor walk. The defaultInfinityleaves them untouched. See ATTACKS.md Category 36 andtest/ghsa/GHSA-v836-6xw4-9cx3/.
- If you use
NodeVM({ require: { builtin: ['*'] } })and depend onosordns, those two builtins are now denied (GHSA-m5w8-4gq2-6f8x), together withnode:os,node:dns, anddns/promises. They join the process-wide class closed in 3.11.4: they expose host-process identity and network topology, anddns.setServers()/dns.setDefaultResultOrder()/os.setPriority()are outright writes to host-process state. Embedders needing a sandbox-local subset (typicallyos.platform(),os.EOL,os.constants) should register a controlled wrapper viarequire.mockorrequire.override. - If you set a finite
bufferAllocLimit, the cap now also coversBuffer.concat,Buffer.from,Buffer.copyBytesFrom, and theArrayBuffer/SharedArrayBuffer/ TypedArray /WebAssembly.Memoryconstructors (GHSA-gmc2-2x9w-cgh9, GHSA-v836-6xw4-9cx3). Sandbox code that previously allocated past the cap through those paths now gets the sameRangeError. The defaultbufferAllocLimit: Infinityleaves every one of them untouched, so this is a no-op unless you opted into the cap. - Errors thrown by embedder-exposed host functions now reach sandbox
catchblocks as sandbox-realm errors rather than proxies of the host error (GHSA-m283-3h24-438v). Primitive diagnostics (message,name,stack,code,errno,syscall,path, …) are preserved and error subclasses (TypeError,RangeError, …) now satisfyinstanceofcorrectly inside the sandbox; non-primitive properties, and anything reachable through the error's prototype chain, are gone by design.
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 () {}).