Comparing version 3.8.4 to 3.9.0
@@ -0,1 +1,11 @@ | ||
v3.9.0 (2020-03-21) | ||
------------------- | ||
[new] Added vm.Script `lineOffset` and `columnOffset` options (azu) | ||
[new] Allow to specify a compiler per VMScript (XmiliaH) | ||
[new] Add option to disable async (XmiliaH) | ||
[new] Added allot of jsdoc (XmiliaH) | ||
[fix] Fix access to frozen or unconfigurable properties (XmiliaH) | ||
[fix] Double wrap Objects to prevent breakout via inspect (XmiliaH) | ||
[fix] Compile now compiles VM code (XmiliaH) | ||
v3.8.4 (2019-09-13) | ||
@@ -2,0 +12,0 @@ ------------------- |
@@ -72,19 +72,40 @@ import {EventEmitter} from 'events'; | ||
/** 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; | ||
} | ||
/** 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; | ||
/** | ||
* 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. */ | ||
@@ -100,3 +121,3 @@ require(module: string): any; | ||
*/ | ||
static code(script: string, filename: string, options: NodeVMOptions): NodeVM; | ||
static code(script: string, filename?: string, options?: NodeVMOptions): any; | ||
@@ -109,20 +130,24 @@ /** | ||
*/ | ||
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; | ||
} | ||
@@ -136,7 +161,25 @@ | ||
export class VMScript { | ||
constructor(code: string, path?: string); | ||
/** Wraps the code */ | ||
wrap(prefix: string, postfix: string): VMScript; | ||
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; | ||
}); | ||
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; | ||
} | ||
@@ -143,0 +186,0 @@ |
@@ -60,2 +60,27 @@ /* global host */ | ||
const SHARED_ARROW = ()=>{}; | ||
function SHARED_FUNC() {} | ||
const SHARED_ARRAY = []; | ||
const SHARED_OBJECT = {__proto__: null}; | ||
function getBaseObject(obj) { | ||
if (typeof obj === 'function') { | ||
try { | ||
// eslint-disable-next-line no-new | ||
new new host.Proxy(obj, { | ||
__proto__: null, | ||
construct() { | ||
return this; | ||
} | ||
})(); | ||
} catch (e) { | ||
return SHARED_ARROW; | ||
} | ||
return SHARED_FUNC; | ||
} else if (host.Array.isArray(obj)) { | ||
return SHARED_ARRAY; | ||
} | ||
return SHARED_OBJECT; | ||
} | ||
/** | ||
@@ -88,64 +113,2 @@ * VMError definition. | ||
/* | ||
* Proxy Helper | ||
* | ||
* Here we track Proxy creations so that we know for every proxy in the VM the | ||
* target. If the Proxy is given to decontextify we are going to lookup | ||
* the target and unsing this non proxy as target for the decontextify proxy. | ||
* | ||
*/ | ||
const ProxyHelper = host.Object.create(null); | ||
// Marker for revoked proxy objects | ||
ProxyHelper.revoked = 'Revoked'; | ||
// Tracks for every proxy the target. | ||
ProxyHelper.tracker = new host.WeakMap(); | ||
// Gets the target of a proxy recursively until target is not any more a proxy | ||
ProxyHelper.getTarget = (proxy) => { | ||
let obj = proxy; | ||
let next; | ||
while ((next = ProxyHelper.tracker.get(obj))!==undefined) { | ||
obj = next; | ||
} | ||
// Target could be revoked. | ||
if (obj === ProxyHelper.revoked) { | ||
obj = host.Object.create(null); | ||
} | ||
return obj; | ||
}; | ||
// This is not so nice, I would prefer globalThis.Proxy but globalThis is relatively new | ||
Proxy = ((ProxyFunc) => { | ||
// Handle Proxy.revocable() | ||
const ProxyRevocableHandler = host.Object.create(null); | ||
ProxyRevocableHandler.apply = (target, thiz, args) => { | ||
const proxyTarget = args[0]; | ||
const ret = local.Reflect.apply(target, thiz, args); | ||
const proxy = ret.proxy; | ||
ProxyHelper.tracker.set(proxy, proxyTarget); | ||
const revokeHandler = host.Object.create(null); | ||
revokeHandler.apply = (rTarget, rThiz, rArgs) => { | ||
const rRet = local.Reflect.apply(rTarget, rThiz, rArgs); | ||
ProxyHelper.tracker.set(proxy, ProxyHelper.revoked); | ||
return rRet; | ||
}; | ||
ret.revoke = new host.Proxy(ret.revoke, revokeHandler); | ||
return ret; | ||
}; | ||
ProxyFunc.revocable = new host.Proxy(Proxy.revocable, ProxyRevocableHandler); | ||
// Handle new Proxy() | ||
const ProxyHandler = host.Object.create(null); | ||
ProxyHandler.construct = (target, args, newTarget) => { | ||
const proxyTarget = args[0]; | ||
const proxy = local.Reflect.construct(target, args, newTarget); | ||
ProxyHelper.tracker.set(proxy, proxyTarget); | ||
return proxy; | ||
}; | ||
return new host.Proxy(ProxyFunc, ProxyHandler); | ||
})(Proxy); | ||
/** | ||
@@ -402,6 +365,37 @@ * Decontextify. | ||
const proxy = new host.Proxy(ProxyHelper.getTarget(object), host.Object.assign(base, traps, deepTraps)); | ||
Decontextify.proxies.set(object, proxy); | ||
host.Object.assign(base, traps, deepTraps); | ||
let shallow; | ||
if (host.Array.isArray(object)) { | ||
const origGet = base.get; | ||
shallow = { | ||
__proto__: null, | ||
ownKeys: base.ownKeys, | ||
get: origGet | ||
}; | ||
base.ownKeys = target => { | ||
try { | ||
const keys = local.Reflect.ownKeys(object); | ||
// Do this hack so that console.log(decontextify([1,2,3])) doesn't write the properties twice | ||
// a la [1,2,3,'0':1,'1':2,'2':3] | ||
return Decontextify.value(keys.filter(key=>typeof key!=='string' || !key.match(/^\d+$/))); | ||
} catch (e) { | ||
throw Decontextify.value(e); | ||
} | ||
}; | ||
base.get = (target, key, receiver) => { | ||
if (key === host.Symbol.toStringTag) return; | ||
return origGet(target, key, receiver); | ||
}; | ||
} else { | ||
shallow = SHARED_OBJECT; | ||
} | ||
const proxy = new host.Proxy(getBaseObject(object), base); | ||
Decontextified.set(proxy, object); | ||
return proxy; | ||
// We need two proxys since nodes inspect just removes one. | ||
const proxy2 = new host.Proxy(proxy, shallow); | ||
Decontextify.proxies.set(object, proxy2); | ||
Decontextified.set(proxy2, object); | ||
return proxy2; | ||
}; | ||
@@ -422,5 +416,5 @@ Decontextify.value = (value, traps, deepTraps, flags, mock) => { | ||
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'); | ||
@@ -731,3 +725,3 @@ } else if (instanceOf(value, RangeError)) { return Decontextify.instance(value, host.RangeError, deepTraps, flags, 'Error'); | ||
const proxy = new host.Proxy(object, host.Object.assign(base, traps, deepTraps)); | ||
const proxy = new host.Proxy(getBaseObject(object), host.Object.assign(base, traps, deepTraps)); | ||
Contextify.proxies.set(object, proxy); | ||
@@ -751,5 +745,5 @@ Contextified.set(proxy, object); | ||
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'); | ||
@@ -791,5 +785,18 @@ } else if (instanceOf(value, host.RangeError)) { return Contextify.instance(value, RangeError, deepTraps, flags, 'Error'); | ||
}; | ||
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) => { | ||
@@ -801,2 +808,7 @@ return Contextify.value(value, null, FROZEN_TRAPS, null, mock); | ||
}; | ||
Contextify.connect = (outer, inner) => { | ||
Decontextified.set(outer, inner); | ||
Contextified.set(inner, outer); | ||
}; | ||
Contextify.makeModule = ()=>({exports: {}}); | ||
@@ -810,5 +822,16 @@ const BufferMock = host.Object.create(null); | ||
}; | ||
const BufferOverride = host.Object.create(null); | ||
BufferOverride.inspect = function inspect(recurseTimes, ctx) { | ||
// Mimic old behavior, could throw but didn't pass a test. | ||
const max = host.INSPECT_MAX_BYTES; | ||
const actualMax = Math.min(max, this.length); | ||
const remaining = this.length - max; | ||
let str = this.hexSlice(0, actualMax).replace(/(.{2})/g, '$1 ').trim(); | ||
if (remaining > 0) str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`; | ||
return `<${this.constructor.name} ${str}>`; | ||
}; | ||
const LocalBuffer = global.Buffer = Contextify.readonly(host.Buffer, BufferMock); | ||
Contextify.connect(host.Buffer.prototype.inspect, BufferOverride.inspect); | ||
const exportsMap = host.Object.create(null); | ||
@@ -818,3 +841,4 @@ exportsMap.Contextify = Contextify; | ||
exportsMap.Buffer = LocalBuffer; | ||
exportsMap.sandbox = Decontextify.value(global); | ||
return exportsMap; |
1268
lib/main.js
@@ -5,2 +5,20 @@ /* eslint-disable global-require, no-use-before-define */ | ||
/** | ||
* 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 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'); | ||
@@ -10,6 +28,96 @@ const vm = require('vm'); | ||
const {EventEmitter} = require('events'); | ||
const {INSPECT_MAX_BYTES} = require('buffer'); | ||
const _compileToJS = function compileToJS(code, compiler, filename) { | ||
if ('function' === typeof compiler) return compiler(code, filename); | ||
/** | ||
* 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 (!CACHE.coffeeScriptCompiler) { | ||
try { | ||
const coffeeScript = require('coffee-script'); | ||
CACHE.coffeeScriptCompiler = (code, filename) => { | ||
return coffeeScript.compile(code, {header: false, bare: true}); | ||
}; | ||
} catch (e) { | ||
throw new VMError('Coffee-Script compiler is not installed.'); | ||
} | ||
} | ||
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) { | ||
@@ -20,8 +128,3 @@ case 'coffeescript': | ||
case 'text/coffeescript': | ||
try { | ||
return require('coffee-script').compile(code, {header: false, bare: true}); | ||
} catch (ex) { | ||
throw new VMError('Coffee-Script compiler is not installed.'); | ||
} | ||
return getCoffeeScriptCompiler(); | ||
case 'javascript': | ||
@@ -31,8 +134,7 @@ case 'java-script': | ||
case 'text/javascript': | ||
return code; | ||
return jsCompiler; | ||
default: | ||
throw new VMError(`Unsupported compiler '${compiler}'.`); | ||
} | ||
}; | ||
} | ||
@@ -42,33 +144,219 @@ /** | ||
* | ||
* @class | ||
* @public | ||
*/ | ||
class VMScript { | ||
class VMScript { | ||
/** | ||
* The script code with wrapping. If set will invalidate the cache.<br> | ||
* 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 <code>null</code>. | ||
* | ||
* @private | ||
* @member {?vm.Script} _compiledVM | ||
* @memberOf VMScript# | ||
*/ | ||
/** | ||
* The compiled vm.Script for the NodeVM or if not compiled <code>null</code>. | ||
* | ||
* @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. | ||
* @return {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 {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 {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 = {}; | ||
} | ||
constructor(code, filename) { | ||
this._code = String(code); | ||
this.filename = filename || 'vm.js'; | ||
this._prefix = ''; | ||
this._suffix = ''; | ||
this._compiledVM = null; | ||
this._compiledNodeVM = null; | ||
const { | ||
compiler = 'javascript', | ||
lineOffset = 0, | ||
columnOffset = 0 | ||
} = useOptions; | ||
// Throw if the compiler is unknown. | ||
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.<br> | ||
* This will replace the old wrapping.<br> | ||
* 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; | ||
@@ -83,11 +371,11 @@ this._prefix = strPrefix; | ||
/** | ||
* Noop. | ||
* We need to change the code depending whether it is run in VM or NodeVM. | ||
* This function cannot decide for which to compile. | ||
* Compile this script. <br> | ||
* This is useful to detect syntax errors in the script. | ||
* | ||
* @deprecated Will be done on first run | ||
* @return {VMScript} | ||
* @public | ||
* @return {this} This for chaining. | ||
* @throws {SyntaxError} If there is a syntax error in the script. | ||
*/ | ||
compile() { | ||
this._compileVM(); | ||
return this; | ||
@@ -97,168 +385,340 @@ } | ||
/** | ||
* 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; | ||
_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 | ||
}); | ||
} | ||
/** | ||
* For backwards compatibility. | ||
* Will invalidate the code cache. | ||
* Will return the cached version of the script intended for VM or compile it. | ||
* | ||
* @param {String} newCode The new code to run. | ||
* @private | ||
* @return {vm.Script} The compiled script | ||
* @throws {SyntaxError} If there is a syntax error in the script. | ||
*/ | ||
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; | ||
_compileVM() { | ||
let script = this._compiledVM; | ||
if (!script) { | ||
this._compiledVM = script = this._compile('', ''); | ||
} | ||
return script; | ||
} | ||
/** | ||
* Will compile the code for VM 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. | ||
*/ | ||
_compileVM() { | ||
if (this._compiledVM) return this; | ||
_compileNodeVM() { | ||
let script = this._compiledNodeVM; | ||
if (!script) { | ||
this._compiledNodeVM = script = this._compile('(function (exports, require, module, __filename, __dirname) { ', '\n})'); | ||
} | ||
return script; | ||
} | ||
this._compiledVM = new vm.Script(this._prefix + this._code + this._suffix, { | ||
filename: this.filename, | ||
displayErrors: false | ||
}); | ||
} | ||
return this; | ||
} | ||
/** | ||
* | ||
* This callback will be called and has a specific time to finish.<br> | ||
* No parameters will be supplied.<br> | ||
* If parameters are required, use a closure. | ||
* | ||
* @private | ||
* @callback runWithTimeout | ||
* @return {*} | ||
* | ||
*/ | ||
/** | ||
* Will compile the code for NodeVM and cache it | ||
* | ||
* @return {VMScript} | ||
*/ | ||
_compileNodeVM() { | ||
if (this._compiledNodeVM) return this; | ||
this._compiledNodeVM = new vm.Script('(function (exports, require, module, __filename, __dirname) { ' + | ||
this._prefix + this._code + this._suffix + '\n})', { | ||
filename: this.filename, | ||
/** | ||
* 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) { | ||
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 | ||
}); | ||
return this; | ||
} | ||
ctx.fn = fn; | ||
try { | ||
return script.runInContext(ctx, { | ||
displayErrors: false, | ||
timeout | ||
}); | ||
} finally { | ||
ctx.fn = null; | ||
} | ||
} | ||
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(), | ||
exp: new VMScript('({exports: {}})')._compileVM() | ||
}; | ||
/** | ||
* Class VM. | ||
* | ||
* @property {Object} options VM options. | ||
* @public | ||
*/ | ||
class VM extends EventEmitter { | ||
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)().<br> | ||
* Only available for node v10+. | ||
* @param {boolean} [options.wasm=true] - Allow to run wasm code.<br> | ||
* Only available for node v10+. | ||
* @param {boolean} [options.fixAsync=false] - Filters for async functions. | ||
* @throws {VMError} If the compiler is unknown. | ||
*/ | ||
constructor(options = {}) { | ||
super(); | ||
// defaults | ||
this.options = { | ||
timeout: options.timeout, | ||
sandbox: options.sandbox, | ||
compiler: options.compiler || 'javascript', | ||
eval: options.eval === false ? false : true, | ||
wasm: options.wasm === false ? false : true | ||
}; | ||
// Read all options | ||
const { | ||
timeout, | ||
sandbox, | ||
compiler = 'javascript' | ||
} = options; | ||
const allowEval = options.eval !== false; | ||
const allowWasm = options.wasm !== false; | ||
const 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 | ||
}; | ||
// Early error if sandbox is not an object. | ||
if (sandbox && 'object' !== typeof sandbox) { | ||
throw new VMError('Sandbox must be object.'); | ||
} | ||
this._context = vm.createContext(undefined, { | ||
// 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: { | ||
strings: this.options.eval, | ||
wasm: this.options.wasm | ||
strings: allowEval, | ||
wasm: allowWasm | ||
} | ||
}); | ||
Reflect.defineProperty(this, '_internal', { | ||
value: SCRIPT_CACHE.cf._compiledVM.runInContext(this._context, { | ||
filename: SCRIPT_CACHE.cf.filename, | ||
displayErrors: false | ||
}).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: {value: fixAsync} | ||
}); | ||
// prepare global sandbox | ||
if (this.options.sandbox) { | ||
if ('object' !== typeof this.options.sandbox) { | ||
throw new VMError('Sandbox must be object.'); | ||
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: CACHE.getGlobalScript.runInContext(this._context, DEFAULT_RUN_OPTIONS), | ||
Contextify: this._internal.Contextify, | ||
host: HOST | ||
}; | ||
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); | ||
} | ||
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); | ||
} | ||
// prepare global sandbox | ||
if (sandbox) { | ||
this.setGlobals(sandbox); | ||
} | ||
} | ||
/** | ||
* 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; | ||
@@ -270,11 +730,11 @@ } | ||
* | ||
* @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.setGlobal(globalName, value); | ||
return value; | ||
@@ -286,147 +746,222 @@ } | ||
* | ||
* @param {String} code Code to run. | ||
* @public | ||
* @param {(string|VMScript)} code - Code to run. | ||
* @param {string} [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. | ||
* @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) { | ||
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 | ||
}); | ||
} | ||
run(code) { | ||
if (this.options.compiler !== 'javascript') { | ||
code = _compileToJS(code, this.options.compiler); | ||
if (!this.timeout) { | ||
// If no timeout is given, directly run the script. | ||
try { | ||
return this._internal.Decontextify.value(script.runInContext(this._context, DEFAULT_RUN_OPTIONS)); | ||
} catch (e) { | ||
throw this._internal.Decontextify.value(e); | ||
} | ||
} | ||
const script = code instanceof VMScript ? code : new VMScript(code); | ||
script._compileVM(); | ||
return doWithTimeout(()=>{ | ||
try { | ||
return this._internal.Decontextify.value(script.runInContext(this._context, DEFAULT_RUN_OPTIONS)); | ||
} catch (e) { | ||
throw this._internal.Decontextify.value(e); | ||
} | ||
}, this.timeout); | ||
} | ||
try { | ||
return this._internal.Decontextify.value(script._compiledVM.runInContext(this._context, { | ||
filename: script.filename, | ||
displayErrors: false, | ||
timeout: this.options.timeout | ||
})); | ||
} catch (e) { | ||
throw this._internal.Decontextify.value(e); | ||
/** | ||
* 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 {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 <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. | ||
* | ||
* @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.<br> | ||
* | ||
* 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.<br> | ||
* | ||
* 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)().<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] - 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"] - <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 {boolean} [options.nesting=false] - 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. | ||
* @throws {VMError} If the compiler is unknown. | ||
*/ | ||
constructor(options = {}) { | ||
super(); | ||
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 | ||
this.options = { | ||
sandbox: options.sandbox, | ||
Object.defineProperty(this, 'options', {value: { | ||
console: options.console || 'inherit', | ||
require: options.require || false, | ||
compiler: 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 | ||
}; | ||
if (this.options.nesting) { | ||
host.VM = VM; | ||
host.NodeVM = NodeVM; | ||
let sandboxScript = CACHE.sandboxScript; | ||
if (!sandboxScript) { | ||
CACHE.sandboxScript = sandboxScript = loadAndCompileScript(`${__dirname}/sandbox.js`, | ||
'(function (vm, host, Contextify, Decontextify, Buffer) { ', '\n})'); | ||
} | ||
this._context = vm.createContext(undefined, { | ||
codeGeneration: { | ||
strings: this.options.eval, | ||
wasm: this.options.wasm | ||
} | ||
}); | ||
const closure = sandboxScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); | ||
Object.defineProperty(this, '_internal', { | ||
value: SCRIPT_CACHE.cf._compiledVM.runInContext(this._context, { | ||
filename: SCRIPT_CACHE.cf.filename, | ||
displayErrors: false | ||
}).call(this._context, require, host) | ||
}); | ||
const closure = SCRIPT_CACHE.sb._compiledVM.runInContext(this._context, { | ||
filename: SCRIPT_CACHE.sb.filename, | ||
displayErrors: false | ||
}); | ||
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) { | ||
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); | ||
} | ||
} | ||
if (sandbox) { | ||
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]; | ||
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); | ||
} | ||
for (let i = 0, l = this.options.require.import.length; i < l; i++) { | ||
this.require(this.options.require.import[i]); | ||
} | ||
} | ||
@@ -436,9 +971,15 @@ } | ||
/** | ||
* @deprecated | ||
* @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.apply(args); | ||
return method(...args); | ||
} else { | ||
@@ -450,38 +991,9 @@ 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) { | ||
@@ -497,37 +1009,50 @@ return this.run(`module.exports = require('${module}');`, 'vm.js'); | ||
* | ||
* @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.<br> | ||
* 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) { | ||
if (this.options.compiler !== 'javascript') { | ||
code = _compileToJS(code, this.options.compiler, filename); | ||
} | ||
let dirname; | ||
let returned; | ||
let resolvedFilename; | ||
let script; | ||
if (filename) { | ||
filename = pa.resolve(filename); | ||
dirname = pa.dirname(filename); | ||
if (code instanceof VMScript) { | ||
script = code._compileNodeVM(); | ||
resolvedFilename = pa.resolve(code.filename); | ||
dirname = pa.dirname(resolvedFilename); | ||
} else { | ||
filename = 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._compiledVM.runInContext(this._context, { | ||
displayErrors: false | ||
}); | ||
const wrapper = this.options.wrapper; | ||
const module = this._internal.Contextify.makeModule(); | ||
const script = code instanceof VMScript ? code : new VMScript(code, filename); | ||
script._compileNodeVM(); | ||
try { | ||
const closure = script._compiledNodeVM.runInContext(this._context, { | ||
filename: script.filename, | ||
displayErrors: false | ||
}); | ||
const closure = script.runInContext(this._context, DEFAULT_RUN_OPTIONS); | ||
returned = closure.call(this._context, module.exports, this._prepareRequire(dirname), module, filename, dirname); | ||
const returned = closure.call(this._context, module.exports, this._prepareRequire(dirname), module, resolvedFilename, dirname); | ||
return this._internal.Decontextify.value(wrapper === 'commonjs' ? module.exports : returned); | ||
} catch (e) { | ||
@@ -537,7 +1062,2 @@ 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); | ||
} | ||
} | ||
@@ -548,18 +1068,26 @@ | ||
* | ||
* @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 <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; | ||
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; | ||
} | ||
@@ -571,3 +1099,5 @@ | ||
return new NodeVM(options).run(script, filename); | ||
const resolvedFilename = typeof unresolvedFilename === 'string' ? pa.resolve(unresolvedFilename) : undefined; | ||
return new NodeVM(options).run(script, resolvedFilename); | ||
} | ||
@@ -578,19 +1108,24 @@ | ||
* | ||
* @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); | ||
} | ||
@@ -602,16 +1137,13 @@ } | ||
* | ||
* @class | ||
* @public | ||
* @extends {Error} | ||
* @property {String} stack Call stack. | ||
* @property {String} message Error message. | ||
*/ | ||
class VMError extends Error { | ||
class VMError extends Error { | ||
/** | ||
* Create VMError instance. | ||
* | ||
* @param {String} message Error message. | ||
* @return {VMError} | ||
* @public | ||
* @param {string} message - Error message. | ||
*/ | ||
constructor(message) { | ||
@@ -626,2 +1158,48 @@ super(message); | ||
/** | ||
* 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; | ||
@@ -628,0 +1206,0 @@ exports.NodeVM = NodeVM; |
@@ -62,5 +62,3 @@ /* eslint-disable no-shadow, no-invalid-this */ | ||
let contents = fs.readFileSync(filename, 'utf8'); | ||
if (typeof vm.options.compiler === 'function') { | ||
contents = vm.options.compiler(contents, filename); | ||
} | ||
contents = vm._compiler(contents, filename); | ||
@@ -67,0 +65,0 @@ const code = `(function (exports, require, module, __filename, __dirname) { 'use strict'; ${contents} \n});`; |
const match = (wildcard, s) => { | ||
const regexString = wildcard.replace(/\*/, '\\S*').replace(/\?/g, '.'); | ||
const regexString = wildcard.replace(/\*/g, '\\S*').replace(/\?/g, '.'); | ||
const regex = new RegExp(regexString); | ||
@@ -4,0 +4,0 @@ return regex.test(s); |
@@ -16,3 +16,3 @@ { | ||
], | ||
"version": "3.8.4", | ||
"version": "3.9.0", | ||
"main": "index.js", | ||
@@ -25,3 +25,3 @@ "repository": "github:patriksimek/vm2", | ||
"eslint-config-integromat": "^1.5.0", | ||
"mocha": "^5.2.0" | ||
"mocha": "^6.2.2" | ||
}, | ||
@@ -28,0 +28,0 @@ "engines": { |
@@ -13,3 +13,2 @@ # vm2 [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Package Quality][quality-image]][quality-url] [![Travis CI][travis-image]][travis-url] [![Known Vulnerabilities][snyk-image]][snyk-url] | ||
* You can securely call methods and exchange data and callbacks between sandboxes | ||
* Is immune to `while (true) {}` (see docs) | ||
* Is immune to all known methods of attacks | ||
@@ -101,3 +100,3 @@ * Transpilers support | ||
**IMPORTANT**: Timeout is only effective on synchronous code you run through `run`. Timeout is NOT effective on any method returned by VM. | ||
**IMPORTANT**: Timeout is only effective on synchronous code you run through `run`. Timeout is NOT effective on any method returned by VM. There're some situations when timeout doesn't work - see [#244](https://github.com/patriksimek/vm2/pull/244). | ||
@@ -104,0 +103,0 @@ ```javascript |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
108778
15
2583
387