# vm2: NodeVM `require.external` without an explicit `require.root` grants unrestricted host filesystem access and full RCE

vm2's require.external with default unrestricted root and host context lets sandboxed code require node_modules/vm2, build an unrestricted NodeVM, and run host commands.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: abisheikM1, amagesh1, Den1al
- Disclosed: 2026-10-05
- CVE: CVE-2026-92946
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-j3hm-6rg5-mchv
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-j3hm-6rg5-mchv
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:misconfiguration vuln:rce vuln:sandbox-escape

**Target:** NodeVM sandbox require resolver (lib/resolver-compat.js)
**Root cause:** CustomResolver.isPathAllowed returns true when rootPaths is undefined, and context defaults to 'host' so loadJS calls the real host require(); no external module-name allowlist exists for bare external:true.
**Steps:**
1. Configure NodeVM with `require: { external: true, root: './' }` (README Quick Examples).
2. Compute vm2's own entry path relative to cwd via `require.resolve('vm2')`.
3. Inside vm.run, `require()` that path to get the real unwrapped NodeVM class.
4. Instantiate `new real.NodeVM({ require: { builtin: ['child_process'], external: false } })`.
5. Run `require('child_process').execSync('whoami')` in the inner VM to execute host commands.
**How found:** Code review of resolver-compat.js defaults: noticed rootPaths undefined short-circuits isPathAllowed and context defaults to 'host', then realized node_modules/vm2 sits inside the README's root:'./' tree, enabling self-require of the real classes.
**Exploitation:** Untrusted sandboxed code requires vm2's own installed package through the host require(), escapes the wrapper via readonly exports, constructs a fresh unrestricted NodeVM with child_process builtin, and executes arbitrary OS commands as the Node process user.
**Escalation:** Outer allowlist and all sandbox restrictions become irrelevant once an unrestricted inner NodeVM instance is obtained; fs read/write and full RCE follow.
**Insight:** When a sandbox's own package is reachable from the allowed require root, requiring the sandbox library itself yields unwrapped host classes — always check whether the guard's default is a no-op and whether the sandbox's own node_modules is inside the permitted tree.

