GitHub Security Advisory: GHSA-8hg8-63c5-gwmx
Package: vm2 (NPM)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-284: Improper Access Control, CWE-693: Protection Mechanism Failure
Vulnerable versions: <= 3.11.0
Patched in: 3.11.1

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

Details:
### Summary

When a `NodeVM` is created with `nesting: true`, sandbox code can unconditionally `require('vm2')` regardless of the outer VM's `require` configuration — including `require: false`. With access to `vm2`, the sandbox constructs a new inner `NodeVM` with its own unrestricted `require` settings and executes arbitrary OS commands on the host. Any application that runs untrusted code inside a `NodeVM` with `nesting: true` is fully compromised.

### Details

The vulnerability is in how the `nesting: true` option interacts with the legacy module resolver.

**`lib/nodevm.js:96-99`** — `NESTING_OVERRIDE` is a special builtin map that injects the `vm2` package into the sandbox:

```js
const NESTING_OVERRIDE = Object.freeze({
  __proto__: null,
  vm2: vm2NestingLoader
});
```

**`lib/nodevm.js:268-269`** — When `nesting: true`, this override is passed into the resolver factory alongside the host's `require` options:

```js
const customResolver = requireOpts instanceof Resolver;
const resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions(
  requireOpts,
  nesting && NESTING_OVERRIDE,  // ← injected when nesting:true
  this._compiler
);
```

**`lib/resolver-compat.js:193-197`** — This is the vulnerable branch. When `require: false` is set, `requireOpts` is falsy, so `!options` is true. Without nesting the function returns `DENY_RESOLVER` (block everything). With nesting, it instead builds a resolver that includes `vm2` from `NESTING_OVERRIDE`:

```js
function makeResolverFromLegacyOptions(options, override, compiler) {
  if (!options) {
    if (!override) return DENY_RESOLVER;  // require:false, no nesting → deny all
    // BUG: require:false + nesting:true reaches here
    // override (NESTING_OVERRIDE) is applied, making vm2 available
    const builtins = makeBuiltinsFromLegacyOptions(undefined, defaultRequire, undefined, override);
    return new Resolver(DEFAULT_FS, [], builtins);  // vm2 is now requireable
  }
  // ...
}
```

**`lib/builtin.js:102-106`** — `NESTING_OVERRIDE` is merged unconditionally into builtins, overriding any user-configured allowlist:

```js
if (overrides) {
  const keys = Object.getOwnPropertyNames(overrides);
  for (const key of keys) {
    res.set(key, overrides[key]);  // vm2 always injected when nesting:true
  }
}
```

The result: `require('vm2')` always succeeds inside a `NodeVM` with `nesting: true`, regardless of `require: false`, `require: { builtin: [] }`, or any other restriction. Once the sandbox has `vm2`, it creates a new inner `NodeVM` with whatever `require` config it chooses — unconstrained by the outer VM — and reaches `child_process`.

This was introduced in commit `2353ce60` (Feb 8, 2022) and survived a major refactor in commit `9e2b6051` (Apr 8, 2023). The JSDoc for `nesting` does warn that "scripts can create a NodeVM which can require any host module," but does not document that `nesting: true` silently defeats `require: false`, which is the non-obvious part of this interaction.