nodevm.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. 'use strict';
  2. /**
  3. * This callback will be called to resolve a module if it couldn't be found.
  4. *
  5. * @callback resolveCallback
  6. * @param {string} moduleName - Name of the module used to resolve.
  7. * @param {string} dirname - Name of the current directory.
  8. * @return {(string|undefined)} The file or directory to use to load the requested module.
  9. */
  10. /**
  11. * This callback will be called to require a module instead of node's require.
  12. *
  13. * @callback customRequire
  14. * @param {string} moduleName - Name of the module requested.
  15. * @return {*} The required module object.
  16. */
  17. const fs = require('fs');
  18. const pa = require('path');
  19. const {
  20. Script
  21. } = require('vm');
  22. const {
  23. VMError
  24. } = require('./bridge');
  25. const {
  26. VMScript,
  27. MODULE_PREFIX,
  28. STRICT_MODULE_PREFIX,
  29. MODULE_SUFFIX
  30. } = require('./script');
  31. const {
  32. transformer
  33. } = require('./transformer');
  34. const {
  35. VM
  36. } = require('./vm');
  37. const {
  38. resolverFromOptions
  39. } = require('./resolver-compat');
  40. const objectDefineProperty = Object.defineProperty;
  41. const objectDefineProperties = Object.defineProperties;
  42. /**
  43. * Host objects
  44. *
  45. * @private
  46. */
  47. const HOST = Object.freeze({
  48. __proto__: null,
  49. version: parseInt(process.versions.node.split('.')[0]),
  50. process,
  51. console,
  52. setTimeout,
  53. setInterval,
  54. setImmediate,
  55. clearTimeout,
  56. clearInterval,
  57. clearImmediate
  58. });
  59. /**
  60. * Compile a script.
  61. *
  62. * @private
  63. * @param {string} filename - Filename of the script.
  64. * @param {string} script - Script.
  65. * @return {vm.Script} The compiled script.
  66. */
  67. function compileScript(filename, script) {
  68. return new Script(script, {
  69. __proto__: null,
  70. filename,
  71. displayErrors: false
  72. });
  73. }
  74. let cacheSandboxScript = null;
  75. let cacheMakeNestingScript = null;
  76. const NESTING_OVERRIDE = Object.freeze({
  77. __proto__: null,
  78. vm2: vm2NestingLoader
  79. });
  80. /**
  81. * Event caused by a <code>console.debug</code> call if <code>options.console="redirect"</code> is specified.
  82. *
  83. * @public
  84. * @event NodeVM."console.debug"
  85. * @type {...*}
  86. */
  87. /**
  88. * Event caused by a <code>console.log</code> call if <code>options.console="redirect"</code> is specified.
  89. *
  90. * @public
  91. * @event NodeVM."console.log"
  92. * @type {...*}
  93. */
  94. /**
  95. * Event caused by a <code>console.info</code> call if <code>options.console="redirect"</code> is specified.
  96. *
  97. * @public
  98. * @event NodeVM."console.info"
  99. * @type {...*}
  100. */
  101. /**
  102. * Event caused by a <code>console.warn</code> call if <code>options.console="redirect"</code> is specified.
  103. *
  104. * @public
  105. * @event NodeVM."console.warn"
  106. * @type {...*}
  107. */
  108. /**
  109. * Event caused by a <code>console.error</code> call if <code>options.console="redirect"</code> is specified.
  110. *
  111. * @public
  112. * @event NodeVM."console.error"
  113. * @type {...*}
  114. */
  115. /**
  116. * Event caused by a <code>console.dir</code> call if <code>options.console="redirect"</code> is specified.
  117. *
  118. * @public
  119. * @event NodeVM."console.dir"
  120. * @type {...*}
  121. */
  122. /**
  123. * Event caused by a <code>console.trace</code> call if <code>options.console="redirect"</code> is specified.
  124. *
  125. * @public
  126. * @event NodeVM."console.trace"
  127. * @type {...*}
  128. */
  129. /**
  130. * Class NodeVM.
  131. *
  132. * @public
  133. * @extends {VM}
  134. * @extends {EventEmitter}
  135. */
  136. class NodeVM extends VM {
  137. /**
  138. * Create a new NodeVM instance.<br>
  139. *
  140. * Unlike VM, NodeVM lets you use require same way like in regular node.<br>
  141. *
  142. * However, it does not use the timeout.
  143. *
  144. * @public
  145. * @param {Object} [options] - VM options.
  146. * @param {Object} [options.sandbox] - Objects that will be copied into the global object of the sandbox.
  147. * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use.
  148. * @param {boolean} [options.eval=true] - Allow the dynamic evaluation of code via eval(code) or Function(code)().<br>
  149. * Only available for node v10+.
  150. * @param {boolean} [options.wasm=true] - Allow to run wasm code.<br>
  151. * Only available for node v10+.
  152. * @param {("inherit"|"redirect"|"off")} [options.console="inherit"] - Sets the behavior of the console in the sandbox.
  153. * <code>inherit</code> to enable console, <code>redirect</code> to redirect to events, <code>off</code> to disable console.
  154. * @param {Object|boolean} [options.require=false] - Allow require inside the sandbox.
  155. * @param {(boolean|string[]|Object)} [options.require.external=false] - <b>WARNING: When allowing require the option <code>options.require.root</code>
  156. * should be set to restrict the script from requiring any module. Values can be true, an array of allowed external modules or an object.
  157. * @param {(string[])} [options.require.external.modules] - Array of allowed external modules. Also supports wildcards, so specifying ['@scope/*-ver-??],
  158. * for instance, will allow using all modules having a name of the form @scope/something-ver-aa, @scope/other-ver-11, etc.
  159. * @param {boolean} [options.require.external.transitive=false] - Boolean which indicates if transitive dependencies of external modules are allowed.
  160. * @param {string[]} [options.require.builtin=[]] - Array of allowed built-in modules, accepts ["*"] for all.
  161. * @param {(string|string[])} [options.require.root] - Restricted path(s) where local modules can be required. If omitted every path is allowed.
  162. * @param {Object} [options.require.mock] - Collection of mock modules (both external or built-in).
  163. * @param {("host"|"sandbox")} [options.require.context="host"] - <code>host</code> to require modules in host and proxy them to sandbox.
  164. * <code>sandbox</code> to load, compile and require modules in sandbox.
  165. * Builtin modules except <code>events</code> always required in host and proxied to sandbox.
  166. * @param {string[]} [options.require.import] - Array of modules to be loaded into NodeVM on start.
  167. * @param {resolveCallback} [options.require.resolve] - An additional lookup function in case a module wasn't
  168. * found in one of the traditional node lookup paths.
  169. * @param {customRequire} [options.require.customRequire=require] - Custom require to require host and built-in modules.
  170. * @param {boolean} [options.nesting=false] -
  171. * <b>WARNING: Allowing this is a security risk as scripts can create a NodeVM which can require any host module.</b>
  172. * Allow nesting of VMs.
  173. * @param {("commonjs"|"none")} [options.wrapper="commonjs"] - <code>commonjs</code> to wrap script into CommonJS wrapper,
  174. * <code>none</code> to retrieve value returned by the script.
  175. * @param {string[]} [options.sourceExtensions=["js"]] - Array of file extensions to treat as source code.
  176. * @param {string[]} [options.argv=[]] - Array of arguments passed to <code>process.argv</code>.
  177. * This object will not be copied and the script can change this object.
  178. * @param {Object} [options.env={}] - Environment map passed to <code>process.env</code>.
  179. * This object will not be copied and the script can change this object.
  180. * @param {boolean} [options.strict=false] - If modules should be loaded in strict mode.
  181. * @throws {VMError} If the compiler is unknown.
  182. */
  183. constructor(options = {}) {
  184. const {
  185. compiler,
  186. eval: allowEval,
  187. wasm,
  188. console: consoleType = 'inherit',
  189. require: requireOpts = false,
  190. nesting = false,
  191. wrapper = 'commonjs',
  192. sourceExtensions = ['js'],
  193. argv,
  194. env,
  195. strict = false,
  196. sandbox
  197. } = options;
  198. // Throw this early
  199. if (sandbox && 'object' !== typeof sandbox) {
  200. throw new VMError('Sandbox must be an object.');
  201. }
  202. super({__proto__: null, compiler: compiler, eval: allowEval, wasm});
  203. // This is only here for backwards compatibility.
  204. objectDefineProperty(this, 'options', {__proto__: null, value: {
  205. console: consoleType,
  206. require: requireOpts,
  207. nesting,
  208. wrapper,
  209. sourceExtensions,
  210. strict
  211. }});
  212. const resolver = resolverFromOptions(this, requireOpts, nesting && NESTING_OVERRIDE, this._compiler);
  213. objectDefineProperty(this, '_resolver', {__proto__: null, value: resolver});
  214. if (!cacheSandboxScript) {
  215. cacheSandboxScript = compileScript(`${__dirname}/setup-node-sandbox.js`,
  216. `(function (host, data) { ${fs.readFileSync(`${__dirname}/setup-node-sandbox.js`, 'utf8')}\n})`);
  217. }
  218. const closure = this._runScript(cacheSandboxScript);
  219. const extensions = {
  220. __proto__: null
  221. };
  222. const loadJS = (mod, filename) => resolver.loadJS(this, mod, filename);
  223. for (let i = 0; i < sourceExtensions.length; i++) {
  224. extensions['.' + sourceExtensions[i]] = loadJS;
  225. }
  226. if (!extensions['.json']) extensions['.json'] = (mod, filename) => resolver.loadJSON(this, mod, filename);
  227. if (!extensions['.node']) extensions['.node'] = (mod, filename) => resolver.loadNode(this, mod, filename);
  228. this.readonly(HOST);
  229. this.readonly(resolver);
  230. this.readonly(this);
  231. const {
  232. Module,
  233. jsonParse,
  234. createRequireForModule,
  235. requireImpl
  236. } = closure(HOST, {
  237. __proto__: null,
  238. argv,
  239. env,
  240. console: consoleType,
  241. vm: this,
  242. resolver,
  243. extensions
  244. });
  245. objectDefineProperties(this, {
  246. __proto__: null,
  247. _Module: {__proto__: null, value: Module},
  248. _jsonParse: {__proto__: null, value: jsonParse},
  249. _createRequireForModule: {__proto__: null, value: createRequireForModule},
  250. _requireImpl: {__proto__: null, value: requireImpl},
  251. _cacheRequireModule: {__proto__: null, value: null, writable: true}
  252. });
  253. resolver.init(this);
  254. // prepare global sandbox
  255. if (sandbox) {
  256. this.setGlobals(sandbox);
  257. }
  258. if (requireOpts && requireOpts.import) {
  259. if (Array.isArray(requireOpts.import)) {
  260. for (let i = 0, l = requireOpts.import.length; i < l; i++) {
  261. this.require(requireOpts.import[i]);
  262. }
  263. } else {
  264. this.require(requireOpts.import);
  265. }
  266. }
  267. }
  268. /**
  269. * @ignore
  270. * @deprecated Just call the method yourself like <code>method(args);</code>
  271. * @param {function} method - Function to invoke.
  272. * @param {...*} args - Arguments to pass to the function.
  273. * @return {*} Return value of the function.
  274. * @todo Can we remove this function? It even had a bug that would use args as this parameter.
  275. * @throws {*} Rethrows anything the method throws.
  276. * @throws {VMError} If method is not a function.
  277. * @throws {Error} If method is a class.
  278. */
  279. call(method, ...args) {
  280. if ('function' === typeof method) {
  281. return method(...args);
  282. } else {
  283. throw new VMError('Unrecognized method type.');
  284. }
  285. }
  286. /**
  287. * Require a module in VM and return it's exports.
  288. *
  289. * @public
  290. * @param {string} module - Module name.
  291. * @return {*} Exported module.
  292. * @throws {*} If the module couldn't be found or loading it threw an error.
  293. */
  294. require(module) {
  295. const path = this._resolver.pathResolve('.');
  296. let mod = this._cacheRequireModule;
  297. if (!mod || mod.path !== path) {
  298. const filename = this._resolver.pathConcat(path, '/vm.js');
  299. mod = new (this._Module)(filename, path);
  300. this._resolver.registerModule(mod, filename, path, null, false);
  301. this._cacheRequireModule = mod;
  302. }
  303. return this._requireImpl(mod, module, true);
  304. }
  305. /**
  306. * Run the code in NodeVM.
  307. *
  308. * First time you run this method, code is executed same way like in node's regular `require` - it's executed with
  309. * `module`, `require`, `exports`, `__dirname`, `__filename` variables and expect result in `module.exports'.
  310. *
  311. * @param {(string|VMScript)} code - Code to run.
  312. * @param {(string|Object)} [options] - Options map or filename.
  313. * @param {string} [options.filename="vm.js"] - Filename that shows up in any stack traces produced from this script.<br>
  314. * This is only used if code is a String.
  315. * @param {boolean} [options.strict] - If modules should be loaded in strict mode. Defaults to NodeVM options.
  316. * @param {("commonjs"|"none")} [options.wrapper] - <code>commonjs</code> to wrap script into CommonJS wrapper,
  317. * <code>none</code> to retrieve value returned by the script. Defaults to NodeVM options.
  318. * @return {*} Result of executed code.
  319. * @throws {SyntaxError} If there is a syntax error in the script.
  320. * @throws {*} If the script execution terminated with an exception it is propagated.
  321. * @fires NodeVM."console.debug"
  322. * @fires NodeVM."console.log"
  323. * @fires NodeVM."console.info"
  324. * @fires NodeVM."console.warn"
  325. * @fires NodeVM."console.error"
  326. * @fires NodeVM."console.dir"
  327. * @fires NodeVM."console.trace"
  328. */
  329. run(code, options) {
  330. let script;
  331. let filename;
  332. if (typeof options === 'object') {
  333. filename = options.filename;
  334. } else {
  335. filename = options;
  336. options = {__proto__: null};
  337. }
  338. const {
  339. strict = this.options.strict,
  340. wrapper = this.options.wrapper,
  341. module: customModule,
  342. require: customRequire,
  343. dirname: customDirname = null
  344. } = options;
  345. let sandboxModule = customModule;
  346. let dirname = customDirname;
  347. if (code instanceof VMScript) {
  348. script = strict ? code._compileNodeVMStrict() : code._compileNodeVM();
  349. if (!sandboxModule) {
  350. const resolvedFilename = this._resolver.pathResolve(code.filename);
  351. dirname = this._resolver.pathDirname(resolvedFilename);
  352. sandboxModule = new (this._Module)(resolvedFilename, dirname);
  353. this._resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false);
  354. }
  355. } else {
  356. const unresolvedFilename = filename || 'vm.js';
  357. if (!sandboxModule) {
  358. if (filename) {
  359. const resolvedFilename = this._resolver.pathResolve(filename);
  360. dirname = this._resolver.pathDirname(resolvedFilename);
  361. sandboxModule = new (this._Module)(resolvedFilename, dirname);
  362. this._resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false);
  363. } else {
  364. sandboxModule = new (this._Module)(null, null);
  365. sandboxModule.id = unresolvedFilename;
  366. }
  367. }
  368. const prefix = strict ? STRICT_MODULE_PREFIX : MODULE_PREFIX;
  369. let scriptCode = this._compiler(code, unresolvedFilename);
  370. scriptCode = transformer(null, scriptCode, false, false, unresolvedFilename).code;
  371. script = new Script(prefix + scriptCode + MODULE_SUFFIX, {
  372. __proto__: null,
  373. filename: unresolvedFilename,
  374. displayErrors: false
  375. });
  376. }
  377. const closure = this._runScript(script);
  378. const usedRequire = customRequire || this._createRequireForModule(sandboxModule);
  379. const ret = Reflect.apply(closure, this.sandbox, [sandboxModule.exports, usedRequire, sandboxModule, filename, dirname]);
  380. return wrapper === 'commonjs' ? sandboxModule.exports : ret;
  381. }
  382. /**
  383. * Create NodeVM and run code inside it.
  384. *
  385. * @public
  386. * @static
  387. * @param {string} script - Code to execute.
  388. * @param {string} [filename] - File name (used in stack traces only).
  389. * @param {Object} [options] - VM options.
  390. * @param {string} [options.filename] - File name (used in stack traces only). Used if <code>filename</code> is omitted.
  391. * @return {*} Result of executed code.
  392. * @see {@link NodeVM} for the options.
  393. * @throws {SyntaxError} If there is a syntax error in the script.
  394. * @throws {*} If the script execution terminated with an exception it is propagated.
  395. */
  396. static code(script, filename, options) {
  397. let unresolvedFilename;
  398. if (filename != null) {
  399. if ('object' === typeof filename) {
  400. options = filename;
  401. unresolvedFilename = options.filename;
  402. } else if ('string' === typeof filename) {
  403. unresolvedFilename = filename;
  404. } else {
  405. throw new VMError('Invalid arguments.');
  406. }
  407. } else if ('object' === typeof options) {
  408. unresolvedFilename = options.filename;
  409. }
  410. if (arguments.length > 3) {
  411. throw new VMError('Invalid number of arguments.');
  412. }
  413. const resolvedFilename = typeof unresolvedFilename === 'string' ? pa.resolve(unresolvedFilename) : undefined;
  414. return new NodeVM(options).run(script, resolvedFilename);
  415. }
  416. /**
  417. * Create NodeVM and run script from file inside it.
  418. *
  419. * @public
  420. * @static
  421. * @param {string} filename - Filename of file to load and execute in a NodeVM.
  422. * @param {Object} [options] - NodeVM options.
  423. * @return {*} Result of executed code.
  424. * @see {@link NodeVM} for the options.
  425. * @throws {Error} If filename is not a valid filename.
  426. * @throws {SyntaxError} If there is a syntax error in the script.
  427. * @throws {*} If the script execution terminated with an exception it is propagated.
  428. */
  429. static file(filename, options) {
  430. const resolvedFilename = pa.resolve(filename);
  431. if (!fs.existsSync(resolvedFilename)) {
  432. throw new VMError(`Script '${filename}' not found.`);
  433. }
  434. if (fs.statSync(resolvedFilename).isDirectory()) {
  435. throw new VMError('Script must be file, got directory.');
  436. }
  437. return new NodeVM(options).run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename);
  438. }
  439. }
  440. function vm2NestingLoader(resolver, vm, id) {
  441. if (!cacheMakeNestingScript) {
  442. cacheMakeNestingScript = compileScript('nesting.js', '(vm, nodevm) => ({VM: vm, NodeVM: nodevm})');
  443. }
  444. const makeNesting = vm._runScript(cacheMakeNestingScript);
  445. return makeNesting(vm.readonly(VM), vm.readonly(NodeVM));
  446. }
  447. exports.NodeVM = NodeVM;