Releases: patriksimek/vm2
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
v3.10.4
What's Changed
- fix: prevent sandbox escape via Promise static method stealing
- fix: prevent sandbox escape via Reflect.construct Promise species bypass
- fix: prevent sandbox escape via proxy handler exposure in util.inspect
- fix: prevent sandbox escape via fromOtherWithContext exposure in util.inspect
Full Changelog: v3.10.3...v3.10.4
v3.10.3
What's Changed
- fix: prevent sandbox escape via Symbol.for cross-realm symbols
- fix: prevent Symbol.for bypass via hasOwnProperty override
- fix: prevent Symbol.for bypass via object key coercion
- fix: prevent cross-realm symbol extraction via Object.getOwnPropertySymbols
- fix: prevent cross-realm symbol extraction via spread operator on bridge proxies
- fix: prevent sandbox escape via Promise species manipulation
- fix: prevent Symbol.species getter TOCTOU bypass in Promise species reset
- fix: eliminate Promise species TOCTOU by unconditional constructor override
- fix: prevent sandbox escape via Promise.try static method stealing
Full Changelog: v3.10.2...v3.10.3
v3.10.2
What's Changed
- fix: use Reflect.apply instead of .call() in Promise handlers by @patriksimek in #549
Full Changelog: v3.10.1...v3.10.2