Skip to content

Commit af983a8

Browse files
committed
Merge remote-tracking branch 'upstream/master' into resolver-api
2 parents 7b4eeab + 4d662e3 commit af983a8

File tree

11 files changed

+123
-48
lines changed

11 files changed

+123
-48
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
v3.9.17 (2023-04-17)
2+
--------------------
3+
[fix] Multiple security fixes.
4+
15
v3.9.16 (2023-04-11)
26
--------------------
37
[fix] Security fix (see https://github.com/patriksimek/vm2/issues/516).

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.

index.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad
7272
*/
7373
export type HostRequire = (id: string) => any;
7474

75+
/**
76+
* This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided.
77+
*/
78+
export type PathContextCallback = (modulePath: string, extensionType: string) => 'host' | 'sandbox';
79+
7580
/**
7681
* Require options for a VM
7782
*/
@@ -83,9 +88,10 @@ export interface VMRequire {
8388
builtin?: readonly string[];
8489
/*
8590
* `host` (default) to require modules in host and proxy them to sandbox. `sandbox` to load, compile and
86-
* require modules in sandbox. Built-in modules except `events` always required in host and proxied to sandbox
91+
* require modules in sandbox or a callback which chooses the context based on the filename.
92+
* Built-in modules except `events` always required in host and proxied to sandbox
8793
*/
88-
context?: "host" | "sandbox";
94+
context?: "host" | "sandbox" | PathContextCallback;
8995
/** `true`, an array of allowed external modules or an object with external options (default: `false`) */
9096
external?: boolean | readonly string[] | { modules: readonly string[], transitive: boolean };
9197
/** Array of modules to be loaded into NodeVM on start. */

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 {
@@ -208,14 +219,16 @@ class NodeVM extends VM {
208219
* @param {string[]} [options.require.builtin=[]] - Array of allowed built-in modules, accepts ["*"] for all.
209220
* @param {(string|string[])} [options.require.root] - Restricted path(s) where local modules can be required. If omitted every path is allowed.
210221
* @param {Object} [options.require.mock] - Collection of mock modules (both external or built-in).
211-
* @param {("host"|"sandbox")} [options.require.context="host"] - <code>host</code> to require modules in host and proxy them to sandbox.
222+
* @param {("host"|"sandbox"|pathContextCallback)} [options.require.context="host"] -
223+
* <code>host</code> to require modules in host and proxy them to sandbox.
212224
* <code>sandbox</code> to load, compile and require modules in sandbox.
225+
* <code>pathContext(modulePath, ext)</code> to choose a mode per module (full path provided).
213226
* Builtin modules except <code>events</code> always required in host and proxied to sandbox.
214227
* @param {string[]} [options.require.import] - Array of modules to be loaded into NodeVM on start.
215228
* @param {resolveCallback} [options.require.resolve] - An additional lookup function in case a module wasn't
216229
* found in one of the traditional node lookup paths.
217230
* @param {customRequire} [options.require.customRequire=require] - Custom require to require host and built-in modules.
218-
* @param {boolean} [option.require.strict=true] - Load required modules in strict mode.
231+
* @param {boolean} [options.require.strict=true] - Load required modules in strict mode.
219232
* @param {boolean} [options.nesting=false] -
220233
* <b>WARNING: Allowing this is a security risk as scripts can create a NodeVM which can require any host module.</b>
221234
* Allow nesting of VMs.

lib/resolver-compat.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ function makeExternalMatcher(obj) {
4040

4141
class CustomResolver extends DefaultResolver {
4242

43-
constructor(fileSystem, globalPaths, builtinModules, rootPaths, context, customResolver, hostRequire, compiler, strict) {
43+
constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict) {
4444
super(fileSystem, globalPaths, builtinModules);
4545
this.rootPaths = rootPaths;
46-
this.context = context;
46+
this.pathContext = pathContext;
4747
this.customResolver = customResolver;
4848
this.hostRequire = hostRequire;
4949
this.compiler = compiler;
@@ -60,13 +60,13 @@ class CustomResolver extends DefaultResolver {
6060
}
6161

6262
loadJS(vm, mod, filename) {
63-
if (this.context === 'sandbox') return super.loadJS(vm, mod, filename);
63+
if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(vm, mod, filename);
6464
const m = this.hostRequire(filename);
6565
mod.exports = vm.readonly(m);
6666
}
6767

6868
loadNode(vm, mod, filename) {
69-
if (this.context === 'sandbox') return super.loadNode(vm, mod, filename);
69+
if (this.pathContext(filename, 'node') !== 'host') return super.loadNode(vm, mod, filename);
7070
const m = this.hostRequire(filename);
7171
mod.exports = vm.readonly(m);
7272
}
@@ -94,8 +94,8 @@ class CustomResolver extends DefaultResolver {
9494

9595
class LegacyResolver extends CustomResolver {
9696

97-
constructor(fileSystem, globalPaths, builtinModules, rootPaths, context, customResolver, hostRequire, compiler, strict, externals, allowTransitive) {
98-
super(fileSystem, globalPaths, builtinModules, rootPaths, context, customResolver, hostRequire, compiler, strict);
97+
constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) {
98+
super(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict);
9999
this.externals = externals.map(makeExternalMatcher);
100100
this.externalCache = externals.map(pattern => new RegExp(makeExternalMatcherRegex(pattern)));
101101
this.currMod = undefined;
@@ -158,10 +158,10 @@ class LegacyResolver extends CustomResolver {
158158
}
159159

160160
loadJS(vm, mod, filename) {
161-
if (this.context === 'sandbox') {
161+
if (this.pathContext(filename, 'js') !== 'host') {
162162
const trustedMod = this.trustedMods.get(mod);
163163
const script = this.readScript(filename);
164-
vm.run(script, {filename, strict: true, module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path});
164+
vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path});
165165
} else {
166166
const m = this.hostRequire(filename);
167167
mod.exports = vm.readonly(m);
@@ -217,8 +217,10 @@ function makeResolverFromLegacyOptions(options, override, compiler) {
217217

218218
const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined;
219219

220+
const pathContext = typeof context === 'function' ? context : (() => context);
221+
220222
if (typeof externalOpt !== 'object') {
221-
return new CustomResolver(fsOpt, [], builtins, checkedRootPaths, context, customResolver, hostRequire, compiler, strict);
223+
return new CustomResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict);
222224
}
223225

224226
let transitive = false;
@@ -227,9 +229,9 @@ function makeResolverFromLegacyOptions(options, override, compiler) {
227229
external = externalOpt;
228230
} else {
229231
external = externalOpt.modules;
230-
transitive = context === 'sandbox' && externalOpt.transitive;
232+
transitive = context !== 'host' && externalOpt.transitive;
231233
}
232-
return new LegacyResolver(fsOpt, [], builtins, checkedRootPaths, context, customResolver, hostRequire, compiler, strict, external, transitive);
234+
return new LegacyResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict, external, transitive);
233235
}
234236

235237
exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions;

lib/resolver.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,15 @@ class DefaultResolver extends Resolver {
206206
}
207207

208208
loadJS(vm, mod, filename) {
209-
const script = this.readScript(filename);
210-
vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: mod.path});
209+
filename = this.pathResolve(filename);
210+
this.checkAccess(mod, filename);
211+
if (this.pathContext(filename, 'js') !== 'host') {
212+
const script = this.readScript(filename);
213+
vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: mod.path});
214+
} else {
215+
const m = this.hostRequire(filename);
216+
mod.exports = vm.readonly(m);
217+
}
211218
}
212219

213220
loadJSON(vm, mod, filename) {
@@ -216,7 +223,11 @@ class DefaultResolver extends Resolver {
216223
}
217224

218225
loadNode(vm, mod, filename) {
219-
throw new VMError('Native modules can be required only with context set to \'host\'.');
226+
filename = this.pathResolve(filename);
227+
this.checkAccess(mod, filename);
228+
if (this.pathContext(filename, 'node') !== 'host') throw new VMError('Native modules can be required only with context set to \'host\'.');
229+
const m = this.hostRequire(filename);
230+
mod.exports = vm.readonly(m);
220231
}
221232

222233
customResolve(x, path, extList) {

lib/setup-sandbox.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -439,23 +439,36 @@ global.eval = new LocalProxy(localEval, EvalHandler);
439439
* Promise sanitization
440440
*/
441441

442-
if (localPromise && !allowAsync) {
442+
if (localPromise) {
443443

444444
const PromisePrototype = localPromise.prototype;
445445

446-
overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, AsyncErrorHandler);
447-
// This seems not to work, and will produce
448-
// UnhandledPromiseRejectionWarning: TypeError: Method Promise.prototype.then called on incompatible receiver [object Object].
449-
// This is likely caused since the host.Promise.prototype.then cannot use the VM Proxy object.
450-
// Contextify.connect(host.Promise.prototype.then, Promise.prototype.then);
446+
if (!allowAsync) {
447+
448+
overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, AsyncErrorHandler);
449+
// This seems not to work, and will produce
450+
// UnhandledPromiseRejectionWarning: TypeError: Method Promise.prototype.then called on incompatible receiver [object Object].
451+
// This is likely caused since the host.Promise.prototype.then cannot use the VM Proxy object.
452+
// Contextify.connect(host.Promise.prototype.then, Promise.prototype.then);
453+
454+
} else {
455+
456+
overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, {
457+
__proto__: null,
458+
apply(target, thiz, args) {
459+
if (args.length > 1) {
460+
const onRejected = args[1];
461+
if (typeof onRejected === 'function') {
462+
args[1] = function wrapper(error) {
463+
error = ensureThis(error);
464+
return localReflectApply(onRejected, this, [error]);
465+
};
466+
}
467+
}
468+
return localReflectApply(target, thiz, args);
469+
}
470+
});
451471

452-
if (PromisePrototype.finally) {
453-
overrideWithProxy(PromisePrototype, 'finally', PromisePrototype.finally, AsyncErrorHandler);
454-
// Contextify.connect(host.Promise.prototype.finally, Promise.prototype.finally);
455-
}
456-
if (Promise.prototype.catch) {
457-
overrideWithProxy(PromisePrototype, 'catch', PromisePrototype.catch, AsyncErrorHandler);
458-
// Contextify.connect(host.Promise.prototype.catch, Promise.prototype.catch);
459472
}
460473

461474
}

lib/transformer.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,30 +113,30 @@ function transformer(args, body, isAsync, isGenerator, filename) {
113113
if (nodeType === 'CatchClause') {
114114
const param = node.param;
115115
if (param) {
116-
if (param.type === 'ObjectPattern') {
116+
if (param.type === 'Identifier') {
117+
const name = assertType(param, 'Identifier').name;
118+
const cBody = assertType(node.body, 'BlockStatement');
119+
if (cBody.body.length > 0) {
120+
insertions.push({
121+
__proto__: null,
122+
pos: cBody.body[0].start,
123+
order: TO_LEFT,
124+
coder: () => `${name}=${INTERNAL_STATE_NAME}.handleException(${name});`
125+
});
126+
}
127+
} else {
117128
insertions.push({
118129
__proto__: null,
119130
pos: node.start,
120131
order: TO_RIGHT,
121-
coder: () => `catch(${tmpname}){try{throw(${tmpname}=${INTERNAL_STATE_NAME}.handleException(${tmpname}));}`
132+
coder: () => `catch(${tmpname}){${tmpname}=${INTERNAL_STATE_NAME}.handleException(${tmpname});try{throw ${tmpname};}`
122133
});
123134
insertions.push({
124135
__proto__: null,
125136
pos: node.body.end,
126137
order: TO_LEFT,
127138
coder: () => `}`
128139
});
129-
} else {
130-
const name = assertType(param, 'Identifier').name;
131-
const cBody = assertType(node.body, 'BlockStatement');
132-
if (cBody.body.length > 0) {
133-
insertions.push({
134-
__proto__: null,
135-
pos: cBody.body[0].start,
136-
order: TO_LEFT,
137-
coder: () => `${name}=${INTERNAL_STATE_NAME}.handleException(${name});`
138-
});
139-
}
140140
}
141141
}
142142
} else if (nodeType === 'WithStatement') {

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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"alcatraz",
1414
"contextify"
1515
],
16-
"version": "3.9.16",
16+
"version": "3.9.17",
1717
"main": "index.js",
1818
"sideEffects": false,
1919
"repository": "github:patriksimek/vm2",

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)