Releases: patriksimek/vm2
Release list
v3.11.8
Six advisories closed. Patch release — no API changes for valid configurations, with a few narrow observable behaviour changes (see Upgrade Notes).
What's Changed
Security fixes
- GHSA-3vgf-8m4q-q4qr (dup: GHSA-59g5-pmg6-5gr4) — the default
VM's protected-intrinsic inventory omitted the binary-data and iterator families (ArrayBuffer/SharedArrayBuffer/DataView/ everyTypedArrayand%TypedArray%.prototype, plus the array/string/map/set/regexp-string iterator prototypes and%IteratorPrototype%). BecauseBufferextendsUint8Array, a proto-walk from a hostBufferreached those unprotected host prototypes and polluted them globally — corrupting every host-realm typed array and iterator (host-state corruption). - GHSA-88hf-g992-jg85 — a NodeVM in its default
console: 'inherit'configuration was escapable: sandbox code extracted the raw hostObject.prototype.__proto__getter (never classified dangerous like the setter), climbedconsole._stdout's host prototype chain toEventEmitter.prototype, overwroteemit, and had the host invoke it withthis === process— full host RCE. - GHSA-f8gf-w286-fmq2 —
allowAsync: falsecould be bypassed through Promise thenable assimilation:Promise.resolve/all/race/any/allSettled/try,new Promise(r => r(thenable)),withResolvers().resolve,Array.fromAsync, and the realm-intrinsic Promise base all let V8'sPromiseResolveThenableJobrun an attacker.thenin a microtask afterrun()returned, outside the configuredtimeout. - GHSA-gjq8-xm47-88rc — an embedder-exposed host function (or a host builtin such as
events.once) returning a rejected hostPromisecrashed the entire host process when sandbox code called it and ignored the result: the underlying host promise had no rejection reaction, so Node's defaultunhandledRejectionpolicy tore the process down — a host DoS from one line of untrusted code. - GHSA-r273-hxvj-fxhp — NodeVM exposed host
utilas an unfilteredObject.assign({}, util), soutil.getCallSites()(Node ≥ 22.9) handed the sandbox the host process call stack — absolute paths including vm2's ownlib/and the embedder entrypoint,node:internalframes, function names and line numbers — bypassing the GHSA-v27g host-frame redaction, which only covers sandbox-realm error stacks.getCallSite/setTraceSigInt/ private internals rode the same wholesale copy; thesysalias leaked identically (information disclosure). - GHSA-x965-fc75-jpqh — patch bypass of GHSA-m283-3h24-438v: a host-wrapped
AggregateError/SuppressedErrorrevisited within a singlehandleExceptiontraversal (self-cycle,[shared, shared], mutual cycle) had its raw host proxy returned by the cycle memo and re-embedded into the rebuilterrors[], delivering a live host reference to sandboxcatchcode — host RCE on the exact channel Defense Invariant #3 promises to sanitize.
Documentation
docs/ATTACKS.mdgains Categories 49 (revisited error-carrier cycle memo), 50 (host prototype-chain climb via the raw__proto__getter), 51 (allowAsync: falsethenable assimilation) and 52 (hostutilmember auto-forwarding), plus extensions to Categories 20 (protected binary-data / iterator intrinsics) and 22 (ignored host-promise rejection), and matching rows in the "How The Bridge Defends" table.
Upgrade Notes
allowAsync: falseis now strict about thenable assimilation. UnderallowAsync: false, the assimilatingPromisestatic methods (resolve/all/race/any/allSettled/try) andArray.fromAsyncnow throw in the sandbox, and the native Promise base is not constructable from sandbox code. This closes the microtask-after-run()bypass.allowAsync: trueis completely unaffected — no valid async configuration changes.- The sandbox
utilbuiltin is now a vetted allowlist. Host-introspection / host-mutation members —util.getCallSites,util.getCallSite,util.setTraceSigInt, and private internals — are no longer forwarded to the sandbox (nor via the deprecatedsysalias). Documented util members (format,inspect,promisify,callbackify,types,TextEncoder,parseArgs, …) are unchanged. A member Node adds in future no longer auto-enters the sandbox. - Raw host prototype readers are denied delivery. The sandbox can no longer obtain the raw host
Object.prototype.__proto__getter,Object.getPrototypeOf, orReflect.getPrototypeOfto climb a host object's prototype chain. LegitimateObject.getPrototypeOfon a host proxy, and ordinary data/leaf property writes, are unchanged. - No other valid configurations are affected. The intrinsic-prototype protection (GHSA-3vgf), host-promise rejection handling (GHSA-gjq8), and error-sanitization (GHSA-x965) fixes are transparent to correct sandbox code.
Full Changelog: v3.11.7...v3.11.8
v3.11.7
Twenty advisories closed. Patch release — no API changes, but several observable behaviour changes; see Upgrade Notes.
What's Changed
Security fixes
- GHSA-647f-g98j-qq25 — Patch bypass of the GHSA-m283-3h24-438v host-Promise rejection sanitizer: the apply-trap gate identity-checked only the direct apply target, so registering
onRejectedthroughFunction.prototype.call/.applyindirection (including stacked and mixed forms) skipped the capability-stripping rebuild and delivered the raw host rejection to sandbox code (RCE). - GHSA-8hr7-r645-pc6w — Patch bypass of the GHSA-m4wx-m65x-ghrr nesting guard: the guard accepted any
typeof requireOpts === 'object'value, so{ nesting: true, require: [] }— and likewiseDate/RegExp/Map/ boxed primitives — destructured to all-undefinedand produced aNESTING_OVERRIDE-only resolver exposing hostvm2with no restriction → nested NodeVM →child_process(RCE). - GHSA-27g9-p43v-cw3v — Sandbox escape on Node 26 through a stale
PromiseThenLookupChainprotector: installing thethen/catchwrappers by plain assignment left the protector valid on V8 14.6, soPromise.prototype.finallytook an internal fast path to the original nativethenand an attackerconstructor[Symbol.species]survived to capture a native reaction — reachable witheval: falseandwasm: false(RCE). - GHSA-wjwh-qqvp-g4p4 —
WebAssembly.compileStreaming/instantiateStreamingreturned Promises whose prototype chain reached the host realm, so vm2's sandbox-side Promise overrides never ran on them; an attacker species plusfinally()delivered the raw host rejection into sandbox code (RCE). - GHSA-fcqc-726x-5wfc —
Buffer.from([0]).bufferhanded the sandbox Node's shared 64 KB allocation pool, which backs unrelated host allocations — sandbox code could read host secrets out of neighbouring buffers and corrupt them in place (host memory disclosure and corruption). - GHSA-46pr-c5wc-xffx —
crypto.setEngine(path)passed 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 validated the file — reachable from a NodeVM allowing onlycrypto(native RCE). - GHSA-6w8r-xxw2-g3hx —
node:sqlite'sDatabaseSync(':memory:', { allowExtension: true }).loadExtension(path)loaded a native SQLite extension into the host process, reachable from a NodeVM allowing only that builtin;require('node:node:sqlite')was additionally accepted as a second spelling (native RCE). - 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; the required host array was forgeable throughURLSearchParams.getAll()(host trust-store hijack). - GHSA-h85j-hv3c-qfgq —
http.globalAgent/https.globalAgentexposed the real shared host singleton, so a sandbox.on('free')listener received live host request options — includingAuthorizationheaders — and released sockets from unrelated host requests (host credential and traffic disclosure). - GHSA-qhwx-74w5-xhxq —
node:test'srun({ execArgv: ['--eval=<js>'] })spawned a separate host Node process running attacker code with full host authority, from a sandbox with no filesystem orchild_processof its own (RCE). - 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 canonical name and the host module stayed exposed (RCE). - GHSA-6rh5-qq4q-97xh — Builtin deny tokens did not cover subpath siblings:
builtin: ['*', '-fs']removed onlyfsand left the full hostfs/promisesAPI exposed. The same gap affected every subpath family (-path→path/posix,-stream→stream/*,-timers→timers/promises). - GHSA-c48m-32m9-vx93 — External allowlist bypass with a custom
require.resolve: the bare-specifier pre-check matched by substring, soexternal: ['left-pad']also admittedevil-left-pad; anchoring that left a second route, since the permitted subpath tail accepted..segments. Either way an un-allowlisted host package ran its top-level code in host context (RCE). - GHSA-7q3f-wx44-378m — External allowlist authorized requires with a raw
startsWithtest, so a prefix-sharing sibling package (.../node_modules/foo2for allowlistedfoo) loaded as if it were allowlisted. Scoped names (@scope/pkgvs@scope/pkg-evil) were affected identically. - 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(RCE). - GHSA-jxxv-8r27-vm4p — The shipped CLI (
npx vm2 ./script.js) ran the target with no effective boundary:NodeVM.filewas constructed with norequire.rootand the default host context, so the script couldrequire(__filename)— or any absolute path — and execute it in the host realm (RCE). - GHSA-jf8q-945g-9q4c — Incomplete
nodejs.*symbol filtering let sandbox code extract real hostnodejs.stream.disturbed/erroredsymbols anddefinePropertythem onto a host stream, flippingisDisturbed()/isErrored()host-side on an already-consumed stream (host state corruption). - GHSA-633r-hq9m-c4ff —
vm.freeze()/vm.readonly()bypass: a frozen host object's accessorsetwas still reachable viaObject.getOwnPropertyDescriptor(...).set,__lookupSetter__,Reflect.getOwnPropertyDescriptorandgetOwnPropertyDescriptors, letting sandbox code mutate state through a view the embedder declared read-only. - GHSA-r4fx-v8hh-22mv —
timeoutbypass viaFinalizationRegistrycleanup callbacks, which run outside the timeout's reach and block the host event loop for an unbounded period (host availability). - GHSA-x6m4-chr9-cg97 — Patch bypass of the GHSA-v27g-jcqj-v8rw host-path redaction: a host-realm
SyntaxErrorraised by the transformer had its stack formatted host-side, so the sandbox-realm redaction never ran ande.stackdisclosed absolute host paths — vm2's own files, Node internals, and the embedding application's source (information disclosure).
Documentation
docs/ATTACKS.mdextended with Categories 39–48, plus extensions to Categories 8, 21, 25 and 33 and matching rows in the "How The Bridge Defends" table.
Upgrade Notes
- CLI users —
npx vm2 ./script.jsnow confines requires to the target script's own directory (root: dirname(script)) and loads them inside the sandbox (context: 'sandbox'). A script that previously reached host modules through the CLI will no longer do so; run it withnodedirectly if that was the intent. builtin: ['*']users depending onnode:test— thetestfamily is now denied, including subpaths such asnode:test/reporters, and is refused on explicit request. Itsrun({ execArgv })spawns a host process, so it cannot be exposed safely; re-introduce a narrowed wrapper viarequire.mock/require.overrideif needed.- Builtin deny tokens are now stricter.
-fsalso deniesfs/promises(and equivalently for every subpath family), and-node:xnow matches the canonicalxspelling. If you relied on a subpath surviving its family's deny token, list it explicitly. FinalizationRegistryandWeakRefare no longer sandbox globals by default. Sandbox code referencing them now seesundefined. Re-expose via thesandboxoption only if you accept that GC callbacks are not bounded bytimeout.WebAssembly.compileStreaming/instantiateStreamingare removed from the sandbox. Non-streamingWebAssembly.compile/instantiate/Module/Memoryare unaffected.- Host error stacks reaching sandbox
catchblocks now have host frames redacted. The error message is preserved; absolute host paths, Node internals andfile:/wasm:frames are gone by design. Sandbox-realm frames still appear. vm.freeze()/vm.readonly()no longer expose accessor setters through any descriptor-reading path. Getter reads are preserved; non-frozen host objects are unaffected.require.externalwithoutrequire.rootnow emits a one-time warning. The breadth of that option is unchanged in this release — it still host-requires any named path — and tightening it to deny-by-default is a breaking change deferred to the next major. Setrequire.root, orcontext: 'sandbox', or both.NodeVM({ nesting: <truthy> })with an array-shaped or other non-configrequirenow throws at construction, alongside the shapes already rejected by GHSA-m4wx-m65x-ghrr. Pass a plain config object (e.g.require: { builtin: [] }) or aResolver.
Full Changelog: 3.11.6...v3.11.7
v3.11.6
Five advisories closed. Patch release — no API changes for valid configurations.
What's Changed
Security fixes
- GHSA-cfcw-xp6x-25gj — Stacked-indirection bypass of the GHSA-v6mx-mf47-r5wg apply-trap peel. Two layers of
Function.prototype.{call,apply,bind}pushed the host prototype mutator out of the inspected position; a later variant ran the__proto__setter entirely host-side, defeating both identity checks (RCE). - GHSA-gmc2-2x9w-cgh9 —
bufferAllocLimitbypass viaBuffer.concat(list, totalLength)andBuffer.from(arrayLike), which reach the host allocator without traversing the sandbox-side wrapper — one call allocates past a configured cap (host memory-exhaustion DoS). - GHSA-m283-3h24-438v — Host errors leaked live host references into sandbox
catchblocks viaError.cause, theSuppressedError/AggregateErrorslots, arbitrary own properties (err.detail = process), and the carrier's prototype chain. Any embedder function throwing an error that carried a host reference was an escape (RCE). - GHSA-m5w8-4gq2-6f8x — NodeVM
builtin: ['*']exposedosanddns, the last two process-wide builtins. Both leak host identity and network topology, and both write host state:dns.setServers()hijacks the host's DNS resolver,os.setPriority()renices the host process. - GHSA-v836-6xw4-9cx3 —
bufferAllocLimitbypass viaArrayBuffer,SharedArrayBuffer, every TypedArray constructor, andWebAssembly.Memory, which hit the same timeout-immune V8 allocator uncapped — a ~200-byte payload becomes gigabytes of host RSS (host memory-exhaustion DoS).
Upgrade Notes
builtin: ['*']users depending onosordns— both are now denied, along withnode:os,node:dnsanddns/promises. Re-introduce a safe subset viarequire.mock/require.override.- Finite
bufferAllocLimitusers — the cap now also coversBuffer.concat/from/copyBytesFromand theArrayBuffer/SharedArrayBuffer/ TypedArray /WebAssembly.Memoryconstructors. The defaultInfinityis unaffected. - Host errors now reach sandbox
catchblocks as sandbox-realm errors. Primitive diagnostics are preserved andinstanceofnow works for subclasses; non-primitive properties and the host prototype chain are gone by design.
Full Changelog: v3.11.5...3.11.6
v3.11.5
What's Changed
Bug fixes
- #566 — Restore
util.inspectoutput on Node 26+.console.log(vm.run(...))was rendering asProxy(Proxy({}))/Proxy(Proxy([]))instead of the underlying value. Triggered by Node 26's stricter handling of nested proxies in the inspector. - #567 — Restore array iteration on
vm.freeze()'d host arrays. Calling.map()/.filter()/.forEach()etc. inside the sandbox on a frozen host object containing arrays threw
TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target. Regression from the 3.11.0 proxy-invariant hardening. - #568 — Fix
.nodeextension handler key inlib/resolver.js(the key was' .node'with a leading space, so native addon resolution silently fell through to the default path). Thanks to @cherr-cc.
Upgrade Notes
Drop-in replacement for 3.11.4. No API or configuration changes.
Full Changelog: v3.11.4...v3.11.5
v3.11.4
Ten advisories closed. Patch release — no API changes for valid configurations.
What's Changed
Security fixes
- GHSA-c4cf-2hgv-2qv6 — Bridge
settrap ignoring ECMA-262 §9.5.9Receiver, lettingObject.create(hostObj)children andReflect.set(hostObj, k, v, custom)writes leak onto the host object (write-channel → RCE). - GHSA-m5q2-4fm3-vfqp — Cross-realm
Symbol.fornamespace leak + missing dangerous-symbol guards on the bridge's write traps (set/defineProperty/deleteProperty), enabling sandbox-installednodejs.util.promisify.custom/ stream brand / webstream hooks on host objects (RCE). - GHSA-v6mx-mf47-r5wg — Host prototype mutation via
Function.prototype.{call,apply,bind}andReflect.{apply,construct}indirection throughObject.prototype.__proto__setter, severing host intrinsic prototype chains and escaping viathisEnsureThisproto-walk fallthrough (RCE). - GHSA-q3fm-4wcw-g57x — Defense Invariant #11 violation in
defaultSandboxPrepareStackTrace(second variant of GHSA-9qj6-qjgg-37qq in a different file): sandbox-installedArray.prototype[N]setter /Array.prototype.joinoverride could observe bridge-internal stack-trace state. - GHSA-76w7-j9cq-rx2j — Promise species hijack in
localPromise's swallow-tail, hijacking the downstream child constructor to capture V8's internal(resolve, reject)capability and reach a raw host-realm error → hostFunction(RCE). - GHSA-m4wx-m65x-ghrr — NodeVM constructor patch bypass of GHSA-8hg8-63c5-gwmx: any truthy
nestingpaired with a non-real-configrequireproduced a NESTING_OVERRIDE-only resolver → inner NodeVM with attacker-chosenrequire→child_processRCE. - GHSA-6j2x-vhqr-qr7q — WebAssembly JSPI (
WebAssembly.promising/WebAssembly.Suspending, Node 24+ behind a flag, Node 26+ default) producing Promise objects with a host-realm[[Prototype]]chain and no bridge interposition; species hijack delivers a raw host-realm rejection to sandbox.catch→ hostFunction(RCE). - GHSA-rp36-8xq3-r6c4 — NodeVM builtin denylist bypass via
process(whosegetBuiltinModule(name)reloads any core module regardless of allow/deny config) andinspector/promises(whoseSession().post('Runtime.evaluate', ...)evaluates attacker JS in the host realm). Supersedes GHSA-947f-4v7f-x2v8. - GHSA-r9pm-gxmw-wv6p — NodeVM
builtin: ['*']wildcard exposing Node's undocumented underscored network builtins (_http_client,_http_server,_tls_*,_stream_*) even when the documented-http/-https/-net/-tlsexclusions were used — SSRF-class capability bypass (CVSS 8.6). - GHSA-9g8x-92q2-p28f — NodeVM builtin allowlist surfacing four process-wide observability builtins (
diagnostics_channel,async_hooks,perf_hooks,v8) that read state of the entire host process rather than sandbox-local state — HTTP header / async-context / perf-mark / heap-snapshot exfiltration.
Documentation
docs/ATTACKS.mdextended through Category 35, plus two new Defense Invariants: #12 ("No sandbox-visible object has a host-realm prototype chain without bridge interposition") and #13 ("The NodeVM builtin allowlist is a closed system").
Upgrade Notes
- 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: omittingrequire, 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. 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. - No other valid configurations are affected. Embedders who explicitly listed any of
process/inspector/worker_threads/cluster/vm/repl/module/trace_events/wasi/diagnostics_channel/async_hooks/perf_hooks/v8inbuiltinwere already running an unsandboxed sandbox; those names now throw at load time and can be re-introduced as safe wrappers viamock/override/SPECIAL_MODULES.
Full Changelog: v3.11.3...v3.11.4
v3.11.3
What's Changed
Security fix
- GHSA-248r-7h7q-cr24 — Async generator
yield*-return thenable exception capture (RCE)
Documentation
docs/ATTACKS.mdupdated through Category 29.
Full Changelog: v3.11.2...v3.11.3
v3.11.2
What's Changed
Security fixes
- GHSA-9vg3-4rfj-wgcm — Sandbox-realm null-proto write-through via
bridge.from()set trap (RCE) - GHSA-2cm2-m3w5-gp2f — Internal state reachable via computed-key access on
globalThis - GHSA-9qj6-qjgg-37qq — Bridge saved-state leak via sandbox-installed
Array.prototype[N]setter (RCE)
Documentation
docs/ATTACKS.mdupdated through Category 28, plus a new Defense Invariant
("Bridge-internal containers must not invoke sandbox code").
Full Changelog: v3.11.1...v3.11.2
v3.11.1
Single advisory closed plus prominent documentation of an existing escape hatch. Patch release — no API changes for valid configurations.
Embedders running untrusted code with nesting: true should read the new README section.
What's Changed
Security fix
- 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.
Documentation
- New README section "
nesting: trueis an escape hatch" under Hardening recommendations. Spells out the inner-VM independence: a nested VM'srequireconfig is chosen by the sandbox code that constructs it, not constrained by the outer VM. Do not enablenesting: truefor untrusted code. - JSDoc on the
nestingoption (lib/nodevm.js) upgraded to match. docs/ATTACKS.mdgains Category 25 documenting the configuration trap, plus 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 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).
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.
Full Changelog: v3.11.0...v3.11.1
v3.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 should upgrade. Several of the advisories close full sandbox-escape RCE primitives.
What's Changed
Security fixes
- GHSA-grj5-jjm8-h35p — Array species self-return sandbox escape (RCE)
- GHSA-v37h-5mfm-c47c — Handler reconstruction via
util.inspectleak (RCE) - GHSA-qcp4-v2jj-fjx8 — Trap method on leaked handler with forged target (RCE)
- GHSA-47x8-96vw-5wg6 — Cross-realm symbol extraction from host objects (RCE)
- GHSA-55hx-c926-fr95 — Promise structural-leak / SuppressedError / AggregateError sanitisation (RCE)
- GHSA-vwrp-x96c-mhwq — Host intrinsic prototype pollution via bridge write traps
- GHSA-947f-4v7f-x2v8 — NodeVM builtin allowlist bypass via host-passthrough builtins (RCE)
- GHSA-hw58-p9xv-2mjh — Promise executor unhandled rejection host-process DoS
- GHSA-6785-pvv7-mvg7 — Unbounded
Buffer.alloc(N)host-heap DoS - GHSA-mpf8-4hx2-7cjg — Host Promise
.then(onFulfilled)/ sanitiser-callback null-proto unwrapping (RCE) - GHSA-v27g-jcqj-v8rw —
CallSitehost-frame information disclosure viaprepareStackTrace - GHSA-wp5r-2gw5-m7q7 — Transformer fast-path bypass via
with/INTERNAL_STATE_NAME/ unicode-escape identifier (RCE) - GHSA-cp6g-6699-wx9c — NodeVM
require.rootsymlink bypass (RCE)
Plus two related hardenings discovered during pre-release red-team:
trace_eventsadded to the dangerous-builtins denylist —createTracing({categories: [Proxy<Array>]})triggered a C++ assertion that aborted the host process.wasiadded to the denylist — experimental syscall surface (filesystempreopens, host clock/random, network) too broad for default'*'exposure.
New options
bufferAllocLimit(VM, NodeVM) — non-negative number orInfinity. Caps individualBuffer.allocfamily requests from inside the sandbox. Default:Infinity(fully backwards-compatible). Embedders running untrusted code should opt into a finite cap as part of layered DoS defense.
FileSystem contract
DefaultFileSystem.realpath()andVMFileSystem.realpath()added.require.rootboundary checks now canonicalise candidate paths viarealpathbefore the prefix check, closing CWE-59 (symlinks inside the allowed root pointing outside it). Especially relevant for pnpm / npm-workspaces /npm linklayouts where everynode_modulesentry is a symlink by design.
Upgrade Notes
- 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. Defaultfsusers are unaffected. - 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 Hardening recommendations. - Embedders in memory-constrained environments should opt into a finite
bufferAllocLimit(e.g.32 * 1024 * 1024).
Documentation
- New README Hardening recommendations section.
docs/ATTACKS.mdupdated through Category 24, plus new Defense Invariants and Category Entry Format sections.
Full Changelog: v3.10.5...v3.11.0
v3.10.5
What's Changed
- fix: allow Object.setPrototypeOf on sandbox-local objects
- fix: block Function constructor access via getOwnPropertyDescriptor
- fix: block Function constructor from crossing bridge regardless of access path
- fix: block all code-executing constructors from crossing bridge via property descriptors
- fix: block Function constructor extraction via nested property descriptors
- fix: prevent proxy unwrapping to block Function constructor extraction via Object.entries
- fix: prevent sandbox escape via doPreventExtensions exposure in util.inspect
- fix: prevent sandbox escape via getFactory exposure in util.inspect
- fix: sanitize SuppressedError sub-errors to prevent sandbox escape
- fix: block host Function constructor leak via direct handler.get() call
- fix: block WebAssembly.JSTag to prevent wasm-level exception catch sandbox escape in Node 25
Full Changelog: v3.10.4...v3.10.5