You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							505 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
	
	
							505 lines
						
					
					
						
							16 KiB
						
					
					
				| '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.
 | |
|  */
 | |
| 
 | |
| 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 {
 | |
| 	resolverFromOptions
 | |
| } = require('./resolver-compat');
 | |
| 
 | |
| 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
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * 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} [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")} [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.
 | |
| 	 * 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.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});
 | |
| 
 | |
| 		// This is only here for backwards compatibility.
 | |
| 		objectDefineProperty(this, 'options', {__proto__: null, value: {
 | |
| 			console: consoleType,
 | |
| 			require: requireOpts,
 | |
| 			nesting,
 | |
| 			wrapper,
 | |
| 			sourceExtensions,
 | |
| 			strict
 | |
| 		}});
 | |
| 
 | |
| 		const resolver = resolverFromOptions(this, requireOpts, nesting && NESTING_OVERRIDE, this._compiler);
 | |
| 
 | |
| 		objectDefineProperty(this, '_resolver', {__proto__: null, value: resolver});
 | |
| 
 | |
| 		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 = {
 | |
| 			__proto__: null
 | |
| 		};
 | |
| 
 | |
| 		const loadJS = (mod, filename) => resolver.loadJS(this, mod, filename);
 | |
| 
 | |
| 		for (let i = 0; i < sourceExtensions.length; i++) {
 | |
| 			extensions['.' + sourceExtensions[i]] = loadJS;
 | |
| 		}
 | |
| 
 | |
| 		if (!extensions['.json']) extensions['.json'] = (mod, filename) => resolver.loadJSON(this, mod, filename);
 | |
| 		if (!extensions['.node']) extensions['.node'] = (mod, filename) => resolver.loadNode(this, mod, filename);
 | |
| 
 | |
| 
 | |
| 		this.readonly(HOST);
 | |
| 		this.readonly(resolver);
 | |
| 		this.readonly(this);
 | |
| 
 | |
| 		const {
 | |
| 			Module,
 | |
| 			jsonParse,
 | |
| 			createRequireForModule,
 | |
| 			requireImpl
 | |
| 		} = closure(HOST, {
 | |
| 			__proto__: null,
 | |
| 			argv,
 | |
| 			env,
 | |
| 			console: consoleType,
 | |
| 			vm: this,
 | |
| 			resolver,
 | |
| 			extensions
 | |
| 		});
 | |
| 
 | |
| 		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 (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 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.pathResolve('.');
 | |
| 		let mod = this._cacheRequireModule;
 | |
| 		if (!mod || mod.path !== path) {
 | |
| 			const filename = this._resolver.pathConcat(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.pathResolve(code.filename);
 | |
| 				dirname = this._resolver.pathDirname(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.pathResolve(filename);
 | |
| 					dirname = this._resolver.pathDirname(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(resolver, vm, id) {
 | |
| 	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;
 |