'use strict'; /** * This callback will be called to transform a script to JavaScript. * * @callback compileCallback * @param {string} code - Script code to transform to JavaScript. * @param {string} filename - Filename of this script. * @return {string} JavaScript code that represents the script code. */ /** * This callback will be called to resolve a module if it couldn't be found. * * @callback resolveCallback * @param {string} moduleName - Name of the modulusedRequiree to resolve. * @param {string} dirname - Name of the current directory. * @return {(string|undefined)} The file or directory to use to load the requested module. */ const fs = require('fs'); const pa = require('path'); const { Script, createContext } = require('vm'); const { EventEmitter } = require('events'); const { INSPECT_MAX_BYTES } = require('buffer'); const { createBridge, VMError } = require('./bridge'); const { transformer, INTERNAL_STATE_NAME } = require('./transformer'); const { lookupCompiler } = require('./compiler'); const { VMScript } = require('./script'); const objectDefineProperties = Object.defineProperties; /** * Host objects * * @private */ const HOST = Object.freeze({ Buffer, Function, Object, transformAndCheck, INSPECT_MAX_BYTES, INTERNAL_STATE_NAME }); /** * 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 }); } /** * Default run options for vm.Script.runInContext * * @private */ const DEFAULT_RUN_OPTIONS = Object.freeze({__proto__: null, displayErrors: false}); function checkAsync(allow) { if (!allow) throw new VMError('Async not available'); } function transformAndCheck(args, code, isAsync, isGenerator, allowAsync) { const ret = transformer(args, code, isAsync, isGenerator, undefined); checkAsync(allowAsync || !ret.hasAsync); return ret.code; } /** * * This callback will be called and has a specific time to finish.
* No parameters will be supplied.
* If parameters are required, use a closure. * * @private * @callback runWithTimeout * @return {*} * */ let cacheTimeoutContext = null; let cacheTimeoutScript = null; /** * Run a function with a specific timeout. * * @private * @param {runWithTimeout} fn - Function to run with the specific timeout. * @param {number} timeout - The amount of time to give the function to finish. * @return {*} The value returned by the function. * @throws {Error} If the function took to long. */ function doWithTimeout(fn, timeout) { if (!cacheTimeoutContext) { cacheTimeoutContext = createContext(); cacheTimeoutScript = new Script('fn()', { __proto__: null, filename: 'timeout_bridge.js', displayErrors: false }); } cacheTimeoutContext.fn = fn; try { return cacheTimeoutScript.runInContext(cacheTimeoutContext, { __proto__: null, displayErrors: false, timeout }); } finally { cacheTimeoutContext.fn = null; } } const bridgeScript = compileScript(`${__dirname}/bridge.js`, `(function(global) {"use strict"; const exports = {};${fs.readFileSync(`${__dirname}/bridge.js`, 'utf8')}\nreturn exports;})`); const setupSandboxScript = compileScript(`${__dirname}/setup-sandbox.js`, `(function(global, host, bridge, data, context) { ${fs.readFileSync(`${__dirname}/setup-sandbox.js`, 'utf8')}\n})`); const getGlobalScript = compileScript('get_global.js', 'this'); let getGeneratorFunctionScript = null; let getAsyncFunctionScript = null; let getAsyncGeneratorFunctionScript = null; try { getGeneratorFunctionScript = compileScript('get_generator_function.js', '(function*(){}).constructor'); } catch (ex) {} try { getAsyncFunctionScript = compileScript('get_async_function.js', '(async function(){}).constructor'); } catch (ex) {} try { getAsyncGeneratorFunctionScript = compileScript('get_async_generator_function.js', '(async function*(){}).constructor'); } catch (ex) {} /** * Class VM. * * @public */ class VM extends EventEmitter { /** * The timeout for {@link VM#run} calls. * * @public * @since v3.9.0 * @member {number} timeout * @memberOf VM# */ /** * Get the global sandbox object. * * @public * @readonly * @since v3.9.0 * @member {Object} sandbox * @memberOf VM# */ /** * The compiler to use to get the JavaScript code. * * @public * @readonly * @since v3.9.0 * @member {(string|compileCallback)} compiler * @memberOf VM# */ /** * The resolved compiler to use to get the JavaScript code. * * @private * @readonly * @member {compileCallback} _compiler * @memberOf VM# */ /** * Create a new VM instance. * * @public * @param {Object} [options] - VM options. * @param {number} [options.timeout] - The amount of time until a call to {@link VM#run} will timeout. * @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)().
* Only available for node v10+. * @param {boolean} [options.wasm=true] - Allow to run wasm code.
* Only available for node v10+. * @param {boolean} [options.allowAsync=true] - Allows for async functions. * @throws {VMError} If the compiler is unknown. */ constructor(options = {}) { super(); // Read all options const { timeout, sandbox, compiler = 'javascript', allowAsync: optAllowAsync = true } = options; const allowEval = options.eval !== false; const allowWasm = options.wasm !== false; const allowAsync = optAllowAsync && !options.fixAsync; // Early error if sandbox is not an object. if (sandbox && 'object' !== typeof sandbox) { throw new VMError('Sandbox must be object.'); } // Early error if compiler can't be found. const resolvedCompiler = lookupCompiler(compiler); // Create a new context for this vm. const _context = createContext(undefined, { __proto__: null, codeGeneration: { __proto__: null, strings: allowEval, wasm: allowWasm } }); const sandboxGlobal = getGlobalScript.runInContext(_context, DEFAULT_RUN_OPTIONS); // Initialize the sandbox bridge const { createBridge: sandboxCreateBridge } = bridgeScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal); // Initialize the bridge const bridge = createBridge(sandboxCreateBridge, () => {}); const data = { __proto__: null, allowAsync }; if (getGeneratorFunctionScript) { data.GeneratorFunction = getGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS); } if (getAsyncFunctionScript) { data.AsyncFunction = getAsyncFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS); } if (getAsyncGeneratorFunctionScript) { data.AsyncGeneratorFunction = getAsyncGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS); } // Create the bridge between the host and the sandbox. const internal = setupSandboxScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal, HOST, bridge.other, data, _context); const runScript = (script) => { // This closure is intentional to hide _context and bridge since the allow to access the sandbox directly which is unsafe. let ret; try { ret = script.runInContext(_context, DEFAULT_RUN_OPTIONS); } catch (e) { throw bridge.from(e); } return bridge.from(ret); }; const makeReadonly = (value, mock) => { try { internal.readonly(value, mock); } catch (e) { throw bridge.from(e); } return value; }; const makeProtected = (value) => { const sandboxBridge = bridge.other; try { sandboxBridge.fromWithFactory(sandboxBridge.protectedFactory, value); } catch (e) { throw bridge.from(e); } return value; }; const addProtoMapping = (hostProto, sandboxProto) => { const sandboxBridge = bridge.other; let otherProto; try { otherProto = sandboxBridge.from(sandboxProto); sandboxBridge.addProtoMapping(otherProto, hostProto); } catch (e) { throw bridge.from(e); } bridge.addProtoMapping(hostProto, otherProto); }; const addProtoMappingFactory = (hostProto, sandboxProtoFactory) => { const sandboxBridge = bridge.other; const factory = () => { const proto = sandboxProtoFactory(this); bridge.addProtoMapping(hostProto, proto); return proto; }; try { const otherProtoFactory = sandboxBridge.from(factory); sandboxBridge.addProtoMappingFactory(otherProtoFactory, hostProto); } catch (e) { throw bridge.from(e); } }; // Define the properties of this object. // Use Object.defineProperties here to be able to // hide and set properties read-only. objectDefineProperties(this, { __proto__: null, timeout: { __proto__: null, value: timeout, writable: true, enumerable: true }, compiler: { __proto__: null, value: compiler, enumerable: true }, sandbox: { __proto__: null, value: bridge.from(sandboxGlobal), enumerable: true }, _runScript: {__proto__: null, value: runScript}, _makeReadonly: {__proto__: null, value: makeReadonly}, _makeProtected: {__proto__: null, value: makeProtected}, _addProtoMapping: {__proto__: null, value: addProtoMapping}, _addProtoMappingFactory: {__proto__: null, value: addProtoMappingFactory}, _compiler: {__proto__: null, value: resolvedCompiler}, _allowAsync: {__proto__: null, value: allowAsync} }); // prepare global sandbox if (sandbox) { this.setGlobals(sandbox); } } /** * Adds all the values to the globals. * * @public * @since v3.9.0 * @param {Object} values - All values that will be added to the globals. * @return {this} This for chaining. * @throws {*} If the setter of a global throws an exception it is propagated. And the remaining globals will not be written. */ setGlobals(values) { for (const name in values) { if (Object.prototype.hasOwnProperty.call(values, name)) { this.sandbox[name] = values[name]; } } return this; } /** * Set a global value. * * @public * @since v3.9.0 * @param {string} name - The name of the global. * @param {*} value - The value of the global. * @return {this} This for chaining. * @throws {*} If the setter of the global throws an exception it is propagated. */ setGlobal(name, value) { this.sandbox[name] = value; return this; } /** * Get a global value. * * @public * @since v3.9.0 * @param {string} name - The name of the global. * @return {*} The value of the global. * @throws {*} If the getter of the global throws an exception it is propagated. */ getGlobal(name) { return this.sandbox[name]; } /** * Freezes the object inside VM making it read-only. Not available for primitive values. * * @public * @param {*} value - Object to freeze. * @param {string} [globalName] - Whether to add the object to global. * @return {*} Object to freeze. * @throws {*} If the setter of the global throws an exception it is propagated. */ freeze(value, globalName) { this.readonly(value); if (globalName) this.sandbox[globalName] = value; return value; } /** * Freezes the object inside VM making it read-only. Not available for primitive values. * * @public * @param {*} value - Object to freeze. * @param {*} [mock] - When the object does not have a property the mock is used before prototype lookup. * @return {*} Object to freeze. */ readonly(value, mock) { return this._makeReadonly(value, mock); } /** * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. * * @public * @param {*} value - Object to protect. * @param {string} [globalName] - Whether to add the object to global. * @return {*} Object to protect. * @throws {*} If the setter of the global throws an exception it is propagated. */ protect(value, globalName) { this._makeProtected(value); if (globalName) this.sandbox[globalName] = value; return value; } /** * Run the code in VM. * * @public * @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.
* This is only used if code is a String. * @return {*} Result of executed code. * @throws {SyntaxError} If there is a syntax error in the script. * @throws {Error} An error is thrown when the script took to long and there is a timeout. * @throws {*} If the script execution terminated with an exception it is propagated. */ run(code, options) { let script; let filename; if (typeof options === 'object') { filename = options.filename; } else { filename = options; } if (code instanceof VMScript) { script = code._compileVM(); checkAsync(this._allowAsync || !code._hasAsync); } else { const useFileName = filename || 'vm.js'; let scriptCode = this._compiler(code, useFileName); const ret = transformer(null, scriptCode, false, false, useFileName); scriptCode = ret.code; checkAsync(this._allowAsync || !ret.hasAsync); // Compile the script here so that we don't need to create a instance of VMScript. script = new Script(scriptCode, { __proto__: null, filename: useFileName, displayErrors: false }); } if (!this.timeout) { return this._runScript(script); } return doWithTimeout(() => { return this._runScript(script); }, this.timeout); } /** * Run the code in VM. * * @public * @since v3.9.0 * @param {string} filename - Filename of file to load and execute in a NodeVM. * @return {*} Result of executed code. * @throws {Error} If filename is not a valid filename. * @throws {SyntaxError} If there is a syntax error in the script. * @throws {Error} An error is thrown when the script took to long and there is a timeout. * @throws {*} If the script execution terminated with an exception it is propagated. */ runFile(filename) { 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 this.run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename); } } exports.VM = VM;