Original flaw
Earlier flawvm2 nesting and require configuration could enable an unsafe nested sandbox path.
Sink: vm2 option validation for nesting and require
Originally written by XmiliaH
How AI contributed: Incomplete remediationPublished May 29, 2026 in
patriksimek/vm2
Summary The fix for GHSA-8hg8-63c5-gwmx (CVE-2023-37903) introduced a check in nodevm.js line 263 that blocks the combination nesting: true + require: false. However, the check uses strict equality (options.require === false), which is trivially bypassed by omitting the require option entirely. When require is not specified, options.require is undefined, not false. The strict equality check fails, so the security guard is skipped. Immediately after (line 280), the destructuring default require: requireOpts = false assigns requireOpts = false, producing the exact configuration the patch was designed to prevent. Root Cause Impact Full Remote Code Execution on the host system. An attacker running code inside a NodeVM({ nesting: true }) sandbox (without specifying require) can: 1. require('vm2') to get the vm2 library 2. Construct an inner NodeVM with require: { builtin: ['child_process'] } 3. Execute arbitrary OS commands via child_process.execSync The inner VM is completely unconstrained by the outer sandbox configuration. Reproduction Suggested Fix Or move the check after the destructuring default assignment:
The incomplete guard compared nesting and require to exact boolean literals, while the downstream resolver used nesting truthiness and accepted only a real require configuration. The final fix applies that structural condition before constructing the resolver.
Case facts
Only the highlighted steps are this advisory. The first step is the earlier flaw the AI tried, and failed, to close.
Original flaw
Earlier flawSink: vm2 option validation for nesting and require
Originally written by XmiliaH
This advisoryCVE-2026-47137 (GHSA-M4WX-M65X-GHRR)
AI tried to fix this
The AI-assisted change was a real security patch, but it left the same advisory reachable.
Missed: Did not close equivalent unsafe nesting configurations.
Fixed again
This is the patch that actually stops the same attack path.
@@ -243,6 +250,28 @@ class NodeVM extends VM { * @throws {VMError} If the compiler is unknown. */ constructor(options = {}) {+ // SECURITY (GHSA-8hg8-63c5-gwmx): `nesting: true` injects a NESTING_OVERRIDE+ // builtin that exposes `vm2` to the sandbox regardless of `require: false`.+ // The sandbox then constructs an inner NodeVM with attacker-chosen `require`+ // config (unconstrained by the outer config — by design of nesting) and+ // reaches `child_process` for full host RCE. The contradictory option pair+ // is the specific trap the advisory describes; reject it at construction+ // with a clear error rather than silently producing an unsandboxed config.+ // (Bare `nesting: true` without `require: false` continues to work as the+ // documented escape hatch; the README "`nesting: true` is an escape hatch"+ // section explains the broader trade-off.)+ if (options.nesting === true && options.require === false) {+ throw new VMError(+ 'NodeVM `nesting: true` is incompatible with `require: false`. '+ + '`nesting: true` is an escape hatch that lets sandbox code '+ + '`require(\'vm2\')` and construct nested VMs unconstrained by the outer '+ + 'config — which contradicts `require: false`. To deny all requires, '+ + 'remove `nesting: true`. To allow nested VMs, replace `require: false` '+ + 'with an explicit config (e.g. `require: { builtin: [] }`) so the '+ + 'tradeoff is visible. See README "`nesting: true` is an escape hatch". '+ + 'Context: GHSA-8hg8-63c5-gwmx.'+ );+ } const { compiler, eval: allowEval,@@ -269,37 +269,54 @@ class NodeVM extends VM { } = options; // SECURITY (GHSA-m4wx-m65x-ghrr, supersedes GHSA-8hg8-63c5-gwmx):- // `nesting: true` injects a NESTING_OVERRIDE builtin that exposes `vm2` to- // the sandbox. When `requireOpts` is also falsy, `makeResolverFromLegacyOptions`- // hits its `if (!options)` branch and builds a resolver whose ONLY builtin- // is `vm2` — a pure escape primitive (sandbox does `require('vm2')`, builds- // an inner NodeVM with attacker-chosen `require` config, reaches- // `child_process` for host RCE).+ // A truthy `nesting` injects a NESTING_OVERRIDE builtin that exposes+ // `vm2` to the sandbox. When `requireOpts` is not a real require-config+ // object, `makeResolverFromLegacyOptions` produces a resolver whose ONLY+ // builtin is `vm2` — a pure escape primitive (sandbox does+ // `require('vm2')`, builds an inner NodeVM with attacker-chosen+ // `require` config, reaches `child_process` for host RCE). //- // The check must run *after* destructuring and against `requireOpts`, not- // against the raw `options.require`, because the destructuring default- // (`require: requireOpts = false`) collapses every falsy/omitted path to- // the same insecure resolver:- // - `require: false` → requireOpts = false (GHSA-8hg8 PoC)- // - `require` omitted → requireOpts = false (GHSA-m4wx PoC)- // - `require: undefined` → requireOpts = false (default kicks in)- // - `require: null` / `0` / '' → requireOpts is null/0/''- // All of them hit `if (!options)` in makeResolverFromLegacyOptions and- // produce the NESTING_OVERRIDE-only resolver. Use `!requireOpts` to reject- // every shape that yields that resolver; the documented escape hatch- // (`nesting: true` + an explicit `require` config object, even `{}`)- // continues to work — the developer's intent is then visible.- if (nesting === true && !requireOpts) {+ // The guard mirrors the actual reachability of that insecure resolver+ // on two axes:+ //+ // 1. `nesting` truthiness, not strict `=== true`. The override is gated+ // a few lines below by `nesting && NESTING_OVERRIDE`, which fires for+ // ANY truthy value (`1`, `'yes'`, `{}`, `[]`, etc.). The check must+ // cover every truthy `nesting` value the override gate accepts.+ //+ // 2. `requireOpts` must be an actual require-config object (or a+ // `Resolver` instance), not just "truthy". `makeResolverFromLegacyOptions`+ // destructures every primitive/function value to all-`undefined` and+ // falls into the same NESTING_OVERRIDE-only `if (!options)` branch as+ // falsy values do. The shapes that collapse to the insecure resolver:+ // - `require: false` / `undefined` / `null` / `0` / `''`+ // → falsy → `if (!options)` branch+ // - `require: true` / `1` / `'yes'` / `Symbol()` / `function(){}`+ // → truthy non-object → destructured to all-undefined →+ // `makeBuiltinsFromLegacyOptions(undefined, …, override)` →+ // same NESTING_OVERRIDE-only resolver+ // The documented escape hatch (a truthy `nesting` + an explicit+ // `require` config object, even `{}`) continues to work — a non-null+ // object counts as the developer's deliberate acknowledgment of the+ // tradeoff. A custom `Resolver` instance is also accepted (the+ // `customResolver` path below bypasses `makeResolverFromLegacyOptions`+ // entirely, so NESTING_OVERRIDE is never injected into it).+ const hasRealRequireConfig =+ requireOpts instanceof Resolver+ || (typeof requireOpts === 'object' && requireOpts !== null);+ if (nesting && !hasRealRequireConfig) { throw new VMError(- 'NodeVM `nesting: true` requires an explicit `require` config. '- + '`nesting: true` is an escape hatch that exposes `vm2` to the '+ 'NodeVM `nesting` requires an explicit `require` config object. '+ + '`nesting` is an escape hatch that exposes `vm2` to the ' + 'sandbox (sandbox code can `require(\'vm2\')` and construct nested '- + 'VMs unconstrained by the outer config). With `require` falsy or '- + 'omitted, the resolver exposes ONLY `vm2` — a pure escape primitive. '- + 'To deny all requires, remove `nesting: true`. To allow nested VMs, '- + 'pass an explicit `require` config (e.g. `require: { builtin: [] }`) '- + 'so the tradeoff is visible. See README "`nesting: true` is an escape '- + 'hatch". Context: GHSA-m4wx-m65x-ghrr (supersedes GHSA-8hg8-63c5-gwmx).'+ + 'VMs unconstrained by the outer config). With `require` set to '+ + 'anything other than a config object (`false`, omitted, `true`, '+ + 'a number, a string, a function), the resolver exposes ONLY '+ + '`vm2` — a pure escape primitive. To deny all requires, remove '+ + '`nesting`. To allow nested VMs, pass an explicit `require` '+ + 'config (e.g. `require: { builtin: [] }`) so the tradeoff is '+ + 'visible. See README "`nesting: true` is an escape hatch". '+ + 'Context: GHSA-m4wx-m65x-ghrr (supersedes GHSA-8hg8-63c5-gwmx).' ); } AI-assisted change 8ffbe6491c5211cff22378213ec20e0111409b83d2d7be6666752272fb09deb3 · Fix fdb6d612ba5a1bc32eb5043047e084545d4e20c1dc2dd6adcafa6d9fdca4b817