Oct 2, 2026·8 min read·3 visits
An optimization bug in the V8 engine on Node.js 26 fails to invalidate a promise lookup protector when properties are assigned sequentially, allowing attackers to bypass vm2's promise wrappers and achieve full sandbox escape and remote code execution.
A critical engine-level reachability failure in Node.js 26 running V8 14.6 allows attackers to escape the vm2 sandbox environment. When consecutive prototype properties are modified using sequential assignments, a V8 optimization bug fails to invalidate the PromiseThenLookupChain protector. By calling Promise.prototype.finally, the attacker bypasses the vm2 wrappers, hijacks the promise reaction using a custom constructor, triggers a calibrated stack overflow to capture a host-realm RangeError, and executes arbitrary shell commands on the host.
CVE-2026-92944 (also tracked via GitHub Security Advisory GHSA-27g9-p43v-cw3v) is a critical sandbox escape vulnerability affecting the vm2 Node.js library in versions 3.10.2 through 3.11.6. The flaw represents an engine-level reachability failure arising from a discrepancy between optimization assumptions in the V8 JavaScript engine and the prototype overriding patterns used by the sandbox. The vulnerability allows an attacker to execute arbitrary code outside the restricted environment on Node.js 26 runtimes running V8 14.6.
To prevent untrusted code from hijacking native objects, the vm2 sandbox implements wrapper functions over prototype methods. However, the introduction of a performance optimization in V8 14.6—named proto_assign_seq_opt—undermines this mechanism. Under specific conditions, sequential property modifications on prototypes fail to invalidate the engine's internal optimization state. This leaves a critical internal protector cell in a stale valid state, opening a bypass channel.
An attacker can exploit this stale state to skip the sandboxed wrapper for Promise.prototype.then during native promise operations. By chaining this bypass with a custom promise constructor that intercepts a native promise reaction, the attacker can trigger a stack overflow. This induces the host realm to construct and throw an error, which the attacker intercepts to extract reference handles to the host's global context, resulting in a complete sandbox escape.
The root cause lies in V8's optimization of Consecutive Prototype Property Assignments (proto_assign_seq_opt). When a series of properties are modified on an object's prototype, V8 attempts to consolidate these modifications. It groups consecutive assignments into a single bulk pathway named SetPrototypeProperties to reduce overhead and avoid repeatedly regenerating prototype maps.
However, the SetPrototypeProperties implementation in V8 14.6 contains a validation flaw. When modifying an existing data property on the prototype, the execution path flows through the SetDataProperty function. This branch fails to call the internal it.UpdateProtector() method. This omitted update is critical because it is the mechanism responsible for notifying the engine's optimizer that a protected prototype property has been overwritten.
V8 relies on a specific cell known as the PromiseThenLookupChain protector. When this protector is active, the engine assumes that no user-defined modifications have been made to Promise.prototype.then. This assumption enables internal functions, such as Promise.prototype.finally(), to use a fast path (InvokeThen) that directly executes the native implementation of then, bypassing regular prototype chain resolution.
Because the protector cell is never updated or invalidated during the sandbox initialization sequence, the V8 engine is left in an inconsistent state. The Promise.prototype.then property successfully points to the sandbox's custom wrapper, yet the optimization cell remains marked as valid. Consequently, when native code executes internal promise operations, V8 bypasses the property lookup, completely skipping the wrapper protection mechanism.
The original, vulnerable implementation of the sandbox setup in lib/setup-sandbox.js initialized the wrapper functions using standard JavaScript property assignments.
// Vulnerable assignment pattern
globalPromise.prototype.then = function then(onFulfilled, onRejected) { ... };
globalPromise.prototype.catch = function _catch(onRejected) { ... };Under normal circumstances, these consecutive assignments would trigger prototype transitions that invalidate any active lookup protectors. However, due to the proto_assign_seq_opt optimization bug in V8 14.6, these sequential assignments are merged. The modifications write the new function references to the prototype properties, but the engine fails to trigger the invalidation logic for PromiseThenLookupChain.
The patch resolved this vulnerability using a dual-layered approach. First, the assignment method was changed from standard assignment to explicit property definition via Reflect.defineProperty:
// Patched assignment pattern
localReflectDefineProperty(globalPromise.prototype, 'then', {
__proto__: null,
value: wrappedPromiseThen,
writable: true,
enumerable: false,
configurable: true,
});Using [[DefineOwnProperty]] semantics instead of standard assignments prevents V8 from grouping these changes under the optimized SetPrototypeProperties path. This forces V8 to execute the standard property definition routine, which correctly invalidates the PromiseThenLookupChain protector.
Second, the patch wraps the finally method itself to provide structural defense-in-depth:
const wrappedPromiseFinally = typeof globalPromiseFinally === 'function'
? function _finally(onFinally) {
resetPromiseSpecies(this);
return apply(globalPromiseFinally, this, [onFinally]);
}
: undefined;This second layer ensures that even if the optimizer protector were to fail in some other manner, calling finally() directly on a promise will execute resetPromiseSpecies(). This completely neutralizes custom species constructors prior to invoking the native finally logic.
To execute a successful sandbox escape, an attacker must carry out a multi-stage process that leverages the stale optimization cell to bypass wrappers, intercept a promise reaction, and force a cross-realm error. The execution must take place on Node.js 26 running V8 14.6. No specialized privileges or prior authentication are required.
First, the attacker creates a custom class (Evil) that acts as a promise species constructor. This class intercepts the executor function passed during a promise reaction. Within this constructor, the attacker intercepts the native promise rejection callback:
class Evil {
constructor(executor) {
executor(v => {}, hostErr => {
// Capture the raw host-realm RangeError
// err.constructor.constructor is the host Function constructor
const hostProcess = hostErr.constructor.constructor('return process')();
hostProcess.mainModule.require('child_process').execSync('id');
});
}
}Next, the attacker creates an engine-realm promise, typically by executing a basic asynchronous expression. The attacker then defines a custom constructor on this promise instance, mapping its Symbol.species property to the Evil class. Because the object is a standard promise, V8 expects it to follow the fast lookup route.
const p = (async () => 1)();
Object.defineProperty(p, 'constructor', {
value: { [Symbol.species]: Evil },
configurable: true
});Finally, the attacker calls p.finally(). Because the PromiseThenLookupChain protector cell remains valid, the V8 engine bypasses the vm2 wrapper for then and immediately invokes the native then method. This passes the promise reaction to the host-realm engine without stripping the species constructor. By recursively deep-nesting the execution inside the constructor, the attacker triggers a stack overflow. The resulting RangeError is instantiated in the host realm but caught by the sandboxed Evil constructor's rejection handler, allowing access to the host global scope.
The security impact of CVE-2026-92944 is critical, carrying a CVSS v3.1 base score of 9.8 and a CVSS v4.0 base score of 9.3. Successful exploitation permits full, unauthenticated remote code execution (RCE) on the host operating system running the sandbox environment. The attacker gains the security context of the parent Node.js process, which can lead to complete system compromise.
Because vm2 is frequently utilized to run untrusted, user-submitted code in multi-tenant SaaS environments, serverless computing platforms, and automated test runners, the fallout of a sandbox escape in these contexts is severe. An attacker can read sensitive environmental variables, access internal databases, modify local files, or pivot to make unauthorized network calls to other services in the internal infrastructure.
Although the statistical EPSS score is 0.00841 (reflecting a low likelihood of wide-scale opportunistic scans, as it requires a specific V8 version), targeted exploitation is highly reliable. In environments hosting untrusted JavaScript executables on Node.js 26, the exploit executes deterministically with zero user interaction.
The primary remediation strategy is upgrading the vm2 package to version 3.11.7 or higher. This version corrects the prototype assignment calls to ensure correct optimizer invalidation and introduces explicit wrappers around Promise.prototype.finally. These modifications completely close the escape vector on vulnerable V8 runtimes.
However, users must be aware that the vm2 library was officially deprecated in 2023. Pure JavaScript-based sandboxes that rely on prototype isolation are fundamentally fragile due to the constant introduction of performance-oriented optimizations in modern JavaScript engines. It is strongly recommended to migrate away from vm2 to process-level isolation frameworks, such as isolated-vm (which executes code in separate V8 isolates), or container-based runtimes.
If an immediate upgrade of vm2 is not possible, a runtime workaround exists. Administrators can launch the Node.js application with a specific command-line argument that disables the faulty V8 optimization:
node --no-proto-assign-seq-opt your-app.jsBy supplying the --no-proto-assign-seq-opt flag, the engine is forced to evaluate prototype assignments using the classic, unoptimized pathways. This guarantees that any changes to prototype methods immediately and correctly invalidate the PromiseThenLookupChain protector, maintaining the security boundary of the sandbox.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
vm2 patriksimek | >= 3.10.2, <= 3.11.6 | 3.11.7 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-693 |
| Attack Vector | Network |
| CVSS Score | 9.8 |
| EPSS Score | 0.00841 |
| Impact | Arbitrary Code Execution / Sandbox Escape |
| Exploit Status | PoC |
| KEV Status | No |
The product does not use or incorrectly implements a protection mechanism, which leaves it vulnerable to attacks.
A module allowlist bypass vulnerability (CVE-2026-92945 / GHSA-7q3f-wx44-378m) was identified in the vm2 sandboxing library prior to version 3.11.7. This flaw permits unauthenticated or untrusted code running within the sandbox environment to bypass explicit module restrictions. When the transitive resolution option is disabled, the system fails to validate file system path boundaries, allowing prefix-sharing sibling directories to be resolved and loaded, thereby escaping intended sandbox restrictions.
CVE-2026-92941 is a critical sandbox-escape and trust-manipulation vulnerability in the vm2 library (versions 3.11.3 to 3.11.6). This security flaw allows untrusted code executing within a NodeVM sandbox environment to compromise the global TLS trust store of the host Node.js process. By leveraging a design flaw where the host's native 'tls.setDefaultCACertificates' can be executed via a proxy wrapper, combined with a bridge unwrapping bypass in the 'url' module, an attacker can modify the process-wide default root Certificate Authorities. Consequently, all subsequent outbound TLS/HTTPS clients running on the host thread are forced to trust attacker-signed certificates, facilitating transparent Man-in-the-Middle (MitM) attacks. The vulnerability was resolved in version 3.11.7 of vm2 by introducing built-in member-level sanitization before applying read-only proxy wrappers.
A critical sandbox escape vulnerability in the vm2 library allows sandboxed JavaScript code to bypass containment and execute arbitrary native code on the host process. This occurs when the host's builtin crypto module is exposed to the NodeVM environment. Although vm2 implements a read-only proxy layer to restrict direct modifications to host properties, it does not prevent invocation of host-level functions. By calling the crypto.setEngine API with a path to a malicious native library on disk, an attacker can trigger OpenSSL's dynamic module loader. The host's operating system loader immediately runs the dynamic library's initializers/constructors before verifying engine compatibility, leading to remote code execution in the context of the host process.
CVE-2026-92938 is a critical sandbox escape vulnerability in the vm2 library (versions 3.11.3 through 3.11.6) that allows arbitrary native code execution on the host when the node:sqlite built-in module is loaded inside a sandboxed NodeVM environment.
CVE-2026-92937 is a critical sandbox escape vulnerability in the `vm2` Node.js library. Due to a logical failure in checking direct invocation targets inside the Proxy bridge, an attacker can register Promise callbacks using `Function.prototype.call` or `Function.prototype.apply` indirection. This bypasses the error sanitization wrappers, delivering raw host error objects directly to sandboxed callbacks and allowing the attacker to escape the sandbox and execute arbitrary shell commands on the host.
CVE-2026-92935 is a critical sandbox escape and remote code execution vulnerability in the vm2 library. By supplying an array or exotic object to the require property of NodeVM while nesting is enabled, attackers can bypass security checks, load the host vm2 module, and run arbitrary shell commands on the hosting server.