| 'use strict'; |
| |
| /** |
| * This callback will be called to resolve a module if it couldn't be found. |
| * |
| * @callback resolveCallback |
| * @param {string} moduleName - Name of the module used to resolve. |
| * @param {string} dirname - Name of the current directory. |
| * @return {(string|undefined)} The file or directory to use to load the requested module. |
| */ |
| |
| /** |
| * This callback will be called to require a module instead of node's require. |
| * |
| * @callback customRequire |
| * @param {string} moduleName - Name of the module requested. |
| * @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 { |
| Script |
| } = require('vm'); |
| const { |
| VMError |
| } = require('./bridge'); |
| const { |
| VMScript, |
| MODULE_PREFIX, |
| STRICT_MODULE_PREFIX, |
| MODULE_SUFFIX |
| } = require('./script'); |
| const { |
| transformer |
| } = require('./transformer'); |
| const { |
| VM |
| } = require('./vm'); |
| const { |
| makeResolverFromLegacyOptions |
| } = require('./resolver-compat'); |
| const { Resolver } = require('./resolver'); |
| |
| const objectDefineProperty = Object.defineProperty; |
| const objectDefineProperties = Object.defineProperties; |
| |
| /** |
| * Host objects |
| * |
| * @private |
| */ |
| const HOST = Object.freeze({ |
| __proto__: null, |
| version: parseInt(process.versions.node.split('.')[0]), |
| process, |
| console, |
| setTimeout, |
| setInterval, |
| setImmediate, |
| clearTimeout, |
| clearInterval, |
| clearImmediate |
| }); |
| |
| /** |
| * Compile a script. |
| * |
| * @private |
| * @param {string} filename - Filename of the script. |
| * @param {string} script - Script. |
| * @return {vm.Script} The compiled script. |
| */ |
| function compileScript(filename, script) { |
| return new Script(script, { |
| __proto__: null, |
| filename, |
| displayErrors: false |
| }); |
| } |
| |
| let cacheSandboxScript = null; |
| let cacheMakeNestingScript = null; |
| |
| const NESTING_OVERRIDE = Object.freeze({ |
| __proto__: null, |
| vm2: vm2NestingLoader |
| }); |
| |
| function makeCustomExtensions(vm, resolver, sourceExtensions) { |
| const extensions = { __proto__: null }; |
| const loadJS = resolver.makeExtensionHandler(vm, 'loadJS'); |
| |
| for (let i = 0; i < sourceExtensions.length; i++) { |
| extensions['.' + sourceExtensions[i]] = loadJS; |
| } |
| |
| if (!extensions['.json']) extensions['.json'] = resolver.makeExtensionHandler(vm, 'loadJSON'); |
| if (!extensions['.node']) extensions['.node'] = resolver.makeExtensionHandler(vm, 'loadNode'); |
| return extensions; |
| } |
| |
| function makeSafePaths(unsafePaths) { |
| if (unsafePaths === undefined) return undefined; |
| if (!Array.isArray(unsafePaths)) return true; |
| const paths = [...unsafePaths]; |
| if (paths.some(path => typeof path !== 'string')) return true; |
| return paths; |
| } |
| |
| function makeSafeOptions(unsafeOptions) { |
| if (unsafeOptions === undefined || unsafeOptions == null) return unsafeOptions; |
| if (typeof unsafeOptions !== 'object' && typeof unsafeOptions !== 'function') return unsafeOptions; |
| return { |
| unsafeOptions, |
| paths: makeSafePaths(unsafeOptions.paths) |
| }; |
| } |
| |
| /** |
| * Event caused by a <code>console.debug</code> call if <code>options.console="redirect"</code> is specified. |
| * |
| * @public |
| * @event NodeVM."console.debug" |
| * @type {...*} |
| */ |
| |
| /** |
| * Event caused by a <code>console.log</code> call if <code>options.console="redirect"</code> is specified. |
| * |
| * @public |
| * @event NodeVM."console.log" |
| * @type {...*} |
| */ |
| |
| /** |
| * Event caused by a <code>console.info</code> call if <code>options.console="redirect"</code> is specified. |
| * |
| * @public |
| * @event NodeVM."console.info" |
| * @type {...*} |
| */ |
| |
| /** |
| * Event caused by a <code>console.warn</code> call if <code>options.console="redirect"</code> is specified. |
| * |
| * @public |
| * @event NodeVM."console.warn" |
| * @type {...*} |
| */ |
| |
| /** |
| * Event caused by a <code>console.error</code> call if <code>options.console="redirect"</code> is specified. |
| * |
| * @public |
| * @event NodeVM."console.error" |
| * @type {...*} |
| */ |
| |
| /** |
| * Event caused by a <code>console.dir</code> call if <code>options.console="redirect"</code> is specified. |
| * |
| * @public |
| * @event NodeVM."console.dir" |
| * @type {...*} |
| */ |
| |
| /** |
| * Event caused by a <code>console.trace</code> call if <code>options.console="redirect"</code> is specified. |
| * |
| * @public |
| * @event NodeVM."console.trace" |
| * @type {...*} |
| */ |
| |
| /** |
| * Class NodeVM. |
| * |
| * @public |
| * @extends {VM} |
| * @extends {EventEmitter} |
| */ |
| class NodeVM extends VM { |
| |
| /** |
| * Create a new NodeVM instance.<br> |
| * |
| * Unlike VM, NodeVM lets you use require same way like in regular node.<br> |
| * |
| * However, it does not use the timeout. |
| * |
| * @public |
| * @param {Object} [options] - VM options. |
| * @param {Object} [options.sandbox] - Objects that will be copied into the global object of the sandbox. |
| * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. |
| * @param {boolean} [options.eval=true] - Allow the dynamic evaluation of code via eval(code) or Function(code)().<br> |
| * Only available for node v10+. |
| * @param {boolean} [options.wasm=true] - Allow to run wasm code.<br> |
| * Only available for node v10+. |
| * @param {("inherit"|"redirect"|"off")} [options.console="inherit"] - Sets the behavior of the console in the sandbox. |
| * <code>inherit</code> to enable console, <code>redirect</code> to redirect to events, <code>off</code> to disable console. |
| * @param {Object|boolean|Resolver} [options.require=false] - Allow require inside the sandbox. |
| * @param {(boolean|string[]|Object)} [options.require.external=false] - <b>WARNING: When allowing require the option <code>options.require.root</code> |
| * should be set to restrict the script from requiring any module. Values can be true, an array of allowed external modules or an object. |
| * @param {(string[])} [options.require.external.modules] - Array of allowed external modules. Also supports wildcards, so specifying ['@scope/*-ver-??], |
| * for instance, will allow using all modules having a name of the form @scope/something-ver-aa, @scope/other-ver-11, etc. |
| * @param {boolean} [options.require.external.transitive=false] - Boolean which indicates if transitive dependencies of external modules are allowed. |
| * @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"|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} [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. |
| * @param {("commonjs"|"none")} [options.wrapper="commonjs"] - <code>commonjs</code> to wrap script into CommonJS wrapper, |
| * <code>none</code> to retrieve value returned by the script. |
| * @param {string[]} [options.sourceExtensions=["js"]] - Array of file extensions to treat as source code. |
| * @param {string[]} [options.argv=[]] - Array of arguments passed to <code>process.argv</code>. |
| * This object will not be copied and the script can change this object. |
| * @param {Object} [options.env={}] - Environment map passed to <code>process.env</code>. |
| * This object will not be copied and the script can change this object. |
| * @param {boolean} [options.strict=false] - If modules should be loaded in strict mode. |
| * @throws {VMError} If the compiler is unknown. |
| */ |
| constructor(options = {}) { |
| const { |
| compiler, |
| eval: allowEval, |
| wasm, |
| console: consoleType = 'inherit', |
| require: requireOpts = false, |
| nesting = false, |
| wrapper = 'commonjs', |
| sourceExtensions = ['js'], |
| argv, |
| env, |
| strict = false, |
| sandbox |
| } = options; |
| |
| // Throw this early |
| if (sandbox && 'object' !== typeof sandbox) { |
| throw new VMError('Sandbox must be an object.'); |
| } |
| |
| super({__proto__: null, compiler: compiler, eval: allowEval, wasm}); |
| |
| const customResolver = requireOpts instanceof Resolver; |
| const resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions(requireOpts, nesting && NESTING_OVERRIDE, this._compiler); |
| |
| // This is only here for backwards compatibility. |
| objectDefineProperty(this, 'options', {__proto__: null, value: { |
| console: consoleType, |
| require: requireOpts, |
| nesting, |
| wrapper, |
| sourceExtensions, |
| strict |
| }}); |
| |
| objectDefineProperty(this, 'resolver', {__proto__: null, value: resolver, enumerable: true}); |
| |
| if (!cacheSandboxScript) { |
| cacheSandboxScript = compileScript(`${__dirname}/setup-node-sandbox.js`, |
| `(function (host, data) { ${fs.readFileSync(`${__dirname}/setup-node-sandbox.js`, 'utf8')}\n})`); |
| } |
| |
| const closure = this._runScript(cacheSandboxScript); |
| |
| const extensions = makeCustomExtensions(this, resolver, sourceExtensions); |
| |
| this.readonly(HOST); |
| |
| const { |
| Module, |
| jsonParse, |
| createRequireForModule, |
| requireImpl |
| } = closure(HOST, { |
| __proto__: null, |
| argv, |
| env, |
| console: consoleType, |
| extensions, |
| emitArgs: (event, args) => { |
| if (typeof event !== 'string' && typeof event !== 'symbol') throw new Error('Event is not a string'); |
| return this.emit(event, ...args); |
| }, |
| globalPaths: [...resolver.globalPaths], |
| getLookupPathsFor: (path) => { |
| if (typeof path !== 'string') return []; |
| return [...resolver.genLookupPaths(path)]; |
| }, |
| resolve: (mod, id, opt, ext, direct) => { |
| if (typeof id !== 'string') throw new Error('Id is not a string'); |
| const extList = Object.getOwnPropertyNames(ext); |
| return resolver.resolve(mod, id, makeSafeOptions(opt), extList, !!direct); |
| }, |
| lookupPaths: (mod, id) => { |
| if (typeof id !== 'string') throw new Error('Id is not a string'); |
| return [...resolver.lookupPaths(mod, id)]; |
| }, |
| loadBuiltinModule: (id) => { |
| if (typeof id !== 'string') throw new Error('Id is not a string'); |
| return resolver.loadBuiltinModule(this, id); |
| }, |
| registerModule: (mod, filename, path, parent, direct) => { |
| return resolver.registerModule(mod, filename, path, parent, direct); |
| }, |
| builtinModules: [...resolver.getBuiltinModulesList(this)], |
| dirname: (path) => { |
| if (typeof path !== 'string') return path; |
| return resolver.fs.dirname(path); |
| }, |
| basename: (path) => { |
| if (typeof path !== 'string') return path; |
| return resolver.fs.basename(path); |
| } |
| }); |
| |
| objectDefineProperties(this, { |
| __proto__: null, |
| _Module: {__proto__: null, value: Module}, |
| _jsonParse: {__proto__: null, value: jsonParse}, |
| _createRequireForModule: {__proto__: null, value: createRequireForModule}, |
| _requireImpl: {__proto__: null, value: requireImpl}, |
| _cacheRequireModule: {__proto__: null, value: null, writable: true} |
| }); |
| |
| |
| resolver.init(this); |
| |
| // prepare global sandbox |
| if (sandbox) { |
| this.setGlobals(sandbox); |
| } |
| |
| if (!customResolver && requireOpts && requireOpts.import) { |
| if (Array.isArray(requireOpts.import)) { |
| for (let i = 0, l = requireOpts.import.length; i < l; i++) { |
| this.require(requireOpts.import[i]); |
| } |
| } else { |
| this.require(requireOpts.import); |
| } |
| } |
| } |
| |
| /** |
| * @ignore |
| * @deprecated |
| */ |
| get _resolver() { |
| return this.resolver; |
| } |
| |
| /** |
| * @ignore |
| * @deprecated Just call the method yourself like <code>method(args);</code> |
| * @param {function} method - Function to invoke. |
| * @param {...*} args - Arguments to pass to the function. |
| * @return {*} Return value of the function. |
| * @todo Can we remove this function? It even had a bug that would use args as this parameter. |
| * @throws {*} Rethrows anything the method throws. |
| * @throws {VMError} If method is not a function. |
| * @throws {Error} If method is a class. |
| */ |
| call(method, ...args) { |
| if ('function' === typeof method) { |
| return method(...args); |
| } else { |
| throw new VMError('Unrecognized method type.'); |
| } |
| } |
| |
| /** |
| * Require a module in VM and return it's exports. |
| * |
| * @public |
| * @param {string} module - Module name. |
| * @return {*} Exported module. |
| * @throws {*} If the module couldn't be found or loading it threw an error. |
| */ |
| require(module) { |
| const path = this.resolver.fs.resolve('.'); |
| let mod = this._cacheRequireModule; |
| if (!mod || mod.path !== path) { |
| const filename = this.resolver.fs.join(path, '/vm.js'); |
| mod = new (this._Module)(filename, path); |
| this.resolver.registerModule(mod, filename, path, null, false); |
| this._cacheRequireModule = mod; |
| } |
| return this._requireImpl(mod, module, true); |
| } |
| |
| /** |
| * Run the code in NodeVM. |
| * |
| * First time you run this method, code is executed same way like in node's regular `require` - it's executed with |
| * `module`, `require`, `exports`, `__dirname`, `__filename` variables and expect result in `module.exports'. |
| * |
| * @param {(string|VMScript)} code - Code to run. |
| * @param {(string|Object)} [options] - Options map or filename. |
| * @param {string} [options.filename="vm.js"] - Filename that shows up in any stack traces produced from this script.<br> |
| * This is only used if code is a String. |
| * @param {boolean} [options.strict] - If modules should be loaded in strict mode. Defaults to NodeVM options. |
| * @param {("commonjs"|"none")} [options.wrapper] - <code>commonjs</code> to wrap script into CommonJS wrapper, |
| * <code>none</code> to retrieve value returned by the script. Defaults to NodeVM options. |
| * @return {*} Result of executed code. |
| * @throws {SyntaxError} If there is a syntax error in the script. |
| * @throws {*} If the script execution terminated with an exception it is propagated. |
| * @fires NodeVM."console.debug" |
| * @fires NodeVM."console.log" |
| * @fires NodeVM."console.info" |
| * @fires NodeVM."console.warn" |
| * @fires NodeVM."console.error" |
| * @fires NodeVM."console.dir" |
| * @fires NodeVM."console.trace" |
| */ |
| run(code, options) { |
| let script; |
| let filename; |
| |
| if (typeof options === 'object') { |
| filename = options.filename; |
| } else { |
| filename = options; |
| options = {__proto__: null}; |
| } |
| |
| const { |
| strict = this.options.strict, |
| wrapper = this.options.wrapper, |
| module: customModule, |
| require: customRequire, |
| dirname: customDirname = null |
| } = options; |
| |
| let sandboxModule = customModule; |
| let dirname = customDirname; |
| |
| if (code instanceof VMScript) { |
| script = strict ? code._compileNodeVMStrict() : code._compileNodeVM(); |
| if (!sandboxModule) { |
| const resolvedFilename = this.resolver.fs.resolve(code.filename); |
| dirname = this.resolver.fs.dirname(resolvedFilename); |
| sandboxModule = new (this._Module)(resolvedFilename, dirname); |
| this.resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); |
| } |
| } else { |
| const unresolvedFilename = filename || 'vm.js'; |
| if (!sandboxModule) { |
| if (filename) { |
| const resolvedFilename = this.resolver.fs.resolve(filename); |
| dirname = this.resolver.fs.dirname(resolvedFilename); |
| sandboxModule = new (this._Module)(resolvedFilename, dirname); |
| this.resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); |
| } else { |
| sandboxModule = new (this._Module)(null, null); |
| sandboxModule.id = unresolvedFilename; |
| } |
| } |
| const prefix = strict ? STRICT_MODULE_PREFIX : MODULE_PREFIX; |
| let scriptCode = this._compiler(code, unresolvedFilename); |
| scriptCode = transformer(null, scriptCode, false, false, unresolvedFilename).code; |
| script = new Script(prefix + scriptCode + MODULE_SUFFIX, { |
| __proto__: null, |
| filename: unresolvedFilename, |
| displayErrors: false |
| }); |
| } |
| |
| const closure = this._runScript(script); |
| |
| const usedRequire = customRequire || this._createRequireForModule(sandboxModule); |
| |
| const ret = Reflect.apply(closure, this.sandbox, [sandboxModule.exports, usedRequire, sandboxModule, filename, dirname]); |
| return wrapper === 'commonjs' ? sandboxModule.exports : ret; |
| } |
| |
| /** |
| * Create NodeVM and run code inside it. |
| * |
| * @public |
| * @static |
| * @param {string} script - Code to execute. |
| * @param {string} [filename] - File name (used in stack traces only). |
| * @param {Object} [options] - VM options. |
| * @param {string} [options.filename] - File name (used in stack traces only). Used if <code>filename</code> is omitted. |
| * @return {*} Result of executed code. |
| * @see {@link NodeVM} for the options. |
| * @throws {SyntaxError} If there is a syntax error in the script. |
| * @throws {*} If the script execution terminated with an exception it is propagated. |
| */ |
| static code(script, filename, options) { |
| let unresolvedFilename; |
| if (filename != null) { |
| if ('object' === typeof filename) { |
| options = filename; |
| unresolvedFilename = options.filename; |
| } else if ('string' === typeof filename) { |
| unresolvedFilename = filename; |
| } else { |
| throw new VMError('Invalid arguments.'); |
| } |
| } else if ('object' === typeof options) { |
| unresolvedFilename = options.filename; |
| } |
| |
| if (arguments.length > 3) { |
| throw new VMError('Invalid number of arguments.'); |
| } |
| |
| const resolvedFilename = typeof unresolvedFilename === 'string' ? pa.resolve(unresolvedFilename) : undefined; |
| |
| return new NodeVM(options).run(script, resolvedFilename); |
| } |
| |
| /** |
| * Create NodeVM and run script from file inside it. |
| * |
| * @public |
| * @static |
| * @param {string} filename - Filename of file to load and execute in a NodeVM. |
| * @param {Object} [options] - NodeVM options. |
| * @return {*} Result of executed code. |
| * @see {@link NodeVM} for the options. |
| * @throws {Error} If filename is not a valid filename. |
| * @throws {SyntaxError} If there is a syntax error in the script. |
| * @throws {*} If the script execution terminated with an exception it is propagated. |
| */ |
| static file(filename, options) { |
| const resolvedFilename = pa.resolve(filename); |
| |
| if (!fs.existsSync(resolvedFilename)) { |
| throw new VMError(`Script '${filename}' not found.`); |
| } |
| |
| if (fs.statSync(resolvedFilename).isDirectory()) { |
| throw new VMError('Script must be file, got directory.'); |
| } |
| |
| return new NodeVM(options).run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename); |
| } |
| } |
| |
| function vm2NestingLoader(vm) { |
| if (!cacheMakeNestingScript) { |
| cacheMakeNestingScript = compileScript('nesting.js', '(vm, nodevm) => ({VM: vm, NodeVM: nodevm})'); |
| } |
| const makeNesting = vm._runScript(cacheMakeNestingScript); |
| return makeNesting(vm.readonly(VM), vm.readonly(NodeVM)); |
| } |
| |
| exports.NodeVM = NodeVM; |