@n8n/vm2
Version:
vm2 is a sandbox that can run untrusted code with whitelisted Node's built-in modules. Securely!
238 lines (201 loc) • 8.24 kB
JavaScript
'use strict';
// Translate the old options to the new Resolver functionality.
const {
Resolver,
DefaultResolver
} = require('./resolver');
const {VMError} = require('./bridge');
const {DefaultFileSystem} = require('./filesystem');
const {makeBuiltinsFromLegacyOptions} = require('./builtin');
const {jsCompiler} = require('./compiler');
/**
* Require wrapper to be able to annotate require with webpackIgnore.
*
* @private
* @param {string} moduleName - Name of module to load.
* @return {*} Module exports.
*/
function defaultRequire(moduleName) {
// Set module.parser.javascript.commonjsMagicComments=true in your webpack config.
// eslint-disable-next-line global-require
return require(/* webpackIgnore: true */ moduleName);
}
// source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
function escapeRegExp(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function makeExternalMatcherRegex(obj) {
return escapeRegExp(obj).replace(/\\\\|\//g, '[\\\\/]')
.replace(/\\\*\\\*/g, '.*').replace(/\\\*/g, '[^\\\\/]*').replace(/\\\?/g, '[^\\\\/]');
}
function makeExternalMatcher(obj) {
const regexString = makeExternalMatcherRegex(obj);
return new RegExp(`[\\\\/]node_modules[\\\\/]${regexString}(?:[\\\\/](?!(?:.*[\\\\/])?node_modules[\\\\/]).*)?$`);
}
class CustomResolver extends DefaultResolver {
constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict) {
super(fileSystem, globalPaths, builtinModules);
this.rootPaths = rootPaths;
this.pathContext = pathContext;
this.customResolver = customResolver;
this.hostRequire = hostRequire;
this.compiler = compiler;
this.strict = strict;
}
isPathAllowed(filename) {
return this.rootPaths === undefined || this.rootPaths.some(path => {
if (!filename.startsWith(path)) return false;
const len = path.length;
if (filename.length === len || (len > 0 && this.fs.isSeparator(path[len-1]))) return true;
return this.fs.isSeparator(filename[len]);
});
}
loadJS(vm, mod, filename) {
if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(vm, mod, filename);
const m = this.hostRequire(filename);
mod.exports = vm.readonly(m);
}
loadNode(vm, mod, filename) {
if (this.pathContext(filename, 'node') !== 'host') return super.loadNode(vm, mod, filename);
const m = this.hostRequire(filename);
mod.exports = vm.readonly(m);
}
customResolve(x, path, extList) {
if (this.customResolver === undefined) return undefined;
const resolved = this.customResolver(x, path);
if (!resolved) return undefined;
if (typeof resolved === 'string') {
return this.loadAsFileOrDirectory(resolved, extList);
}
const {module=x, path: resolvedPath} = resolved;
return this.loadNodeModules(module, [resolvedPath], extList);
}
getCompiler(filename) {
return this.compiler;
}
isStrict(filename) {
return this.strict;
}
}
class LegacyResolver extends CustomResolver {
constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) {
super(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict);
this.externals = externals.map(makeExternalMatcher);
this.externalCache = externals.map(pattern => new RegExp(makeExternalMatcherRegex(pattern)));
this.currMod = undefined;
this.trustedMods = new WeakMap();
this.allowTransitive = allowTransitive;
}
isPathAllowed(path) {
return this.isPathAllowedForModule(path, this.currMod);
}
isPathAllowedForModule(path, mod) {
if (!super.isPathAllowed(path)) return false;
if (mod) {
if (mod.allowTransitive) return true;
if (path.startsWith(mod.path)) {
const rem = path.slice(mod.path.length);
if (!/(?:^|[\\\\/])node_modules(?:$|[\\\\/])/.test(rem)) return true;
}
}
return this.externals.some(regex => regex.test(path));
}
registerModule(mod, filename, path, parent, direct) {
const trustedParent = this.trustedMods.get(parent);
this.trustedMods.set(mod, {
filename,
path,
paths: this.genLookupPaths(path),
allowTransitive: this.allowTransitive &&
((direct && trustedParent && trustedParent.allowTransitive) || this.externals.some(regex => regex.test(filename)))
});
}
resolveFull(mod, x, options, extList, direct) {
this.currMod = undefined;
if (!direct) return super.resolveFull(mod, x, options, extList, false);
const trustedMod = this.trustedMods.get(mod);
if (!trustedMod || mod.path !== trustedMod.path) return super.resolveFull(mod, x, options, extList, false);
const paths = [...mod.paths];
if (paths.length !== trustedMod.paths.length) return super.resolveFull(mod, x, options, extList, false);
for (let i = 0; i < paths.length; i++) {
if (paths[i] !== trustedMod.paths[i]) {
return super.resolveFull(mod, x, options, extList, false);
}
}
try {
this.currMod = trustedMod;
return super.resolveFull(trustedMod, x, options, extList, true);
} finally {
this.currMod = undefined;
}
}
checkAccess(mod, filename) {
const trustedMod = this.trustedMods.get(mod);
if ((!trustedMod || trustedMod.filename !== filename) && !this.isPathAllowedForModule(filename, undefined)) {
throw new VMError(`Module '${filename}' is not allowed to be required. The path is outside the border!`, 'EDENIED');
}
}
loadJS(vm, mod, filename) {
if (this.pathContext(filename, 'js') !== 'host') {
const trustedMod = this.trustedMods.get(mod);
const script = this.readScript(filename);
vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path});
} else {
const m = this.hostRequire(filename);
mod.exports = vm.readonly(m);
}
}
customResolve(x, path, extList) {
if (this.customResolver === undefined) return undefined;
if (!(this.pathIsAbsolute(x) || this.pathIsRelative(x))) {
if (!this.externalCache.some(regex => regex.test(x))) return undefined;
}
const resolved = this.customResolver(x, path);
if (!resolved) return undefined;
if (typeof resolved === 'string') {
this.externals.push(new RegExp('^' + escapeRegExp(resolved)));
return this.loadAsFileOrDirectory(resolved, extList);
}
const {module=x, path: resolvedPath} = resolved;
this.externals.push(new RegExp('^' + escapeRegExp(resolvedPath)));
return this.loadNodeModules(module, [resolvedPath], extList);
}
}
const DEFAULT_FS = new DefaultFileSystem();
const DENY_RESOLVER = new Resolver(DEFAULT_FS, [], new Map());
function makeResolverFromLegacyOptions(options, override, compiler) {
if (!options) {
if (!override) return DENY_RESOLVER;
const builtins = makeBuiltinsFromLegacyOptions(undefined, defaultRequire, undefined, override);
return new Resolver(DEFAULT_FS, [], builtins);
}
const {
builtin: builtinOpt,
mock: mockOpt,
external: externalOpt,
root: rootPaths,
resolve: customResolver,
customRequire: hostRequire = defaultRequire,
context = 'host',
strict = true,
fs: fsOpt = DEFAULT_FS,
} = options;
const builtins = makeBuiltinsFromLegacyOptions(builtinOpt, hostRequire, mockOpt, override);
if (!externalOpt) return new Resolver(fsOpt, [], builtins);
if (!compiler) compiler = jsCompiler;
const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined;
const pathContext = typeof context === 'function' ? context : (() => context);
if (typeof externalOpt !== 'object') {
return new CustomResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict);
}
let transitive = false;
let external = undefined;
if (Array.isArray(externalOpt)) {
external = externalOpt;
} else {
external = externalOpt.modules;
transitive = context !== 'host' && externalOpt.transitive;
}
return new LegacyResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict, external, transitive);
}
exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions;