vm2 NodeVM versions before 3.12.1 contain a sandbox escape vulnerability where the DANGEROUS_BUILTINS denylist omits child_process despite blocking other host-spawning modules. Attackers can require child_process and execute arbitrary commands on the host system when NodeVM is configured with builtin:[''] or explicit child_process allowance. This fork hardens NodeVM with a DANGEROUS_BUILTINS denylist that blocks host‑code‑reaching core modules even when the sandbox requests builtin:[''] or names them explicitly — the list contains …
NodeVM exposes the host util module to the sandbox through an unfiltered shallow copy (Object.assign({}, util)). On Node.js >= 22.9 this hands sandboxed code util.getCallSites(), a programmatic stack-introspection API that returns the host process's full call stack — absolute file paths, function names, and line numbers — including vm2 bridge internals and the embedding application's entrypoint. This bypasses the host-frame redaction established in GHSA-v27g-jcqj-v8rw, which only covers the Error.prepareStackTrace channel.
vm2's VM({ timeout }) option is documented and relied upon as the mechanism that bounds how long sandboxed code may execute. In the current implementation, the timeout only wraps the single synchronous call to VM#run() (via doWithTimeout → this._runScript(script) in lib/vm.js). It does not, and structurally cannot, bound code that the V8 engine itself schedules to run after that call has already returned. FinalizationRegistry and WeakRef are exposed to sandboxed …
Sandboxed code is able to disclose host memory used by small allocations by Buffer.from, Buffer.concat.
It being possible to obtain the host proto getter/setter, has been used in many reports: https://github.com/patriksimek/vm2/security/advisories/GHSA-vwrp-x96c-mhwq https://github.com/patriksimek/vm2/security/advisories/GHSA-v6mx-mf47-r5wg https://github.com/patriksimek/vm2/security/advisories/GHSA-grj5-jjm8-h35p https://github.com/patriksimek/vm2/security/advisories/GHSA-47x8-96vw-5wg6 Yet it was never patched… This can, still, be used to escape the sandbox, one example (I'm sure there's other ways as well), is via console._stdout/console._stderr (NodeVM with console: 'inherit', which is the default)
When an application explicitly exposes Node's zlib module through vm2's NodeVM builtin allowlist, an untrusted guest can obtain a pool-backed host Buffer from zlib.deflateSync, create a full-width view of its backing ArrayBuffer, read bytes outside the compressed result, and flip a byte in an unrelated host buffer. The pinned vm2 revision reproduces disclosure and host-memory modification, while a sandbox-local Buffer control remains exact-size and non-mutating.
At source revision 91034466bfb7f56b95fd48083ec6ca36d058f164 of vm2 3.11.8, an untrusted NodeVM guest can turn one allowlisted custom-resolved module into authorization for a separate file whose path merely shares the resolved path's string prefix. LegacyResolver.customResolve stores the resolved path in this.externals as ^<path> without an end or separator boundary; a later absolute require of a sibling such as foo2/index.js therefore passes the external check and is loaded through hostRequire when the configured …
NodeVM's require.external option lets sandboxed code require() local files and npm packages. When require.external is enabled and require.root is not explicitly set to a path that excludes node_modules, two defaults combine to fully defeat the sandbox: require.root defaults to unrestricted — "if omitted every path is allowed." require.context defaults to "host" — files loaded this way run through the real Node.js require(), not inside any vm2 sandbox. Sandboxed code can …
vm2 current head (v3.11.5, commit 7a1f5100b96f48d34e0fe104ab37c0acc5944f92) can still be used to terminate the host Node.js process when sandbox code calls a host-realm function that returns a rejected host Promise and then ignores the returned value. This is an incomplete-fix variant of the GHSA-hw58-p9xv-2mjh unhandled rejection hardening. The localPromise constructor now catches and consumes sandbox-created unhandled rejections, but host Promises returned across the bridge are not marked handled at the bridge …
An untrusted script run by VM.run can construct an embedder-exposed host function that returns a rejected native Promise and ignore the result. The sandbox-to-host construct trap forwards that Promise without applying the host-side rejection handling already used by the neighboring apply trap, so Node's strict unhandled-rejection policy terminates the host process.
vm2's current host-intrinsic prototype protection is incomplete. The fix for GHSA-vwrp-x96c-mhwq blocks sandbox writes into classic host intrinsics such as Object.prototype, Array.prototype, and Function.prototype, but current head still lets sandbox code in a default VM reach and mutate host Uint8Array.prototype, %TypedArray%.prototype, and ArrayBuffer.prototype. After VM.run() returns, normal host typed-array and ArrayBuffer objects observe attacker-controlled properties and methods installed by the sandbox.
When allowAsync is set to false, vm2 is expected to reject attempts to run asynchronous code. Direct use of Promise.prototype.then is blocked, but Promise static methods still assimilate attacker-controlled thenables. Promise.resolve(thenable), Promise.all([thenable]), Promise.race([thenable]), Promise.any([thenable]), and Promise.allSettled([thenable]) can invoke the thenable's then method in a microtask after VM.run() or NodeVM.run() has already returned. This bypasses the documented async-execution restriction and runs outside the configured timeout, allowing sandboxed code to continue executing …
There is a sandbox escape in vm2 3.11.5 / current HEAD when it is used on Node.js 26. The issue is reachable from a default new VM() sandbox. No NodeVM, require permission, host object injection, or intentionally unsafe configuration is required. The escape is a patch-bypass of the same security invariant addressed by GHSA-6j2x-vhqr-qr7q. The earlier fix removed the JSPI entry points WebAssembly.promising and WebAssembly.Suspending, because those APIs exposed a …
vm2 3.11.6 (this fork's latest release) contains an incomplete-fix bypass of the Error.cause host-reference sanitization added in GHSA-m283-3h24-438v (commit 7e3faaf). Sandbox code that catches a host-wrapped AggregateError which is revisited within a single handleException traversal (self-cycle, mutual-cycle, or the same host aggregate referenced twice in errors[]) receives a live, unsanitized host proxy inside the "sanitized" errors[], yielding full host RCE on the throw channel that the fix and Defense Invariant …
Attacker-controlled code can trigger a host-realm syntax error and read its stack through the vm2 bridge. Host-realm stack formatting bypasses the sandbox-side redaction, so the returned value exposes absolute paths from vm2, Node.js internals, and the embedding application. Default VM and NodeVM configurations are affected without requiring special options.
Untrusted JavaScript running inside new VM().run() / new NodeVM().run() can bypass vm.freeze() / vm.readonly() and mutate a host object the embedder explicitly marked read-only - the documented contract is "prevent sandboxed scripts from adding, changing, or deleting properties". If the frozen host object has an accessor (get/set) own-property, the sandbox can read the host setter back out via Object.getOwnPropertyDescriptor() and call it directly; the call lands in BaseHandler.apply which unwraps …
NodeVM normalizes node:-prefixed builtin specifiers during require() resolution, but it does not normalize user-provided negative builtin entries in wildcard policy. As a result, this configuration: new NodeVM({ require: { builtin: ['*', '-node:child_process'] } }); does not deny the canonical child_process builtin. Sandboxed code can require both child_process and node:child_process, and receives the host module with process-spawning APIs such as execSync and spawn. The safe proof below only checks module and …
The NodeVM constructor computes hasRealRequireConfig using typeof requireOpts === 'object' && requireOpts !== null, so require: [] bypasses the guard intended to reject nesting without an explicit require configuration. makeResolverFromLegacyOptions() then destructures the array to undefined option fields and returns a resolver containing only NESTING_OVERRIDE.vm2. Any attacker whose JavaScript is executed by a downstream NodeVM configured with {nesting: true, require: []} can load the host vm2 module, create an inner …
NodeVM's builtin wildcard policy can allow sandboxed code to access fs/promises even when the embedder denies fs. With the following configuration: require: { builtin: ['*', '-fs', '-child_process'] } require('fs') and require('child_process') are blocked, but require('fs/promises') and require('node:fs/promises') are still available. This allows sandboxed code to create and write files on the host filesystem through the promise-based filesystem API.
On Node.js 24 and newer, vm2 can expose the host node:test module to sandboxed NodeVM code when the embedder explicitly allows the node:test builtin. Sandbox code can reach that module through require('node:node:test') and call run() with attacker-controlled execArgv. node:test.run() starts a separate Node process for process-isolated test execution and forwards the supplied execArgv values to that process. Supplying –eval=<JavaScript> therefore executes arbitrary JavaScript in an unrestricted host Node process, outside …
vm2 current head (v3.11.5, commit 7a1f5100b96f48d34e0fe104ab37c0acc5944f92) still exposes registered Node.js internal symbols from host WebStream prototypes to sandbox code. The prior nodejs.* symbol hardening blocks Symbol.for('nodejs.<name>') at the source, but the extraction filters and bridge write traps still enumerate a fixed set of known registered symbols. On Node.js v25.8.0, stream/web exposes two additional registered symbols: nodejs.stream.disturbed nodejs.stream.errored Sandbox code can extract those real host symbols with Object.getOwnPropertySymbols(streamWeb.ReadableStream.prototype) and then use …
Untrusted JavaScript run by vm2 can escape the sandbox and execute arbitrary commands in the host Node.js process when an embedder-exposed host Promise rejects. This is an incomplete fix for GHSA-m283-3h24-438v: the advisory's capability-bearing rejection rebuild runs only through this direct Promise-handler path, so call/apply indirection bypasses the protection it introduced. The bridge sanitises host rejection values before sandbox callbacks run, but the gate at lib/bridge.js:1624 identity-checks only the direct …
isPathAllowedForModule decides whether a resolved path belongs to an allowlisted external module using a raw string prefix test. node_modules/foo2 starts with node_modules/foo, so a package whose name merely shares a prefix with an allowlisted one is treated as being inside it, and a relative require from the allowlisted package reaches it even with transitive loading disabled.
The latest published vm2 release, 3.11.5, and the current main branch are vulnerable to a sandbox escape when used on Node.js 26. An ordinary fulfilled Promise created by an async function can retain an attacker-controlled constructor[Symbol.species] across Promise.prototype.finally(). vm2 installs wrappers on the intrinsic Promise.prototype.then and catch methods. On Node.js 26 / V8 14.6, V8's SetPrototypeProperties path updates these existing data properties without invalidating the PromiseThenLookupChain protector. Promise.prototype.finally() subsequently trusts …
Summary vm2 3.11.6 exposes the host tls module to a NodeVM when that builtin is explicitly allowed. Although the module object is wrapped as read-only, its functions still execute against process-wide host state. On Node.js versions that provide tls.setDefaultCACertificates(), sandbox code can replace the certificate authorities trusted by subsequent host-realm TLS clients. The exploit needs only the narrowly allowed tls and url builtins. It does not require fs, process, module, …
Summary vm2 3.11.6 exposes the host process's real https.globalAgent when a NodeVM is explicitly allowed to require https. The module is wrapped as read-only, but calls to methods on the shared agent still mutate the host object. Sandbox code can register a free listener and receive host request options and the host TLS socket whenever an unrelated host HTTPS request releases a pooled connection. In a contained test, sandbox code …
vm2 is a sandbox library for isolating and executing untrusted JavaScript code inside a Node.js process. It can restrict access to built-in modules and external packages. When NodeVM enables an external allowlist together with a custom resolve callback, vm2 checks the requested package name with a non-exact match. For example, if the allowlist only permits left-pad, an attacker can still bypass the check with a colliding package name such as …
Summary vm2 3.11.6 exposes the host crypto module to a NodeVM when that single builtin is allowed. The module is presented through a read-only bridge, but its functions still execute with host-process authority. crypto.setEngine() accepts a filesystem path and asks OpenSSL to dynamically load the referenced native library. An attacker whose untrusted plugin package contains a native library can therefore load that library into the host process by calling crypto.setEngine() …
The vm2 command-line tool installed by npm install -g vm2 and documented in the README's "CLI" section runs the supplied script under NodeVM with require:{external:true} and no root / context / builtin configured. With these defaults the resolver loads every relative or absolute require() target through the host require() function, executing the attacker's module body in the host Node.js process before the result is ever proxied back into the sandbox. …
vm2 3.11.6 exposes Node.js's host node:sqlite module to NodeVM code when that builtin is allowed explicitly or through builtin: ['*']. The module is wrapped as read-only, but callable methods retain host-process authority. A sandboxed plugin can construct an in-memory database with extension loading enabled and call DatabaseSync.loadExtension() on a native library bundled in the plugin directory. SQLite loads the library into the Node.js host process and invokes its native extension …
vm2 bufferAllocLimit cap bypassed by Buffer.concat and Buffer.from arrayLike The bufferAllocLimit option introduced in 3.11.0 (GHSA-6785-pvv7-mvg7) caps host-side Buffer allocations driven by sandbox code, the way embedders opt into timeout. The cap wraps Buffer.alloc, Buffer.allocUnsafe, Buffer.allocUnsafeSlow, and the deprecated Buffer(N) / new Buffer(N) forms. Two other API paths reach the same host C++ allocator with an attacker-controlled size and are not capped: Buffer.concat(list, totalLength) and Buffer.from(arrayLike) with a fake length. …
VM2 suffers from a sandbox breakout vulnerability. This allows attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
GHSA-9g8x-92q2-p28f closed the "process-wide observability builtins" class by adding diagnostics_channel, async_hooks, perf_hooks, and v8 to DANGEROUS_BUILTINS in lib/builtin.js. The fix's rationale (in the commit message and docs/ATTACKS.md Category 35) is general: Process-wide observability builtins. Unlike most Node builtins, these expose state of the entire host process rather than sandbox-local state — the vm2 boundary cannot usefully contain them because the data they surface […] belongs to the embedder. Even a …
I found that handleException() in lib/setup-sandbox.js recursively sanitizes sub-errors for SuppressedError and AggregateError, but completely ignores the ES2022 Error.cause property. When sandbox code catches a host-thrown error carrying a .cause that references a host object like process, it can traverse that reference to achieve arbitrary command execution on the host. The project's own docs/ATTACKS.md (Defense Invariant #3, line 54) explicitly claims Error.cause is sanitized. The implementation does not match this …
The bufferAllocLimit defense (GHSA-6785-pvv7-mvg7) can be completely bypassed using ArrayBuffer, SharedArrayBuffer, or any TypedArray constructor. These allocate identical host-process RSS through the same V8/libuv C++ allocation path as Buffer.alloc but are not subject to the size cap.
The BaseHandler.set trap in bridge.js (line 1231) ignores the receiver parameter and unconditionally writes to the host target object. Per the Proxy set trap specification, when receiver !== proxy (e.g., when a child object inherits from the proxy via Object.create), the property assignment should create an own property on the receiver, not on the proxy target. The current implementation always calls otherReflectSet(object, key, value) against the host target, causing all …
defaultSandboxPrepareStackTrace in lib/setup-sandbox.js (lines 605, 607) appends to a fresh sandbox-realm lines = [] via lines[lines.length] = value. This is the exact invariant-violating pattern that GHSA-9qj6-qjgg-37qq (commit ca195f0, 2026-05-01) just patched in neutralizeArraySpeciesBatch and codified as Defense Invariant #11 ("Bridge-internal containers must not invoke sandbox code"). A sandbox-installed Array.prototype[N] setter fires during the bridge's safe-default stack-trace formatting and observes / intercepts each appended line.
A sandbox escape vulnerability in vm2 allows arbitrary code execution in the host process when untrusted code is executed with async support on runtimes exposing WebAssembly JSPI (WebAssembly.promising / WebAssembly.Suspending). In the tested configuration, a JSPI-backed Promise can reach Promise.prototype.finally() in a way that bypasses the expected Promise-species hardening and exposes a host-originated rejection object to attacker-controlled species logic, breaking the sandbox boundary. This is a critical sandbox escape: any …
VM2 suffers from a sandbox breakout vulnerability. This allows attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
vm2 3.11.2 Symbol.for override in setup-sandbox.js only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's set/defineProperty/deleteProperty traps having no isDangerousCrossRealmSymbol key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full util.promisify hijack chain.
By combining Buffer.call.call({}.lookupGetter, Buffer, "proto"), Buffer.call.call({}.lookupSetter, Buffer, "proto"), and Node.js's ERR_INVALID_ARG_TYPE Error, the host's TypeError constructor can be obtained, which allows the escape from the sandbox. This allows attackers to run arbitrary code.
The fix for GHSA-8hg8-63c5-gwmx (CVE-2023-37903) introduced a check in nodevm.js line 263 that blocks the combination nesting: true + require: false. However, the check uses strict equality (options.require === false), which is trivially bypassed by omitting the require option entirely. When require is not specified, options.require is undefined, not false. The strict equality check fails, so the security guard is skipped. Immediately after (line 280), the destructuring default require: requireOpts …
NodeVM exposes some process-wide observability builtins when they are allowed through require.builtin. The following builtins are not blocked by the dangerous builtin denylist: diagnostics_channel async_hooks perf_hooks These modules are process-wide, not sandbox-local. Sandboxed code can use them to observe host application data across the vm2 boundary. Note: It is a host data exposure issue. The impact depends on whether the host application allows these builtins and uses HTTP, async request …
NodeVM supports excluding public network builtins from the wildcard builtin option. With this configuration direct access to http, https, http2, net, dgram, tls, dns, and dns/promises is blocked. However, Node.js also exposes underscored internal HTTP builtins such as _http_client and _http_server. These are not blocked when the public modules are excluded. Sandboxed code can use these internal builtins to make outbound HTTP requests and open listening HTTP sockets even though …
NodeVM blocks several dangerous Node.js builtins such as module, worker_threads, cluster, vm, repl, and inspector. However, the denylist misses process and inspector/promises. Both can be used from sandboxed code to reach host-side execution primitives. This allows sandboxed code to bypass the intended builtin restrictions and execute code in the host process.
VM2 suffers from a sandbox breakout vulnerability. This allows attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
VM2 suffers from a sandbox breakout vulnerability. This allows attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
VM2 suffers from a sandbox breakout vulnerability. This allows attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
https://github.com/patriksimek/vm2/security/advisories/GHSA-wp5r-2gw5-m7q7 is not fully patched.
vm2's code transformer has a performance optimization that skips AST analysis when the code does not contain catch, import, or async keywords. This fast-path bypass allows sandboxed code to directly access the internal VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL variable, which exposes internal security functions (handleException, wrapWith, import).
vm2's bridge exposes mutable proxies for real host-realm intrinsic prototypes and then forwards sandbox writes into the underlying host objects with otherReflectSet() and otherReflectDefineProperty(), which lets attacker-controlled JavaScript running in a default VM or inherited NodeVM mutate shared host Object.prototype, Array.prototype, and Function.prototype from inside the sandbox.
Sandboxed code can call Buffer.alloc() with an arbitrary size to allocate memory directly on the host heap. Because Buffer.alloc is a synchronous C++ native call, vm2's timeout option cannot interrupt it. A single request can exhaust host memory and crash the process with a FATAL ERROR: Reached heap limit.
When a NodeVM is created with nesting: true, sandbox code can unconditionally require('vm2') regardless of the outer VM's require configuration — including require: false. With access to vm2, the sandbox constructs a new inner NodeVM with its own unrestricted require settings and executes arbitrary OS commands on the host. Any application that runs untrusted code inside a NodeVM with nesting: true is fully compromised.
vm2's CallSite wrapper class (intended as a safe wrapper for V8's native CallSite) blocks getThis() and getFunction() to prevent host object leakage, but allows getFileName() to return unsanitized host absolute paths. Any sandboxed code can extract the full directory structure, library paths, and framework versions of the host server.
A sandbox boundary violation in vm2 allows host object identity to cross into the sandbox through host Promise resolution. When a host-side Promise that resolves to a host object is exposed to the sandbox, the value delivered to the sandbox .then() callback preserves host identity. This allows the sandbox to interact with the host object directly, including: Performing identity checks using host-side WeakMap Mutating host object state from inside the …
It is possible to reach BaseHandler.getPrototypeOf, which can be used to get arbitrary prototypes
A sandbox escape vulnerability in vm2 v3.10.5 allows any sandboxed code to crash the host Node.js process via a single Promise constructor that triggers an unhandled rejection propagating to the host. The fix for CVE-2026-22709 (v3.10.2) only sanitized the onRejected callback in .then() and .catch() overrides and did not address the executor-to-unhandledRejection path.
NodeVM's require.root path restriction can be bypassed using filesystem symlinks, allowing sandboxed code to load modules from outside the allowed root directory in host context. Because path validation uses path.resolve() (which does not dereference symlinks) but module loading uses Node's native require() (which does), an attacker can load arbitrary host-realm modules and achieve remote code execution.
NodeVM's builtin allowlist can be bypassed when the module builtin is allowed (including via the '*' wildcard). The module builtin exposes Node's Module._load(), which loads any module by name directly in the host context, completely bypassing vm2's builtin restriction. This allows sandboxed code to load excluded builtins like child_process and achieve remote code execution.
It is possible to obtain the host Object, https://github.com/patriksimek/vm2/commit/ebcfe94ad2f864f0bc35e78cff1d921107cfd160 added some protections, but the implementation is incomplete.
The fix for https://github.com/patriksimek/vm2/security/advisories/GHSA-cchq-frgv-rjh5 is insufficient and can be circumvented allowing attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
VM2 suffers from a sandbox breakout vulnerability through the inspect function. This allows attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
Full sandbox escape with arbitrary code execution. Attacker code inside VM.run() obtains host process object and runs host commands with zero host cooperation.
In vm2 v3.10.4 on Node.js v24.13.0, SuppressedError allows attackers to escape the sandbox and run arbitrary code.
VM2 suffers from a sandbox breakout vulnerability. This allows attackers to write code which can escape from the VM2 sandbox and execute arbitrary commands on the host system.
In vm2 for version 3.10.0, Promise.prototype.then Promise.prototype.catch callback sanitization can be bypassed. This allows attackers to escape the sandbox and run arbitrary code. const { VM } = require("vm2"); const code = ` const error = new Error(); error.name = Symbol(); const f = async () => error.stack; const promise = f(); promise.catch(e => { const Error = e.constructor; const Function = Error.constructor; const f = new Function( "process.mainModule.require('child_process').execSync('echo HELLO …