From 7e45ce3f75b9f61f57d8c12cd835f809956bd9e4 Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Mon, 7 Oct 2019 00:40:40 +0200 Subject: [PATCH 1/6] Added allot of jsdoc. Also added VM.setGlobals, VM.setGlobal, VM.getGlobal to easily set and get globals. VM.runFile was added to load a file and run its contents. Fixed that Boolean, Number, String objects were wrongly converted. Load and compile sandbox.js more lazy. Also moved all the different Caches into one CACHE. Use one global HOST object with all the host objects. --- lib/contextify.js | 31 +- lib/main.js | 1260 +++++++++++++++++++++++++++++++-------------- lib/sandbox.js | 4 +- test/vm.js | 29 +- 4 files changed, 933 insertions(+), 391 deletions(-) diff --git a/lib/contextify.js b/lib/contextify.js index 2457aa3..880547c 100644 --- a/lib/contextify.js +++ b/lib/contextify.js @@ -544,9 +544,9 @@ Decontextify.value = (value, traps, deepTraps, flags, mock) => { case 'object': if (value === null) { return null; - } else if (instanceOf(value, Number)) { return host.Number(value); - } else if (instanceOf(value, String)) { return host.String(value); - } else if (instanceOf(value, Boolean)) { return host.Boolean(value); + } else if (instanceOf(value, Number)) { return Decontextify.instance(value, host.Number, deepTraps, flags, 'Number'); + } else if (instanceOf(value, String)) { return Decontextify.instance(value, host.String, deepTraps, flags, 'String'); + } else if (instanceOf(value, Boolean)) { return Decontextify.instance(value, host.Boolean, deepTraps, flags, 'Boolean'); } else if (instanceOf(value, Date)) { return Decontextify.instance(value, host.Date, deepTraps, flags, 'Date'); } else if (instanceOf(value, RangeError)) { return Decontextify.instance(value, host.RangeError, deepTraps, flags, 'Error'); } else if (instanceOf(value, ReferenceError)) { return Decontextify.instance(value, host.ReferenceError, deepTraps, flags, 'Error'); @@ -871,9 +871,9 @@ Contextify.value = (value, traps, deepTraps, flags, mock) => { case 'object': if (value === null) { return null; - } else if (instanceOf(value, host.Number)) { return host.Number(value); - } else if (instanceOf(value, host.String)) { return host.String(value); - } else if (instanceOf(value, host.Boolean)) { return host.Boolean(value); + } else if (instanceOf(value, host.Number)) { return Contextify.instance(value, Number, deepTraps, flags, 'Number'); + } else if (instanceOf(value, host.String)) { return Contextify.instance(value, String, deepTraps, flags, 'String'); + } else if (instanceOf(value, host.Boolean)) { return Contextify.instance(value, Boolean, deepTraps, flags, 'Boolean'); } else if (instanceOf(value, host.Date)) { return Contextify.instance(value, Date, deepTraps, flags, 'Date'); } else if (instanceOf(value, host.RangeError)) { return Contextify.instance(value, RangeError, deepTraps, flags, 'Error'); } else if (instanceOf(value, host.ReferenceError)) { return Contextify.instance(value, ReferenceError, deepTraps, flags, 'Error'); @@ -910,8 +910,21 @@ Contextify.value = (value, traps, deepTraps, flags, mock) => { return null; } }; -Contextify.globalValue = (value, name) => { - return (global[name] = Contextify.value(value)); +Contextify.setGlobal = (name, value) => { + const prop = Contextify.value(name); + try { + global[prop] = Contextify.value(value); + } catch (e) { + throw Decontextify.value(e); + } +}; +Contextify.getGlobal = (name) => { + const prop = Contextify.value(name); + try { + return Decontextify.value(global[prop]); + } catch (e) { + throw Decontextify.value(e); + } }; Contextify.readonly = (value, mock) => { return Contextify.value(value, null, FROZEN_TRAPS, null, mock); @@ -923,6 +936,7 @@ Contextify.connect = (outer, inner) => { Decontextified.set(outer, inner); Contextified.set(inner, outer); }; +Contextify.makeModule = ()=>({exports: {}}); const BufferMock = host.Object.create(null); BufferMock.allocUnsafe = function allocUnsafe(size) { @@ -949,5 +963,6 @@ const exportsMap = host.Object.create(null); exportsMap.Contextify = Contextify; exportsMap.Decontextify = Decontextify; exportsMap.Buffer = LocalBuffer; +exportsMap.sandbox = Decontextify.value(global); return exportsMap; diff --git a/lib/main.js b/lib/main.js index 271d8f0..914500a 100644 --- a/lib/main.js +++ b/lib/main.js @@ -2,31 +2,121 @@ '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 transform a script to JavaScript. + * + * @callback resolveCallback + * @param {string} moduleName - Name of the module 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 vm = require('vm'); const pa = require('path'); const {EventEmitter} = require('events'); const {INSPECT_MAX_BYTES} = require('buffer'); -const COFFEE_SCRIPT_COMPILER = {compiler: null}; +/** + * Load a script from a file and compile it. + * + * @private + * @param {string} filename - File to load and compile to a script. + * @param {string} prefix - Prefix for the script. + * @param {string} suffix - Suffix for the script. + * @return {vm.Script} The compiled script. + */ +function loadAndCompileScript(filename, prefix, suffix) { + const data = fs.readFileSync(filename, 'utf8'); + return new vm.Script(prefix + data + suffix, { + filename, + displayErrors: false + }); +} + +/** + * + * Cache where we can cache some things + * + * @private + * @property {?compileCallback} coffeeScriptCompiler - The coffee script compiler or null if not yet used. + * @property {?Object} timeoutContext - The context used for the timeout functionality of null if not yet used. + * @property {?vm.Script} timeoutScript - The compiled script used for the timeout functionality of null if not yet used. + * @property {vm.Script} contextifyScript - The compiled script used to setup a sandbox. + * @property {?vm.Script} sandboxScript - The compiled script used to setup the NodeVM require mechanism of null if not yet used. + */ +const CACHE = { + coffeeScriptCompiler: null, + timeoutContext: null, + timeoutScript: null, + contextifyScript: loadAndCompileScript(`${__dirname}/contextify.js`, '(function(require, host) { ', '\n})'), + sandboxScript: null, + fixAsyncScript: null, + getGlobalScript: null, + getGeneratorFunctionScript: null, + getAsyncFunctionScript: null, + getAsyncGeneratorFunctionScript: null, +}; + +/** + * Default run options for vm.Script.runInContext + * + * @private + */ +const DEFAULT_RUN_OPTIONS = {displayErrors: false}; + +/** + * Returns the cached coffee script compiler or loads it + * if it is not found in the cache. + * + * @private + * @return {compileCallback} The coffee script compiler. + * @throws {VMError} If the coffee-script module can't be found. + */ function getCoffeeScriptCompiler() { - if (!COFFEE_SCRIPT_COMPILER.compiler) { + if (!CACHE.coffeeScriptCompiler) { try { const coffeeScript = require('coffee-script'); - COFFEE_SCRIPT_COMPILER.compiler = (code, filename) => { + CACHE.coffeeScriptCompiler = (code, filename) => { return coffeeScript.compile(code, {header: false, bare: true}); }; } catch (e) { throw new VMError('Coffee-Script compiler is not installed.'); } } - return COFFEE_SCRIPT_COMPILER.compiler; + return CACHE.coffeeScriptCompiler; } +/** + * The JavaScript compiler, just a identity function. + * + * @private + * @type {compileCallback} + * @param {string} code - The JavaScript code. + * @param {string} filename - Filename of this script. + * @return {string} The code. + */ function jsCompiler(code, filename) { return code; } +/** + * Look up the compiler for a specific name. + * + * @private + * @param {(string|compileCallback)} compiler - A compile callback or the name of the compiler. + * @return {compileCallback} The resolved compiler. + * @throws {VMError} If the compiler is unknown or the coffee script module was needed and couldn't be found. + */ function lookupCompiler(compiler) { if ('function' === typeof compiler) return compiler; switch (compiler) { @@ -48,41 +138,224 @@ function lookupCompiler(compiler) { /** * Class Script * - * @class + * @public */ - class VMScript { + + /** + * The script code with wrapping. If set will invalidate the cache.
+ * Writable only for backwards compatibility. + * + * @public + * @readonly + * @member {string} code + * @memberOf VMScript# + */ + + /** + * The filename used for this script. + * + * @public + * @readonly + * @since v3.8.5 + * @member {string} filename + * @memberOf VMScript# + */ + + /** + * The line offset use for stack traces. + * + * @public + * @readonly + * @since v3.8.5 + * @member {number} lineOffset + * @memberOf VMScript# + */ + + /** + * The column offset use for stack traces. + * + * @public + * @readonly + * @since v3.8.5 + * @member {number} columnOffset + * @memberOf VMScript# + */ + + /** + * The compiler to use to get the JavaScript code. + * + * @public + * @readonly + * @since v3.8.5 + * @member {(string|compileCallback)} compiler + * @memberOf VMScript# + */ + + /** + * The prefix for the script. + * + * @private + * @member {string} _prefix + * @memberOf VMScript# + */ + + /** + * The suffix for the script. + * + * @private + * @member {string} _suffix + * @memberOf VMScript# + */ + + /** + * The compiled vm.Script for the VM or if not compiled null. + * + * @private + * @member {?vm.Script} _compiledVM + * @memberOf VMScript# + */ + + /** + * The compiled vm.Script for the NodeVM or if not compiled null. + * + * @private + * @member {?vm.Script} _compiledNodeVM + * @memberOf VMScript# + */ + + /** + * The resolved compiler to use to get the JavaScript code. + * + * @private + * @readonly + * @member {compileCallback} _compiler + * @memberOf VMScript# + */ + + /** + * The script to run without wrapping. + * + * @private + * @member {string} _code + * @memberOf VMScript# + */ + /** * Create VMScript instance. * - * @param {String} code Code to run. - * @param {String} [filename] Filename that shows up in any stack traces produced from this script. - * @param {{ lineOffset: number, columnOffset: number }} [options] Options that define vm.Script options. - * @return {VMScript} - */ - - constructor(code, filename, options) { - this._code = String(code); - this.options = options || {}; - this.filename = filename || this.options.filename || 'vm.js'; - this._prefix = ''; - this._suffix = ''; - this._compiledVM = null; - this._compiledNodeVM = null; - this._compiler = lookupCompiler(this.options.compiler || 'javascript'); - this._unresolvedFilename = this.options.filename || this.filename; + * @public + * @param {String} 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. + * @param {number} [options.lineOffset=0] - Passed to vm.Script options. + * @param {number} [options.columnOffset=0] - Passed to vm.Script options. + * @param {(string|compileCallback)} [options.compiler=javascript] - The compiler to use. + * @throws {TypeError} If code is a Symbol. + * @throws {VMError} If the compiler is unknown or if coffee-script was requested but the module not found. + */ + constructor(code, options) { + const sCode = `${code}`; + let useFileName; + let useOptions; + if (arguments.length === 2) { + if (typeof options === 'object' && options.toString === Object.prototype.toString) { + useOptions = options || {}; + useFileName = useOptions.filename; + } else { + useOptions = {}; + useFileName = options; + } + } else if (arguments.length > 2) { + // We do it this way so that there are no more arguments in the function. + // eslint-disable-next-line prefer-rest-params + useOptions = arguments[2] || {}; + useFileName = options || useOptions.filename; + } else { + useOptions = {}; + } + + const { + compiler = 'javascript', + lineOffset = 0, + columnOffset = 0 + } = useOptions; + + // Throw if the compiler is unkown. + const resolvedCompiler = lookupCompiler(compiler); + + Object.defineProperties(this, { + code: { + // Put this here so that it is enumerable, and looks like a property. + get() { + return this._prefix + this._code + this._suffix; + }, + set(value) { + const strNewCode = String(value); + if (strNewCode === this._code && this._prefix === '' && this._suffix === '') return; + this._code = strNewCode; + this._prefix = ''; + this._suffix = ''; + this._compiledVM = null; + this._compiledNodeVM = null; + }, + enumerable: true + }, + filename: { + value: useFileName || 'vm.js', + enumerable: true + }, + lineOffset: { + value: lineOffset, + enumerable: true + }, + columnOffset: { + value: columnOffset, + enumerable: true + }, + compiler: { + value: compiler, + enumerable: true + }, + _code: { + value: sCode, + writable: true + }, + _prefix: { + value: '', + writable: true + }, + _suffix: { + value: '', + writable: true + }, + _compiledVM: { + value: null, + writable: true + }, + _compiledNodeVM: { + value: null, + writable: true + }, + _compiler: {value: resolvedCompiler} + }); } /** - * Wraps the code. + * Wraps the code.
+ * This will replace the old wrapping.
* Will invalidate the code cache. * - * @return {VMScript} + * @public + * @deprecated Since v3.8.5. Wrap your code before passing it into the VMScript object. + * @param {string} prefix - String that will be appended before the script code. + * @param {script} suffix - String that will be appended behind the script code. + * @return {this} This for chaining. + * @throws {TypeError} If prefix or suffix is a Symbol. */ - wrap(prefix, suffix) { - const strPrefix = String(prefix); - const strSuffix = String(suffix); + const strPrefix = `${prefix}`; + const strSuffix = `${suffix}`; if (this._prefix === strPrefix && this._suffix === strSuffix) return this; this._prefix = strPrefix; this._suffix = strSuffix; @@ -92,285 +365,410 @@ class VMScript { } /** - * This code will be compiled to VM code. + * Compile this script.
+ * This is useful to detect syntax errors in the script. * - * @return {VMScript} + * @public + * @return {this} This for chaining. + * @throws {SyntaxError} If there is a syntax error in the script. */ - compile() { - return this._compileVM(); + this._compileVM(); + return this; } /** - * For backwards compatibility. + * Compiles this script to a vm.Script. * - * @return {String} The wrapped code + * @private + * @param {string} prefix - JavaScript code that will be used as prefix. + * @param {string} suffix - JavaScript code that will be used as suffix. + * @return {vm.Script} The compiled vm.Script. + * @throws {SyntaxError} If there is a syntax error in the script. */ - get code() { - return this._prefix + this._code + this._suffix; - } - - /** - * For backwards compatibility. - * Will invalidate the code cache. - * - * @param {String} newCode The new code to run. - */ - set code(newCode) { - const strNewCode = String(newCode); - if (strNewCode === this._prefix + this._code + this._suffix) return; - this._code = strNewCode; - this._prefix = ''; - this._suffix = ''; - this._compiledVM = null; - this._compiledNodeVM = null; + _compile(prefix, suffix) { + return new vm.Script(prefix + this._compiler(this._prefix + this._code + this._suffix, this.filename) + suffix, { + filename: this.filename, + displayErrors: false, + lineOffset: this.lineOffset, + columnOffset: this.columnOffset + }); } /** - * Will compile the code for VM and cache it + * Will return the cached version of the script intended for VM or compile it. * - * @return {VMScript} + * @private + * @return {vm.Script} The compiled script + * @throws {SyntaxError} If there is a syntax error in the script. */ _compileVM() { - if (this._compiledVM) return this; - - this._compiledVM = new vm.Script(this._compiler(this._prefix + this._code + this._suffix, this._unresolvedFilename), { - filename: this.filename, - displayErrors: false, - lineOffset: this.options.lineOffset || 0, - columnOffset: this.options.columnOffset || 0 - }); - - return this; + let script = this._compiledVM; + if (!script) { + this._compiledVM = script = this._compile('', ''); + } + return script; } /** - * Will compile the code for NodeVM and cache it + * Will return the cached version of the script intended for NodeVM or compile it. * - * @return {VMScript} + * @private + * @return {vm.Script} The compiled script + * @throws {SyntaxError} If there is a syntax error in the script. */ _compileNodeVM() { - if (this._compiledNodeVM) return this; - - this._compiledNodeVM = new vm.Script('(function (exports, require, module, __filename, __dirname) { ' + - this._compiler(this._prefix + this._code + this._suffix, this._unresolvedFilename) + '\n})', { - filename: this.filename, - displayErrors: false, - lineOffset: this.options.lineOffset || 0, - columnOffset: this.options.columnOffset || 0 - }); - - return this; - } - - _runInVM(context) { - return this._compiledVM.runInContext(context, { - filename: this.filename, - displayErrors: false - }); - } - - _runInNodeVM(context) { - return this._compiledNodeVM.runInContext(context, { - filename: this.filename, - displayErrors: false - }); + let script = this._compiledNodeVM; + if (!script) { + this._compiledNodeVM = script = this._compile('(function (exports, require, module, __filename, __dirname) { ', '\n})'); + } + return script; } } -function loadScript(filename) { - const data = fs.readFileSync(filename, 'utf8'); - return new VMScript(data, filename); -} - -const SCRIPT_CACHE = { - cf: loadScript(`${__dirname}/contextify.js`).wrap('(function(require, host) { ', '\n})')._compileVM(), - sb: loadScript(`${__dirname}/sandbox.js`).wrap('(function (vm, host, Contextify, Decontextify, Buffer) { ', '\n})')._compileVM(), - fa: loadScript(`${__dirname}/fixasync.js`).wrap('(function () { ', '\n})'), - getGlobal: new VMScript('this'), - getGeneratorFunction: new VMScript('(function*(){}).constructor'), - getAsyncFunction: new VMScript('(async function(){}).constructor'), - getAsyncGeneratorFunction: new VMScript('(async function*(){}).constructor'), - exp: new VMScript('({exports: {}})')._compileVM(), - runTimeout: new VMScript('fn()', 'timeout_bridge.js')._compileVM() -}; - -const TIMEOUT_CONTEXT = {context: null}; +/** + * + * 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 {*} + * + */ +/** + * 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 (!TIMEOUT_CONTEXT.context) { - TIMEOUT_CONTEXT.context = vm.createContext(); + let ctx = CACHE.timeoutContext; + let script = CACHE.timeoutScript; + if (!ctx) { + CACHE.timeoutContext = ctx = vm.createContext(); + CACHE.timeoutScript = script = new vm.Script('fn()', { + filename: 'timeout_bridge.js', + displayErrors: false + }); } - TIMEOUT_CONTEXT.context.fn = fn; + ctx.fn = fn; try { - return SCRIPT_CACHE.runTimeout._compiledVM.runInContext(TIMEOUT_CONTEXT.context, { - filename: SCRIPT_CACHE.runTimeout.filename, + return script.runInContext(ctx, { displayErrors: false, timeout }); } finally { - TIMEOUT_CONTEXT.context.fn = null; + ctx.fn = null; } } /** * Class VM. * - * @property {Object} options VM options. + * @public */ - class VM extends EventEmitter { + /** - * Create VM instance. + * The timeout for {@link VM#run} calls. * - * @param {Object} [options] VM options. - * @return {VM} + * @public + * @since v3.8.5 + * @member {number} timeout + * @memberOf VM# + */ + + /** + * Get the global sandbox object. + * + * @public + * @readonly + * @since v3.8.5 + * @member {Object} sandbox + * @memberOf VM# + */ + + /** + * The compiler to use to get the JavaScript code. + * + * @public + * @readonly + * @since v3.8.5 + * @member {(string|compileCallback)} compiler + * @memberOf VM# + */ + + /** + * The context for this sandbox. + * + * @private + * @readonly + * @member {Object} _context + * @memberOf VM# + */ + + /** + * The internal methods for this sandbox. + * + * @private + * @readonly + * @member {{Contextify: Object, Decontextify: Object, Buffer: Object, sandbox:Object}} _internal + * @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.fixAsync=false] - Filters for async functions. + * @throws If the compiler is unknown. + */ constructor(options = {}) { super(); - // defaults - this.options = { - timeout: options.timeout, - sandbox: options.sandbox, - compiler: lookupCompiler(options.compiler || 'javascript'), - eval: options.eval === false ? false : true, - wasm: options.wasm === false ? false : true, - fixAsync: options.fixAsync - }; - - const host = { - version: parseInt(process.versions.node.split('.')[0]), - console, - String, - Number, - Buffer, - Boolean, - Array, - Date, - Error, - EvalError, - RangeError, - ReferenceError, - SyntaxError, - TypeError, - URIError, - RegExp, - Function, - Object, - VMError, - Proxy, - Reflect, - Map, - WeakMap, - Set, - WeakSet, - Promise, - Symbol, - INSPECT_MAX_BYTES - }; - - this._context = vm.createContext(undefined, { + // Read all options + const { + timeout, + sandbox, + compiler = 'javascript' + } = options; + const allowEval = options.eval !== false; + const allowWasm = options.wasm !== false; + const fixAsync = !!options.fixAsync; + + // Early error if compiler can't be found. + const resolvedCompiler = lookupCompiler(compiler); + + // Early error if sandbox is not an object. + if (sandbox && 'object' !== typeof sandbox) { + throw new VMError('Sandbox must be object.'); + } + + // Create a new context for this vm. + const _context = vm.createContext(undefined, { codeGeneration: { - strings: this.options.eval, - wasm: this.options.wasm + strings: allowEval, + wasm: allowWasm } }); - Reflect.defineProperty(this, '_internal', { - value: SCRIPT_CACHE.cf._runInVM(this._context).call(this._context, require, host) + // Create the bridge between the host and the sandbox. + const _internal = CACHE.contextifyScript.runInContext(_context, DEFAULT_RUN_OPTIONS).call(_context, require, HOST); + + // Define the properties of this object. + // Use Object.defineProperties here to be able to + // hide and set properties write only. + Object.defineProperties(this, { + timeout: { + value: timeout, + writable: true, + enumerable: true + }, + compiler: { + value: compiler, + enumerable: true + }, + sandbox: { + value: _internal.sandbox, + enumerable: true + }, + _context: {value: _context}, + _internal: {value: _internal}, + _compiler: {value: resolvedCompiler}, + _fixAsync: fixAsync }); - if (this.options.fixAsync) { - SCRIPT_CACHE.getGlobal._compileVM(); - SCRIPT_CACHE.fa._compileVM(); + if (fixAsync) { + if (!CACHE.fixAsyncScript) { + CACHE.fixAsyncScript = loadAndCompileScript(`${__dirname}/fixasync.js`, '(function() { ', '\n})'); + CACHE.getGlobalScript = new vm.Script('this', { + filename: 'get_global.js', + displayErrors: false + }); + try { + CACHE.getGeneratorFunctionScript = new vm.Script('(function*(){}).constructor', { + filename: 'get_generator_function.js', + displayErrors: false + }); + } catch (ex) {} + try { + CACHE.getAsyncFunctionScript = new vm.Script('(async function(){}).constructor', { + filename: 'get_async_function.js', + displayErrors: false + }); + } catch (ex) {} + try { + CACHE.getAsyncGeneratorFunctionScript = new vm.Script('(async function*(){}).constructor', { + filename: 'get_async_generator_function.js', + displayErrors: false + }); + } catch (ex) {} + } const internal = { __proto__: null, - global: SCRIPT_CACHE.getGlobal._runInVM(this._context), + global: CACHE.getGlobalScript.runInContext(this._context, DEFAULT_RUN_OPTIONS), Contextify: this._internal.Contextify, - host: host + host: HOST }; - try { - SCRIPT_CACHE.getGeneratorFunction._compileVM(); - internal.GeneratorFunction = SCRIPT_CACHE.getGeneratorFunction._runInVM(this._context); - } catch (ex) {} - try { - SCRIPT_CACHE.getAsyncFunction._compileVM(); - internal.AsyncFunction = SCRIPT_CACHE.getAsyncFunction._runInVM(this._context); - } catch (ex) {} - try { - SCRIPT_CACHE.getAsyncGeneratorFunction._compileVM(); - internal.AsyncGeneratorFunction = SCRIPT_CACHE.getAsyncGeneratorFunction._runInVM(this._context); - } catch (ex) {} - SCRIPT_CACHE.fa._runInVM(this._context).call(internal); + if (CACHE.getGeneratorFunctionScript) { + try { + internal.GeneratorFunction = CACHE.getGeneratorFunctionScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); + } catch (ex) {} + } + if (CACHE.getAsyncFunctionScript) { + try { + internal.AsyncFunction = CACHE.getAsyncFunctionScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); + } catch (ex) {} + } + if (CACHE.getAsyncGeneratorFunctionScript) { + try { + internal.AsyncGeneratorFunction = CACHE.getAsyncGeneratorFunctionScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); + } catch (ex) {} + } + CACHE.fixAsyncScript.runInContext(this._context, DEFAULT_RUN_OPTIONS).call(internal); } // prepare global sandbox - if (this.options.sandbox) { - if ('object' !== typeof this.options.sandbox) { - throw new VMError('Sandbox must be object.'); - } + if (sandbox) { + this.setGlobals(sandbox); + } + } - for (const name in this.options.sandbox) { - if (Object.prototype.hasOwnProperty.call(this.options.sandbox, name)) { - this._internal.Contextify.globalValue(this.options.sandbox[name], name); - } + /** + * Adds all the values to the globals. + * + * @public + * @since v3.8.5 + * @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._internal.Contextify.setGlobal(name, values[name]); } } + return this; + } + + /** + * Set a global value. + * + * @public + * @since v3.8.5 + * @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._internal.Contextify.setGlobal(name, value); + return this; + } + + /** + * Get a global value. + * + * @public + * @since v3.8.5 + * @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._internal.Contextify.getGlobal(name); } /** * Freezes the object inside VM making it read-only. Not available for primitive values. * - * @static - * @param {*} object Object to freeze. - * @param {String} [globalName] Whether to add the object to global. + * @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._internal.Contextify.readonly(value); - if (globalName) this._internal.Contextify.globalValue(value, globalName); + if (globalName) this._internal.Contextify.setGlobal(globalName, value); return value; } /** * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. * - * @static - * @param {*} object Object to protect. - * @param {String} [globalName] Whether to add the object to global. + * @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._internal.Contextify.protected(value); - if (globalName) this._internal.Contextify.globalValue(value, globalName); + if (globalName) this._internal.Contextify.globalValue(globalName, value); return value; } /** * Run the code in VM. * - * @param {String} code Code to run. - * @param {String} [filename] Filename that shows up in any stack traces produced from this script. + * @public + * @param {(string|VMScript)} code - Code to run. + * @param {string} [filename] - 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, filename) { - const script = code instanceof VMScript ? code : new VMScript(code, filename, {compiler: this.options.compiler}); - - if (this.options.fixAsync && /\basync\b/.test(script.code)) { - throw new VMError('Async not available'); + let script; + if (code instanceof VMScript) { + if (this._fixAsync && /\basync\b/.test(code.code)) { + throw new VMError('Async not available'); + } + script = code._compileVM(); + } else { + if (this._fixAsync && /\basync\b/.test(code)) { + throw new VMError('Async not available'); + } + const useFileName = filename || 'vm.js'; + // Compile the script here so that we don't need to create a instance of VMScript. + script = new vm.Script(this._compiler(code, useFileName), { + filename: useFileName, + displayErrors: false + }); } - script._compileVM(); - - if (!this.options.timeout) { + if (!this.timeout) { + // If no timeout is given, directly run the script. try { - return this._internal.Decontextify.value(script._runInVM(this._context)); + return this._internal.Decontextify.value(script.runInContext(this._context, DEFAULT_RUN_OPTIONS)); } catch (e) { throw this._internal.Decontextify.value(e); } @@ -378,183 +776,217 @@ class VM extends EventEmitter { return doWithTimeout(()=>{ try { - return this._internal.Decontextify.value(script._runInVM(this._context)); + return this._internal.Decontextify.value(script.runInContext(this._context, DEFAULT_RUN_OPTIONS)); } catch (e) { throw this._internal.Decontextify.value(e); } - }, this.options.timeout); + }, this.timeout); + } + + /** + * Run the code in VM. + * + * @public + * @since v3.8.5 + * @param {string} filename - Filename of file to load and execute in a NodeVM. + * @return {*} Result of executed code. + * @throws This will throw when there is a syntax error in the script. + * @throws This will throw when the script took to long and there is a timeout. + * @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); } + } +/** + * Event caused by a console.debug call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.debug" + * @type {...*} + */ + +/** + * Event caused by a console.log call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.log" + * @type {...*} + */ + +/** + * Event caused by a console.info call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.info" + * @type {...*} + */ + +/** + * Event caused by a console.warn call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.warn" + * @type {...*} + */ + +/** + * Event caused by a console.error call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.error" + * @type {...*} + */ + +/** + * Event caused by a console.dir call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.dir" + * @type {...*} + */ + +/** + * Event caused by a console.trace call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.trace" + * @type {...*} + */ + /** * Class NodeVM. * - * @class + * @public + * @extends {VM} * @extends {EventEmitter} - * @property {Object} module Pointer to main module. */ +class NodeVM extends VM { -class NodeVM extends EventEmitter { /** - * Create NodeVM instance. + * Create a new NodeVM instance.
* - * Unlike VM, NodeVM lets you use require same way like in regular node. + * Unlike VM, NodeVM lets you use require same way like in regular node.
+ * + * However, it does not use the timeout. * - * @param {Object} [options] VM options. - * @return {NodeVM} + * @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)().
+ * Only available for node v10+. + * @param {boolean} [options.wasm=true] - Allow to run wasm code.
+ * Only available for node v10+. + * @param {("inherit"|"redirect"|"off")} [options.console=inherit] - Sets the behavior of the console in the sandbox. + * inherit to enable console, redirect to redirect to events, off to disable console. + * @param {Object|boolean} [options.require=false] - Allow require inside the sandbox. + * @param {(boolean|string[]|Object)} [options.require.external=false] - 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 builtin 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 builtin). + * @param {("host"|"sandbox")} [options.require.context=host] - host to require modules in host and proxy them to sandbox. + * sandbox to load, compile and require modules in sandbox. + * Builtin modules except events 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 {boolean} [options.nesting=false] - Allow nesting of VMs. + * @param {("commonjs"|"none")} [options.wrapper=commonjs] - commonjs to wrap script into CommonJS wrapper, + * none to retrieve value returned by the script. + * @param {string[]} [options.sourceExtensions=['js']] - Array of file extensions to treat as source code. */ - constructor(options = {}) { - super(); + super({compiler: options.compiler, eval: options.eval, wasm: options.wasm}); + + const sandbox = options.sandbox; // defaults - this.options = { - sandbox: options.sandbox, + Object.defineProperty(this, 'options', {value: { console: options.console || 'inherit', require: options.require || false, - compiler: lookupCompiler(options.compiler || 'javascript'), - eval: options.eval === false ? false : true, - wasm: options.wasm === false ? false : true, nesting: options.nesting || false, wrapper: options.wrapper || 'commonjs', sourceExtensions: options.sourceExtensions || ['js'] - }; - - const host = { - version: parseInt(process.versions.node.split('.')[0]), - require, - process, - console, - setTimeout, - setInterval, - setImmediate, - clearTimeout, - clearInterval, - clearImmediate, - String, - Number, - Buffer, - Boolean, - Array, - Date, - Error, - EvalError, - RangeError, - ReferenceError, - SyntaxError, - TypeError, - URIError, - RegExp, - Function, - Object, - VMError, - Proxy, - Reflect, - Map, - WeakMap, - Set, - WeakSet, - Promise, - Symbol, - INSPECT_MAX_BYTES - }; - - if (this.options.nesting) { - host.VM = VM; - host.NodeVM = NodeVM; - } - - this._context = vm.createContext(undefined, { - codeGeneration: { - strings: this.options.eval, - wasm: this.options.wasm - } - }); + }}); - Object.defineProperty(this, '_internal', { - value: SCRIPT_CACHE.cf._runInVM(this._context).call(this._context, require, host) - }); + let sandboxScript = CACHE.sandboxScript; + if (!sandboxScript) { + CACHE.sandboxScript = sandboxScript = loadAndCompileScript(`${__dirname}/sandbox.js`, + '(function (vm, host, Contextify, Decontextify, Buffer) { ', '\n})'); + } - const closure = SCRIPT_CACHE.sb._runInVM(this._context); + const closure = sandboxScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); Object.defineProperty(this, '_prepareRequire', { - value: closure.call(this._context, this, host, this._internal.Contextify, this._internal.Decontextify, this._internal.Buffer) + value: closure.call(this._context, this, HOST, this._internal.Contextify, this._internal.Decontextify, this._internal.Buffer) }); // prepare global sandbox - if (this.options.sandbox) { - if ('object' !== typeof this.options.sandbox) { + if (sandbox) { + if ('object' !== typeof sandbox) { throw new VMError('Sandbox must be object.'); } - for (const name in this.options.sandbox) { - if (Object.prototype.hasOwnProperty.call(this.options.sandbox, name)) { - this._internal.Contextify.globalValue(this.options.sandbox[name], name); - } - } + this.setGlobals(sandbox); } if (this.options.require && this.options.require.import) { - if (!Array.isArray(this.options.require.import)) { - this.options.require.import = [this.options.require.import]; - } - - for (let i = 0, l = this.options.require.import.length; i < l; i++) { - this.require(this.options.require.import[i]); + if (Array.isArray(this.options.require.import)) { + for (let i = 0, l = this.options.require.import.length; i < l; i++) { + this.require(this.options.require.import[i]); + } + } else { + this.require(this.options.require.import); } } } /** - * @deprecated + * @ignore + * @deprecated Just call the method yourself like method(args); + * @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.apply(args); - + return method(...args); } else { throw new VMError('Unrecognized method type.'); } } - /** - * Freezes the object inside VM making it read-only. Not available for primitive values. - * - * @static - * @param {*} object Object to freeze. - * @param {String} [globalName] Whether to add the object to global. - * @return {*} Object to freeze. - */ - - freeze(value, globalName) { - this._internal.Contextify.readonly(value); - if (global) this._internal.Contextify.globalValue(value, globalName); - return value; - } - - /** - * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. - * - * @static - * @param {*} object Object to protect. - * @param {String} [globalName] Whether to add the object to global. - * @return {*} Object to protect. - */ - - protect(value, globalName) { - this._internal.Contextify.protected(value); - if (global) this._internal.Contextify.globalValue(value, globalName); - return value; - } - /** * Require a module in VM and return it's exports. * - * @param {String} module Module name. + * @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) { return this.run(`module.exports = require('${module}');`, 'vm.js'); } @@ -565,112 +997,140 @@ class NodeVM extends EventEmitter { * 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} code Code to run. - * @param {String} [filename] Filename that shows up in any stack traces produced from this script. + * @param {(string|VMScript)} code - Code to run. + * @param {string} [filename] - 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 {*} 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, filename) { let dirname; - let returned; let resolvedFilename; + let script; - if (filename) { - resolvedFilename = pa.resolve(filename); - dirname = pa.dirname(filename); + if (code instanceof VMScript) { + script = code._compileNodeVM(); + resolvedFilename = pa.resolve(code.filename); + dirname = pa.dirname(resolvedFilename); } else { - resolvedFilename = null; - dirname = null; + const unresolvedFilename = filename || 'vm.js'; + if (filename) { + resolvedFilename = pa.resolve(filename); + dirname = pa.dirname(resolvedFilename); + } else { + resolvedFilename = null; + dirname = null; + } + script = new vm.Script('(function (exports, require, module, __filename, __dirname) { ' + + this._compiler(code, unresolvedFilename) + '\n})', { + filename: unresolvedFilename, + displayErrors: false + }); } - const module = SCRIPT_CACHE.exp._runInVM(this._context); - - const script = code instanceof VMScript ? code : new VMScript(code, resolvedFilename, {compiler: this.options.compiler, filename}); - script._compileNodeVM(); + const wrapper = this.options.wrapper; + const module = this._internal.Contextify.makeModule(); try { - const closure = script._runInNodeVM(this._context); + const closure = script.runInContext(this._context, DEFAULT_RUN_OPTIONS); + + const returned = closure.call(this._context, module.exports, this._prepareRequire(dirname), module, resolvedFilename, dirname); - returned = closure.call(this._context, module.exports, this._prepareRequire(dirname), module, filename, dirname); + return this._internal.Decontextify.value(wrapper === 'commonjs' ? module.exports : returned); } catch (e) { throw this._internal.Decontextify.value(e); } - if (this.options.wrapper === 'commonjs') { - return this._internal.Decontextify.value(module.exports); - } else { - return this._internal.Decontextify.value(returned); - } } /** * Create NodeVM and run code inside it. * - * @param {String} script Javascript code. - * @param {String} [filename] File name (used in stack traces only). - * @param {Object} [options] VM options. - * @return {NodeVM} VM. + * @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 filename 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; - filename = null; + unresolvedFilename = options.filename; } else if ('string' === typeof filename) { - filename = pa.resolve(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.'); } - return new NodeVM(options).run(script, filename); + 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. * - * @param {String} [filename] File name (used in stack traces only). - * @param {Object} [options] VM options. - * @return {NodeVM} VM. + * @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) { - filename = pa.resolve(filename); + const resolvedFilename = pa.resolve(filename); - if (!fs.existsSync(filename)) { + if (!fs.existsSync(resolvedFilename)) { throw new VMError(`Script '${filename}' not found.`); } - if (fs.statSync(filename).isDirectory()) { + if (fs.statSync(resolvedFilename).isDirectory()) { throw new VMError('Script must be file, got directory.'); } - return new NodeVM(options).run(fs.readFileSync(filename, 'utf8'), filename); + return new NodeVM(options).run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename); } } /** * VMError. * - * @class + * @public * @extends {Error} - * @property {String} stack Call stack. - * @property {String} message Error message. */ - class VMError extends Error { + /** * Create VMError instance. * - * @param {String} message Error message. - * @return {VMError} + * @public + * @param {string} message - Error message. */ - constructor(message) { super(message); @@ -680,6 +1140,52 @@ class VMError extends Error { } } +/** + * Host objects + * + * @private + */ +const HOST = { + version: parseInt(process.versions.node.split('.')[0]), + require, + process, + console, + setTimeout, + setInterval, + setImmediate, + clearTimeout, + clearInterval, + clearImmediate, + String, + Number, + Buffer, + Boolean, + Array, + Date, + Error, + EvalError, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + URIError, + RegExp, + Function, + Object, + VMError, + Proxy, + Reflect, + Map, + WeakMap, + Set, + WeakSet, + Promise, + Symbol, + INSPECT_MAX_BYTES, + VM, + NodeVM +}; + exports.VMError = VMError; exports.NodeVM = NodeVM; exports.VM = VM; diff --git a/lib/sandbox.js b/lib/sandbox.js index e5080eb..7e27000 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -59,9 +59,7 @@ return ((vm, host) => { try { // Load module let contents = fs.readFileSync(filename, 'utf8'); - if (typeof vm.options.compiler === 'function') { - contents = vm.options.compiler(contents, filename); - } + contents = vm._compiler(contents, filename); const code = `(function (exports, require, module, __filename, __dirname) { 'use strict'; ${contents} \n});`; diff --git a/test/vm.js b/test/vm.js index af81a30..59192d3 100644 --- a/test/vm.js +++ b/test/vm.js @@ -128,11 +128,24 @@ describe('contextify', () => { assert.strictEqual(Object.prototype.toString.call(vm.run(`new Date`)), '[object Date]'); assert.strictEqual(Object.prototype.toString.call(vm.run(`new RangeError`)), '[object Error]'); assert.strictEqual(Object.prototype.toString.call(vm.run(`/a/g`)), '[object RegExp]'); + assert.strictEqual(Object.prototype.toString.call(vm.run(`new String`)), '[object String]'); + assert.strictEqual(Object.prototype.toString.call(vm.run(`new Number`)), '[object Number]'); + assert.strictEqual(Object.prototype.toString.call(vm.run(`new Boolean`)), '[object Boolean]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)([]), '[object Array]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new Date), '[object Date]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new RangeError), '[object Error]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(/a/g), '[object RegExp]'); + assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new String), '[object String]'); + assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new Number), '[object Number]'); + assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new Boolean), '[object Boolean]'); + + assert.strictEqual(typeof vm.run(`new String`), 'object'); + assert.strictEqual(typeof vm.run(`new Number`), 'object'); + assert.strictEqual(typeof vm.run(`new Boolean`), 'object'); + assert.strictEqual(vm.run(`((obj) => typeof obj)`)(new String), 'object'); + assert.strictEqual(vm.run(`((obj) => typeof obj)`)(new Number), 'object'); + assert.strictEqual(vm.run(`((obj) => typeof obj)`)(new Boolean), 'object'); let o = vm.run('let x = {a: test.date, b: test.date};x'); assert.strictEqual(vm.run('x.valueOf().a instanceof Date'), true); @@ -160,15 +173,15 @@ describe('contextify', () => { it('string', () => { assert.strictEqual(vm.run('(test.string).constructor === String'), true); - assert.strictEqual(vm.run("typeof(test.stringO) === 'string' && test.string.valueOf instanceof Object"), true); + assert.strictEqual(vm.run("typeof(test.string) === 'string' && test.string.valueOf instanceof Object"), true); }); it('number', () => { - assert.strictEqual(vm.run("typeof(test.numberO) === 'number' && test.number.valueOf instanceof Object"), true); + assert.strictEqual(vm.run("typeof(test.number) === 'number' && test.number.valueOf instanceof Object"), true); }); it('boolean', () => { - assert.strictEqual(vm.run("typeof(test.booleanO) === 'boolean' && test.boolean.valueOf instanceof Object"), true); + assert.strictEqual(vm.run("typeof(test.boolean) === 'boolean' && test.boolean.valueOf instanceof Object"), true); }); it('date', () => { @@ -295,7 +308,13 @@ describe('VM', () => { }); it('globals', () => { + const dyn = {}; + vm.setGlobal('dyn', dyn); + vm.setGlobals({dyns: dyn}); assert.equal(vm.run('round(1.5)'), 2); + assert.equal(vm.getGlobal('dyn'), dyn); + assert.equal(vm.sandbox.dyn, dyn); + assert.equal(vm.sandbox.dyns, dyn); }); it('errors', () => { @@ -863,6 +882,10 @@ describe('precompiled scripts', () => { assert.ok('number' === typeof val1 && 'number' === typeof val2); assert.ok( val1 === 0 && val2 === 1); assert.throws(() => failScript.compile(), /SyntaxError/); + assert.ok(Object.keys(failScript).includes('code')); + assert.ok(Object.keys(failScript).includes('filename')); + assert.ok(Object.keys(failScript).includes('compiler')); + assert.ok(!Object.keys(failScript).includes('_code')); }); }); From 410b0f653d2c1eebd48d75d4322b9d5e3ed27781 Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Mon, 7 Oct 2019 00:44:08 +0200 Subject: [PATCH 2/6] Too much copy paste. --- lib/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.js b/lib/main.js index 914500a..a6d723a 100644 --- a/lib/main.js +++ b/lib/main.js @@ -12,7 +12,7 @@ */ /** - * This callback will be called to transform a script to JavaScript. + * This callback will be called to resolve a module if it couldn't be found. * * @callback resolveCallback * @param {string} moduleName - Name of the module to resolve. From c528ff53a24266635a1b17f21275b40ff9ce6dc6 Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Mon, 7 Oct 2019 10:06:20 +0200 Subject: [PATCH 3/6] Updated typescript definitions --- index.d.ts | 98 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4c3a53b..714aa09 100644 --- a/index.d.ts +++ b/index.d.ts @@ -69,23 +69,44 @@ export interface NodeVMOptions extends VMOptions { /** `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script. */ wrapper?: "commonjs" | "none"; /** File extensions that the internal module resolver should accept. */ - sourceExtensions?: string[] + sourceExtensions?: string[]; } /** - * A VM with behavior more similar to running inside Node. + * VM is a simple sandbox, without `require` feature, to synchronously run an untrusted code. + * Only JavaScript built-in objects + Buffer are available. Scheduling functions + * (`setInterval`, `setTimeout` and `setImmediate`) are not available by default. */ -export class NodeVM extends EventEmitter { - constructor(options?: NodeVMOptions); +export class VM { + constructor(options?: VMOptions); + /** Direct access to the global sandbox object */ + readonly sandbox: any; + /** Timeout to use for the run methods */ + timeout?: number; /** Runs the code */ - run(js: string, path: string): any; + run(js: string, path?: string): any; /** Runs the VMScript object */ - run(script: VMScript, path?: string): any; - + run(script: VMScript): any; + /** Runs the code in the specific file */ + runFile(filename: string): any; + /** Loads all the values into the global object with the same names */ + setGlobals(values: any): this; + /** Make a object visible as a global with a specific name */ + setGlobal(name: string, value: any): this; + /** Get the global object with the specific name */ + getGlobal(name: string): any; /** Freezes the object inside VM making it read-only. Not available for primitive values. */ - freeze(object: any, name: string): any; - /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. */ - protect(object: any, name: string): any; + freeze(object: any, name?: string): any; + /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values */ + protect(object: any, name?: string): any; +} + +/** + * A VM with behavior more similar to running inside Node. + */ +export class NodeVM extends EventEmitter implements VM { + constructor(options?: NodeVMOptions); + /** Require a module in VM and return it's exports. */ require(module: string): any; @@ -96,7 +117,7 @@ export class NodeVM extends EventEmitter { * @param {string} [filename] File name (used in stack traces only). * @param {Object} [options] VM options. */ - static code(script: string, filename: string, options: NodeVMOptions): NodeVM; + static code(script: string, filename?: string, options?: NodeVMOptions): any; /** * Create NodeVM and run script from file inside it. @@ -104,24 +125,28 @@ export class NodeVM extends EventEmitter { * @param {string} [filename] File name (used in stack traces only). * @param {Object} [options] VM options. */ - static file(filename: string, options: NodeVMOptions): NodeVM -} + static file(filename: string, options?: NodeVMOptions): any; -/** - * VM is a simple sandbox, without `require` feature, to synchronously run an untrusted code. - * Only JavaScript built-in objects + Buffer are available. Scheduling functions - * (`setInterval`, `setTimeout` and `setImmediate`) are not available by default. - */ -export class VM { - constructor(options?: VMOptions); + /** Direct access to the global sandbox object */ + readonly sandbox: any; + /** Only here because of implements VM. Does nothing. */ + timeout?: number; /** Runs the code */ - run(js: string): any; + run(js: string, path?: string): any; /** Runs the VMScript object */ run(script: VMScript): any; + /** Runs the code in the specific file */ + runFile(filename: string): any; + /** Loads all the values into the global object with the same names */ + setGlobals(values: any): this; + /** Make a object visible as a global with a specific name */ + setGlobal(name: string, value: any): this; + /** Get the global object with the specific name */ + getGlobal(name: string): any; /** Freezes the object inside VM making it read-only. Not available for primitive values. */ - freeze(object: any, name: string): any; + freeze(object: any, name?: string): any; /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values */ - protect(object: any, name: string): any; + protect(object: any, name?: string): any; } /** @@ -130,14 +155,29 @@ export class VM { * to any VM (context); rather, it is bound before each run, just for that run. */ export class VMScript { - constructor(code: string, path?: string, options?: { - lineOffset: number; - columnOffset: number; + constructor(code: string, path: string, options?: { + lineOffset?: number; + columnOffset?: number; + compiler?: "javascript" | "coffeescript" | CompilerFunction; + }); + constructor(code: string, options?: { + filename?: string, + lineOffset?: number; + columnOffset?: number; + compiler?: "javascript" | "coffeescript" | CompilerFunction; }); - /** Wraps the code */ - wrap(prefix: string, postfix: string): VMScript; + readonly code: string; + readonly filename: string; + readonly lineOffset: number; + readonly columnOffset: number; + readonly compiler: "javascript" | "coffeescript" | CompilerFunction; + /** + * Wraps the code + * @deprecated + */ + wrap(prefix: string, postfix: string): this; /** Compiles the code. If called multiple times, the code is only compiled once. */ - compile(): any; + compile(): this; } /** Custom Error class */ From 318f4a30d022aaff0a57a3fc50e4070d81fd4992 Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Tue, 8 Oct 2019 18:43:41 +0200 Subject: [PATCH 4/6] Fix typos. --- lib/main.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/main.js b/lib/main.js index a6d723a..f2502eb 100644 --- a/lib/main.js +++ b/lib/main.js @@ -44,7 +44,6 @@ function loadAndCompileScript(filename, prefix, suffix) { } /** - * * Cache where we can cache some things * * @private @@ -245,13 +244,12 @@ class VMScript { * Create VMScript instance. * * @public - * @param {String} code - Code to run. + * @param {string} 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. * @param {number} [options.lineOffset=0] - Passed to vm.Script options. * @param {number} [options.columnOffset=0] - Passed to vm.Script options. * @param {(string|compileCallback)} [options.compiler=javascript] - The compiler to use. - * @throws {TypeError} If code is a Symbol. * @throws {VMError} If the compiler is unknown or if coffee-script was requested but the module not found. */ constructor(code, options) { @@ -281,7 +279,7 @@ class VMScript { columnOffset = 0 } = useOptions; - // Throw if the compiler is unkown. + // Throw if the compiler is unknown. const resolvedCompiler = lookupCompiler(compiler); Object.defineProperties(this, { @@ -545,7 +543,7 @@ class VM extends EventEmitter { * @param {boolean} [options.wasm=true] - Allow to run wasm code.
* Only available for node v10+. * @param {boolean} [options.fixAsync=false] - Filters for async functions. - * @throws If the compiler is unknown. + * @throws {VMError} If the compiler is unknown. */ constructor(options = {}) { super(); @@ -560,14 +558,14 @@ class VM extends EventEmitter { const allowWasm = options.wasm !== false; const fixAsync = !!options.fixAsync; - // Early error if compiler can't be found. - const resolvedCompiler = lookupCompiler(compiler); - // 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 = vm.createContext(undefined, { codeGeneration: { @@ -790,8 +788,6 @@ class VM extends EventEmitter { * @since v3.8.5 * @param {string} filename - Filename of file to load and execute in a NodeVM. * @return {*} Result of executed code. - * @throws This will throw when there is a syntax error in the script. - * @throws This will throw when the script took to long and there is a timeout. * @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. @@ -913,12 +909,18 @@ class NodeVM extends VM { * @param {("commonjs"|"none")} [options.wrapper=commonjs] - commonjs to wrap script into CommonJS wrapper, * none to retrieve value returned by the script. * @param {string[]} [options.sourceExtensions=['js']] - Array of file extensions to treat as source code. + * @throws {VMError} If the compiler is unknown. */ constructor(options = {}) { - super({compiler: options.compiler, eval: options.eval, wasm: options.wasm}); - const sandbox = options.sandbox; + // Throw this early + if (sandbox && 'object' !== typeof sandbox) { + throw new VMError('Sandbox must be object.'); + } + + super({compiler: options.compiler, eval: options.eval, wasm: options.wasm}); + // defaults Object.defineProperty(this, 'options', {value: { console: options.console || 'inherit', @@ -942,10 +944,6 @@ class NodeVM extends VM { // prepare global sandbox if (sandbox) { - if ('object' !== typeof sandbox) { - throw new VMError('Sandbox must be object.'); - } - this.setGlobals(sandbox); } @@ -1094,7 +1092,7 @@ class NodeVM extends VM { * * @public * @static - * @param {String} filename - Filename of file to load and execute in a NodeVM. + * @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. From d24ccd3c83072c98b700d47df68e18f7f3b1469c Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Tue, 8 Oct 2019 18:52:38 +0200 Subject: [PATCH 5/6] Put optional string in double quotes. --- lib/main.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/main.js b/lib/main.js index f2502eb..c098def 100644 --- a/lib/main.js +++ b/lib/main.js @@ -246,10 +246,10 @@ class VMScript { * @public * @param {string} 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. + * @param {string} [options.filename="vm.js"] - Filename that shows up in any stack traces produced from this script. * @param {number} [options.lineOffset=0] - Passed to vm.Script options. * @param {number} [options.columnOffset=0] - Passed to vm.Script options. - * @param {(string|compileCallback)} [options.compiler=javascript] - The compiler to use. + * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. * @throws {VMError} If the compiler is unknown or if coffee-script was requested but the module not found. */ constructor(code, options) { @@ -537,7 +537,7 @@ class VM extends EventEmitter { * @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 {(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.
@@ -737,7 +737,7 @@ class VM extends EventEmitter { * * @public * @param {(string|VMScript)} code - Code to run. - * @param {string} [filename] - Filename that shows up in any stack traces produced from this script.
+ * @param {string} [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. @@ -884,12 +884,12 @@ class NodeVM extends VM { * @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 {(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 {("inherit"|"redirect"|"off")} [options.console=inherit] - Sets the behavior of the console in the sandbox. + * @param {("inherit"|"redirect"|"off")} [options.console="inherit"] - Sets the behavior of the console in the sandbox. * inherit to enable console, redirect to redirect to events, off to disable console. * @param {Object|boolean} [options.require=false] - Allow require inside the sandbox. * @param {(boolean|string[]|Object)} [options.require.external=false] - true, an array of allowed external modules or an object. @@ -899,16 +899,16 @@ class NodeVM extends VM { * @param {string[]} [options.require.builtin=[]] - Array of allowed builtin 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 builtin). - * @param {("host"|"sandbox")} [options.require.context=host] - host to require modules in host and proxy them to sandbox. + * @param {("host"|"sandbox")} [options.require.context="host"] - host to require modules in host and proxy them to sandbox. * sandbox to load, compile and require modules in sandbox. * Builtin modules except events 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 {boolean} [options.nesting=false] - Allow nesting of VMs. - * @param {("commonjs"|"none")} [options.wrapper=commonjs] - commonjs to wrap script into CommonJS wrapper, + * @param {("commonjs"|"none")} [options.wrapper="commonjs"] - commonjs to wrap script into CommonJS wrapper, * none to retrieve value returned by the script. - * @param {string[]} [options.sourceExtensions=['js']] - Array of file extensions to treat as source code. + * @param {string[]} [options.sourceExtensions=["js"]] - Array of file extensions to treat as source code. * @throws {VMError} If the compiler is unknown. */ constructor(options = {}) { From e8dd146387f6ddd4cb4bea62a8f1aecb870e325b Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Fri, 20 Mar 2020 20:28:54 +0100 Subject: [PATCH 6/6] Small fix --- lib/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.js b/lib/main.js index c098def..ab98a4d 100644 --- a/lib/main.js +++ b/lib/main.js @@ -597,7 +597,7 @@ class VM extends EventEmitter { _context: {value: _context}, _internal: {value: _internal}, _compiler: {value: resolvedCompiler}, - _fixAsync: fixAsync + _fixAsync: {value: fixAsync} }); if (fixAsync) {