Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow per-module choice for vm context #521

Merged
merged 7 commits into from May 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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.
Expand Down
17 changes: 15 additions & 2 deletions lib/nodevm.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"] - <code>host</code> to require modules in host and proxy them to sandbox.
* @param {("host"|"sandbox"|pathContextCallback)} [options.require.context="host"] -
* <code>host</code> to require modules in host and proxy them to sandbox.
* <code>sandbox</code> to load, compile and require modules in sandbox.
* <code>pathContext(modulePath, ext)</code> to choose a mode per module (full path provided).
* Builtin modules except <code>events</code> 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] -
* <b>WARNING: Allowing this is a security risk as scripts can create a NodeVM which can require any host module.</b>
* Allow nesting of VMs.
Expand Down
10 changes: 6 additions & 4 deletions lib/resolver-compat.js
Expand Up @@ -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});
Expand Down Expand Up @@ -332,19 +332,21 @@ 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;
if (Array.isArray(externalOpt)) {
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;
4 changes: 2 additions & 2 deletions lib/resolver.js
Expand Up @@ -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 {
Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions test/nodevm.js
Expand Up @@ -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: {
Expand Down