Security controls bypass or absence In vm2
Description
vm2 contains a sandbox escape vulnerability 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....
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Component | Affected version | Patched versions |
|---|---|---|---|
npm | 3.12.1 |
Aliases
References