Skip to content

Commit 4d662e3

Browse files
authored
Merge pull request #521 from ulixee/pathContext
feat: allow per-module choice for vm context
2 parents 4f63dc2 + 1728bdf commit 4d662e3

File tree

6 files changed

+52
-11
lines changed

6 files changed

+52
-11
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou
141141
* `require.builtin` - Array of allowed built-in modules, accepts ["\*"] for all (default: none). **WARNING**: "\*" can be dangerous as new built-ins can be added.
142142
* `require.root` - Restricted path(s) where local modules can be required (default: every path).
143143
* `require.mock` - Collection of mock modules (both external or built-in).
144-
* `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.
144+
* `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.
145145
* `require.import` - An array of modules to be loaded into NodeVM on start.
146146
* `require.resolve` - An additional lookup function in case a module wasn't found in one of the traditional node lookup paths.
147147
* `require.customRequire` - Use instead of the `require` function to load modules from the host.

lib/nodevm.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
* @return {*} The required module object.
1818
*/
1919

20+
/**
21+
* This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided.
22+
*
23+
* NOTE: many interoperating modules must live in the same context.
24+
*
25+
* @callback pathContextCallback
26+
* @param {string} modulePath - The full path to the module filename being requested.
27+
* @param {string} extensionType - The module type (node = native, js = cjs/esm module)
28+
* @return {("host"|"sandbox")} The context for this module.
29+
*/
30+
2031
const fs = require('fs');
2132
const pa = require('path');
2233
const {
@@ -177,14 +188,16 @@ class NodeVM extends VM {
177188
* @param {string[]} [options.require.builtin=[]] - Array of allowed built-in modules, accepts ["*"] for all.
178189
* @param {(string|string[])} [options.require.root] - Restricted path(s) where local modules can be required. If omitted every path is allowed.
179190
* @param {Object} [options.require.mock] - Collection of mock modules (both external or built-in).
180-
* @param {("host"|"sandbox")} [options.require.context="host"] - <code>host</code> to require modules in host and proxy them to sandbox.
191+
* @param {("host"|"sandbox"|pathContextCallback)} [options.require.context="host"] -
192+
* <code>host</code> to require modules in host and proxy them to sandbox.
181193
* <code>sandbox</code> to load, compile and require modules in sandbox.
194+
* <code>pathContext(modulePath, ext)</code> to choose a mode per module (full path provided).
182195
* Builtin modules except <code>events</code> always required in host and proxied to sandbox.
183196
* @param {string[]} [options.require.import] - Array of modules to be loaded into NodeVM on start.
184197
* @param {resolveCallback} [options.require.resolve] - An additional lookup function in case a module wasn't
185198
* found in one of the traditional node lookup paths.
186199
* @param {customRequire} [options.require.customRequire=require] - Custom require to require host and built-in modules.
187-
* @param {boolean} [option.require.strict=true] - Load required modules in strict mode.
200+
* @param {boolean} [options.require.strict=true] - Load required modules in strict mode.
188201
* @param {boolean} [options.nesting=false] -
189202
* <b>WARNING: Allowing this is a security risk as scripts can create a NodeVM which can require any host module.</b>
190203
* Allow nesting of VMs.

lib/resolver-compat.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class LegacyResolver extends DefaultResolver {
113113
loadJS(vm, mod, filename) {
114114
filename = this.pathResolve(filename);
115115
this.checkAccess(mod, filename);
116-
if (this.pathContext(filename, 'js') === 'sandbox') {
116+
if (this.pathContext(filename, 'js') !== 'host') {
117117
const trustedMod = this.trustedMods.get(mod);
118118
const script = this.readScript(filename);
119119
vm.run(script, {filename, strict: true, module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path});
@@ -332,19 +332,21 @@ function resolverFromOptions(vm, options, override, compiler) {
332332
};
333333
}
334334

335+
const pathContext = typeof context === 'function' ? context : (() => context);
336+
335337
if (typeof externalOpt !== 'object') {
336-
return new DefaultResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict);
338+
return new DefaultResolver(fsOpt, builtins, checkPath, [], pathContext, newCustomResolver, hostRequire, compiler, strict);
337339
}
338340

339341
let transitive = false;
340342
if (Array.isArray(externalOpt)) {
341343
external = externalOpt;
342344
} else {
343345
external = externalOpt.modules;
344-
transitive = context === 'sandbox' && externalOpt.transitive;
346+
transitive = context !== 'host' && externalOpt.transitive;
345347
}
346348
externals = external.map(makeExternalMatcher);
347-
return new LegacyResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict, externals, transitive);
349+
return new LegacyResolver(fsOpt, builtins, checkPath, [], pathContext, newCustomResolver, hostRequire, compiler, strict, externals, transitive);
348350
}
349351

350352
exports.resolverFromOptions = resolverFromOptions;

lib/resolver.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class DefaultResolver extends Resolver {
197197
loadJS(vm, mod, filename) {
198198
filename = this.pathResolve(filename);
199199
this.checkAccess(mod, filename);
200-
if (this.pathContext(filename, 'js') === 'sandbox') {
200+
if (this.pathContext(filename, 'js') !== 'host') {
201201
const script = this.readScript(filename);
202202
vm.run(script, {filename, strict: this.strict, module: mod, wrapper: 'none', dirname: mod.path});
203203
} else {
@@ -216,7 +216,7 @@ class DefaultResolver extends Resolver {
216216
loadNode(vm, mod, filename) {
217217
filename = this.pathResolve(filename);
218218
this.checkAccess(mod, filename);
219-
if (this.pathContext(filename, 'node') === 'sandbox') throw new VMError('Native modules can be required only with context set to \'host\'.');
219+
if (this.pathContext(filename, 'node') !== 'host') throw new VMError('Native modules can be required only with context set to \'host\'.');
220220
const m = this.hostRequire(filename);
221221
mod.exports = vm.readonly(m);
222222
}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/nodevm.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,32 @@ describe('modules', () => {
228228
assert.ok(vm.run("require('module1')", __filename));
229229
});
230230

231+
it('allows choosing a context by path', () => {
232+
const vm = new NodeVM({
233+
require: {
234+
external: {
235+
modules: ['mocha', 'module1'],
236+
transitive: true,
237+
},
238+
context(module) {
239+
if (module.includes('mocha')) return 'host';
240+
return 'sandbox';
241+
}
242+
}
243+
});
244+
function isVMProxy(obj) {
245+
const key = {};
246+
const proto = Object.getPrototypeOf(obj);
247+
if (!proto) return undefined;
248+
proto.isVMProxy = key;
249+
const proxy = obj.isVMProxy !== key;
250+
delete proto.isVMProxy;
251+
return proxy;
252+
}
253+
assert.equal(isVMProxy(vm.run("module.exports = require('mocha')", __filename)), false, 'Mocha is a proxy');
254+
assert.equal(isVMProxy(vm.run("module.exports = require('module1')", __filename)), true, 'Module1 is not a proxy');
255+
});
256+
231257
it('can resolve paths based on a custom resolver', () => {
232258
const vm = new NodeVM({
233259
require: {

0 commit comments

Comments
 (0)