diff --git a/README.md b/README.md index 3eac28c..e73363b 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou * `require.builtin` - Array of allowed built-in modules, accepts ["\*"] for all (default: none). **WARNING**: "\*" can be dangerous as new built-ins can be added. * `require.root` - Restricted path(s) where local modules can be required (default: every path). * `require.mock` - Collection of mock modules (both external or built-in). -* `require.context` - `host` (default) to require modules in the host and proxy them into the sandbox. `sandbox` to load, compile, and require modules in the sandbox. Except for `events`, built-in modules are always required in the host and proxied into the sandbox. +* `require.context` - `host` (default) to require modules in the host and proxy them into the sandbox. `sandbox` to load, compile, and require modules in the sandbox. `callback(moduleFilename, ext)` to dynamically choose a context per module. The default will be sandbox is nothing is specified. Except for `events`, built-in modules are always required in the host and proxied into the sandbox. * `require.import` - An array of modules to be loaded into NodeVM on start. * `require.resolve` - An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. * `require.customRequire` - Use instead of the `require` function to load modules from the host. diff --git a/lib/nodevm.js b/lib/nodevm.js index a55a0e2..2d69987 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -17,6 +17,17 @@ * @return {*} The required module object. */ +/** + * This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided. + * + * NOTE: many interoperating modules must live in the same context. + * + * @callback pathContextCallback + * @param {string} modulePath - The full path to the module filename being requested. + * @param {string} extensionType - The module type (node = native, js = cjs/esm module) + * @return {("host"|"sandbox")} The context for this module. + */ + const fs = require('fs'); const pa = require('path'); const { @@ -177,14 +188,16 @@ class NodeVM extends VM { * @param {string[]} [options.require.builtin=[]] - Array of allowed built-in modules, accepts ["*"] for all. * @param {(string|string[])} [options.require.root] - Restricted path(s) where local modules can be required. If omitted every path is allowed. * @param {Object} [options.require.mock] - Collection of mock modules (both external or built-in). - * @param {("host"|"sandbox")} [options.require.context="host"] - host to require modules in host and proxy them to sandbox. + * @param {("host"|"sandbox"|pathContextCallback)} [options.require.context="host"] - + * host to require modules in host and proxy them to sandbox. * sandbox to load, compile and require modules in sandbox. + * pathContext(modulePath, ext) to choose a mode per module (full path provided). * Builtin modules except events always required in host and proxied to sandbox. * @param {string[]} [options.require.import] - Array of modules to be loaded into NodeVM on start. * @param {resolveCallback} [options.require.resolve] - An additional lookup function in case a module wasn't * found in one of the traditional node lookup paths. * @param {customRequire} [options.require.customRequire=require] - Custom require to require host and built-in modules. - * @param {boolean} [option.require.strict=true] - Load required modules in strict mode. + * @param {boolean} [options.require.strict=true] - Load required modules in strict mode. * @param {boolean} [options.nesting=false] - * WARNING: Allowing this is a security risk as scripts can create a NodeVM which can require any host module. * Allow nesting of VMs. diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 0ef30ee..9021a04 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -113,7 +113,7 @@ class LegacyResolver extends DefaultResolver { loadJS(vm, mod, filename) { filename = this.pathResolve(filename); this.checkAccess(mod, filename); - if (this.pathContext(filename, 'js') === 'sandbox') { + if (this.pathContext(filename, 'js') !== 'host') { const trustedMod = this.trustedMods.get(mod); const script = this.readScript(filename); vm.run(script, {filename, strict: true, module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path}); @@ -332,8 +332,10 @@ function resolverFromOptions(vm, options, override, compiler) { }; } + const pathContext = typeof context === 'function' ? context : (() => context); + if (typeof externalOpt !== 'object') { - return new DefaultResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict); + return new DefaultResolver(fsOpt, builtins, checkPath, [], pathContext, newCustomResolver, hostRequire, compiler, strict); } let transitive = false; @@ -341,10 +343,10 @@ function resolverFromOptions(vm, options, override, compiler) { external = externalOpt; } else { external = externalOpt.modules; - transitive = context === 'sandbox' && externalOpt.transitive; + transitive = context !== 'host' && externalOpt.transitive; } externals = external.map(makeExternalMatcher); - return new LegacyResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict, externals, transitive); + return new LegacyResolver(fsOpt, builtins, checkPath, [], pathContext, newCustomResolver, hostRequire, compiler, strict, externals, transitive); } exports.resolverFromOptions = resolverFromOptions; diff --git a/lib/resolver.js b/lib/resolver.js index 22f9061..71eb31d 100644 --- a/lib/resolver.js +++ b/lib/resolver.js @@ -197,7 +197,7 @@ class DefaultResolver extends Resolver { loadJS(vm, mod, filename) { filename = this.pathResolve(filename); this.checkAccess(mod, filename); - if (this.pathContext(filename, 'js') === 'sandbox') { + if (this.pathContext(filename, 'js') !== 'host') { const script = this.readScript(filename); vm.run(script, {filename, strict: this.strict, module: mod, wrapper: 'none', dirname: mod.path}); } else { @@ -216,7 +216,7 @@ class DefaultResolver extends Resolver { loadNode(vm, mod, filename) { filename = this.pathResolve(filename); this.checkAccess(mod, filename); - if (this.pathContext(filename, 'node') === 'sandbox') throw new VMError('Native modules can be required only with context set to \'host\'.'); + if (this.pathContext(filename, 'node') !== 'host') throw new VMError('Native modules can be required only with context set to \'host\'.'); const m = this.hostRequire(filename); mod.exports = vm.readonly(m); } diff --git a/package-lock.json b/package-lock.json index 5e61b0d..f4e8dd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vm2", - "version": "3.9.11", + "version": "3.9.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vm2", - "version": "3.9.11", + "version": "3.9.16", "license": "MIT", "dependencies": { "acorn": "^8.7.0", diff --git a/test/nodevm.js b/test/nodevm.js index 58addb3..003f518 100644 --- a/test/nodevm.js +++ b/test/nodevm.js @@ -228,6 +228,32 @@ describe('modules', () => { assert.ok(vm.run("require('module1')", __filename)); }); + it('allows choosing a context by path', () => { + const vm = new NodeVM({ + require: { + external: { + modules: ['mocha', 'module1'], + transitive: true, + }, + context(module) { + if (module.includes('mocha')) return 'host'; + return 'sandbox'; + } + } + }); + function isVMProxy(obj) { + const key = {}; + const proto = Object.getPrototypeOf(obj); + if (!proto) return undefined; + proto.isVMProxy = key; + const proxy = obj.isVMProxy !== key; + delete proto.isVMProxy; + return proxy; + } + assert.equal(isVMProxy(vm.run("module.exports = require('mocha')", __filename)), false, 'Mocha is a proxy'); + assert.equal(isVMProxy(vm.run("module.exports = require('module1')", __filename)), true, 'Module1 is not a proxy'); + }); + it('can resolve paths based on a custom resolver', () => { const vm = new NodeVM({ require: {