From 294ce23a55aa63f67d1385df35888a85eead82a7 Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Mon, 17 Apr 2023 11:26:10 -0400 Subject: [PATCH 1/6] feat: allow per-module choice for vm context --- README.md | 1 + lib/nodevm.js | 15 ++++++++++++++- lib/resolver-compat.js | 4 +++- package-lock.json | 4 ++-- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3eac28c..aa8f650 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou * `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.pathContext` - A callback allowing custom context to be determined per module. Parameters are the module name, and extension. The callback must return `host` or `sandbox` as per above. * `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..488f34b 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. + * + * NOTE: many interoperating modules must live in the same context. + * + * @callback pathContextCallback + * @param {string} moduleName - Name of the module requested. + * @param {string} extensionType - The type of extension (node, js, json) + * @return {("host"|"sandbox")} The context for this module. + */ + const fs = require('fs'); const pa = require('path'); const { @@ -180,11 +191,13 @@ class NodeVM extends VM { * @param {("host"|"sandbox")} [options.require.context="host"] - host to require modules in host and proxy them to sandbox. * sandbox to load, compile and require modules in sandbox. * Builtin modules except events always required in host and proxied to sandbox. + * @param {pathContextCallback} [options.require.pathContext] - A callback per-module path to customize "where" to load a module. + * 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..2780b67 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -291,6 +291,8 @@ function resolverFromOptions(vm, options, override, compiler) { const builtins = genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override); + const pathContext = options.pathContext || (() => context); + if (!externalOpt) return new Resolver(fsOpt, builtins, [], hostRequire); let checkPath; @@ -344,7 +346,7 @@ function resolverFromOptions(vm, options, override, compiler) { transitive = context === 'sandbox' && 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/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", From 1b8b855339ae90529fdc703a74f24261476596bb Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Wed, 3 May 2023 08:25:38 -0400 Subject: [PATCH 2/6] fix: pass pathContext to DefaultResolver --- lib/resolver-compat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 2780b67..002049f 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -335,7 +335,7 @@ function resolverFromOptions(vm, options, override, compiler) { } 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; From fb71483524efaaa165e70c31f0ed650e05c7b1dd Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Wed, 3 May 2023 09:04:49 -0400 Subject: [PATCH 3/6] fix: simplify api interface for pathContext --- lib/nodevm.js | 6 +++--- lib/resolver-compat.js | 6 +++--- test/nodevm.js | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/nodevm.js b/lib/nodevm.js index 488f34b..b338ecd 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -188,10 +188,10 @@ 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. - * Builtin modules except events always required in host and proxied to sandbox. - * @param {pathContextCallback} [options.require.pathContext] - A callback per-module path to customize "where" to load a module. + * pathContext(module, ext) to choose a mode per module. * 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 diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 002049f..9600689 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -291,8 +291,6 @@ function resolverFromOptions(vm, options, override, compiler) { const builtins = genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override); - const pathContext = options.pathContext || (() => context); - if (!externalOpt) return new Resolver(fsOpt, builtins, [], hostRequire); let checkPath; @@ -334,6 +332,8 @@ function resolverFromOptions(vm, options, override, compiler) { }; } + const pathContext = typeof options.context === 'function' ? ((module, ext) => options.context(module, ext) || 'host') : (() => context); + if (typeof externalOpt !== 'object') { return new DefaultResolver(fsOpt, builtins, checkPath, [], pathContext, newCustomResolver, hostRequire, compiler, strict); } @@ -343,7 +343,7 @@ 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, [], pathContext, newCustomResolver, hostRequire, compiler, strict, externals, transitive); diff --git a/test/nodevm.js b/test/nodevm.js index 58addb3..69f14d0 100644 --- a/test/nodevm.js +++ b/test/nodevm.js @@ -228,6 +228,23 @@ 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'], + }, + context(module) { + if (module === 'mocha') return 'host'; + if (module === 'module1') return 'sandbox'; + } + } + }); + + assert.ok(vm.run("require('module1')", __filename)); + assert.ok(vm.run("require('mocha')", __filename)); + }); + it('can resolve paths based on a custom resolver', () => { const vm = new NodeVM({ require: { From cbd42bcd8f51742aea9eaf68f524722121ff4203 Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Tue, 9 May 2023 20:15:02 -0400 Subject: [PATCH 4/6] fix: alter test to verify module context --- test/nodevm.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/nodevm.js b/test/nodevm.js index 69f14d0..003f518 100644 --- a/test/nodevm.js +++ b/test/nodevm.js @@ -233,16 +233,25 @@ describe('modules', () => { require: { external: { modules: ['mocha', 'module1'], + transitive: true, }, context(module) { - if (module === 'mocha') return 'host'; - if (module === 'module1') return 'sandbox'; + if (module.includes('mocha')) return 'host'; + return 'sandbox'; } } }); - - assert.ok(vm.run("require('module1')", __filename)); - assert.ok(vm.run("require('mocha')", __filename)); + 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', () => { From e08521991114f4a6ef586804123fa07ed8f0faec Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Wed, 10 May 2023 09:38:37 -0400 Subject: [PATCH 5/6] feat: default pathContext to sandbox --- lib/nodevm.js | 6 +++--- lib/resolver-compat.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/nodevm.js b/lib/nodevm.js index b338ecd..38a6305 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -18,12 +18,12 @@ */ /** - * This callback will be called to specify the context to use "per" module. + * 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} moduleName - Name of the module requested. + * @param {string} modulePath - The full path to the module filename being requested. * @param {string} extensionType - The type of extension (node, js, json) * @return {("host"|"sandbox")} The context for this module. */ @@ -191,7 +191,7 @@ class NodeVM extends VM { * @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(module, ext) to choose a mode per module. + * 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 diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 9600689..7983dbc 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -332,7 +332,7 @@ function resolverFromOptions(vm, options, override, compiler) { }; } - const pathContext = typeof options.context === 'function' ? ((module, ext) => options.context(module, ext) || 'host') : (() => context); + const pathContext = typeof options.context === 'function' ? ((module, ext) => options.context(module, ext) || 'sandbox') : (() => context); if (typeof externalOpt !== 'object') { return new DefaultResolver(fsOpt, builtins, checkPath, [], pathContext, newCustomResolver, hostRequire, compiler, strict); From 1728bdf8d2e59c74264396948394f005e3f9a431 Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Sat, 13 May 2023 14:28:39 -0400 Subject: [PATCH 6/6] chore: simplify default function for path context --- README.md | 3 +-- lib/nodevm.js | 2 +- lib/resolver-compat.js | 4 ++-- lib/resolver.js | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aa8f650..e73363b 100644 --- a/README.md +++ b/README.md @@ -141,8 +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.pathContext` - A callback allowing custom context to be determined per module. Parameters are the module name, and extension. The callback must return `host` or `sandbox` as per above. +* `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 38a6305..2d69987 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -24,7 +24,7 @@ * * @callback pathContextCallback * @param {string} modulePath - The full path to the module filename being requested. - * @param {string} extensionType - The type of extension (node, js, json) + * @param {string} extensionType - The module type (node = native, js = cjs/esm module) * @return {("host"|"sandbox")} The context for this module. */ diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 7983dbc..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,7 +332,7 @@ function resolverFromOptions(vm, options, override, compiler) { }; } - const pathContext = typeof options.context === 'function' ? ((module, ext) => options.context(module, ext) || 'sandbox') : (() => context); + const pathContext = typeof context === 'function' ? context : (() => context); if (typeof externalOpt !== 'object') { return new DefaultResolver(fsOpt, builtins, checkPath, [], pathContext, newCustomResolver, hostRequire, compiler, strict); 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); }