GitHub Security Advisory: GHSA-947f-4v7f-x2v8
Package: vm2 (NPM)
Severity: CRITICAL (CVSS 9.9)
CWE: CWE-863: Incorrect Authorization
Vulnerable versions: = 3.10.5
Patched in: 3.11.0

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

Details:
## Summary
NodeVM's `builtin` allowlist can be bypassed when the `module` builtin is allowed (including via the `'*'` wildcard). The `module` builtin exposes Node's `Module._load()`, which loads any module by name directly in the host context, completely bypassing vm2's builtin restriction. This allows sandboxed code to load excluded builtins like `child_process` and achieve remote code execution.

## Severity
**Critical** (CVSS 3.1: 9.9)

`CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H`

- **Attack Vector:** Network — sandboxed code is typically received from external sources (user-submitted scripts, plugin code)
- **Attack Complexity:** Low — no special conditions required; `['*', '-child_process']` is a common, documented pattern
- **Privileges Required:** Low — attacker needs only the ability to submit code to the sandbox, which is the intended use case
- **User Interaction:** None
- **Scope:** Changed — escape from sandbox boundary to host system
- **Confidentiality Impact:** High — arbitrary command execution on the host
- **Integrity Impact:** High — arbitrary command execution on the host
- **Availability Impact:** High — arbitrary command execution on the host

## Affected Component
- `lib/builtin.js` — `makeBuiltinsFromLegacyOptions()` (lines 109-117) — includes `module` in `'*'` expansion
- `lib/builtin.js` — `addDefaultBuiltin()` (lines 86-90) — loads `module` with generic readonly wrapper
- `lib/builtin.js` — `SPECIAL_MODULES` (line 61) — does NOT include `module`

## CWE
- **CWE-863**: Incorrect Authorization

## Description

### Root Cause: The `module` builtin provides unrestricted host module loading

When `builtin: ['*', '-child_process']` is configured, `makeBuiltinsFromLegacyOptions` iterates over `BUILTIN_MODULES` and adds all modules not explicitly excluded:

```js
// lib/builtin.js:40
const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives')))
    .filter(s=>!s.startsWith('internal/'));

// lib/builtin.js:109-117
if (Array.isArray(builtins)) {
    const def = builtins.indexOf('*') >= 0;
    if (def) {
        for (let i = 0; i < BUILTIN_MODULES.length; i++) {
            const name = BUILTIN_MODULES[i];
            if (builtins.indexOf(`-${name}`) === -1) {
                addDefaultBuiltin(res, name, hostRequire);
            }
        }
    }
```

Node's `builtinModules` includes `'module'` (verified: `require('module').builtinModules.includes('module')` → `true`). Since only `'-child_process'` is excluded, `'module'` passes the filter and gets added.

The `module` builtin is NOT in `SPECIAL_MODULES` (which only covers `events`, `buffer`, `util`), so it gets the generic loader:

```js
// lib/builtin.js:86-90
function addDefaultBuiltin(builtins, key, hostRequire) {
    if (builtins.has(key)) return;
    const special = SPECIAL_MODULES[key];
    builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));
}
```

This wraps Node's `Module` class in a readonly proxy and