vm.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. 'use strict';
  2. /**
  3. * This callback will be called to transform a script to JavaScript.
  4. *
  5. * @callback compileCallback
  6. * @param {string} code - Script code to transform to JavaScript.
  7. * @param {string} filename - Filename of this script.
  8. * @return {string} JavaScript code that represents the script code.
  9. */
  10. /**
  11. * This callback will be called to resolve a module if it couldn't be found.
  12. *
  13. * @callback resolveCallback
  14. * @param {string} moduleName - Name of the modulusedRequiree to resolve.
  15. * @param {string} dirname - Name of the current directory.
  16. * @return {(string|undefined)} The file or directory to use to load the requested module.
  17. */
  18. const fs = require('fs');
  19. const pa = require('path');
  20. const {
  21. Script,
  22. createContext
  23. } = require('vm');
  24. const {
  25. EventEmitter
  26. } = require('events');
  27. const {
  28. INSPECT_MAX_BYTES
  29. } = require('buffer');
  30. const {
  31. createBridge,
  32. VMError
  33. } = require('./bridge');
  34. const {
  35. transformer,
  36. INTERNAL_STATE_NAME
  37. } = require('./transformer');
  38. const {
  39. lookupCompiler
  40. } = require('./compiler');
  41. const {
  42. VMScript
  43. } = require('./script');
  44. const objectDefineProperties = Object.defineProperties;
  45. /**
  46. * Host objects
  47. *
  48. * @private
  49. */
  50. const HOST = Object.freeze({
  51. Buffer,
  52. Function,
  53. Object,
  54. transformAndCheck,
  55. INSPECT_MAX_BYTES,
  56. INTERNAL_STATE_NAME
  57. });
  58. /**
  59. * Compile a script.
  60. *
  61. * @private
  62. * @param {string} filename - Filename of the script.
  63. * @param {string} script - Script.
  64. * @return {vm.Script} The compiled script.
  65. */
  66. function compileScript(filename, script) {
  67. return new Script(script, {
  68. __proto__: null,
  69. filename,
  70. displayErrors: false
  71. });
  72. }
  73. /**
  74. * Default run options for vm.Script.runInContext
  75. *
  76. * @private
  77. */
  78. const DEFAULT_RUN_OPTIONS = Object.freeze({__proto__: null, displayErrors: false});
  79. function checkAsync(allow) {
  80. if (!allow) throw new VMError('Async not available');
  81. }
  82. function transformAndCheck(args, code, isAsync, isGenerator, allowAsync) {
  83. const ret = transformer(args, code, isAsync, isGenerator, undefined);
  84. checkAsync(allowAsync || !ret.hasAsync);
  85. return ret.code;
  86. }
  87. /**
  88. *
  89. * This callback will be called and has a specific time to finish.<br>
  90. * No parameters will be supplied.<br>
  91. * If parameters are required, use a closure.
  92. *
  93. * @private
  94. * @callback runWithTimeout
  95. * @return {*}
  96. *
  97. */
  98. let cacheTimeoutContext = null;
  99. let cacheTimeoutScript = null;
  100. /**
  101. * Run a function with a specific timeout.
  102. *
  103. * @private
  104. * @param {runWithTimeout} fn - Function to run with the specific timeout.
  105. * @param {number} timeout - The amount of time to give the function to finish.
  106. * @return {*} The value returned by the function.
  107. * @throws {Error} If the function took to long.
  108. */
  109. function doWithTimeout(fn, timeout) {
  110. if (!cacheTimeoutContext) {
  111. cacheTimeoutContext = createContext();
  112. cacheTimeoutScript = new Script('fn()', {
  113. __proto__: null,
  114. filename: 'timeout_bridge.js',
  115. displayErrors: false
  116. });
  117. }
  118. cacheTimeoutContext.fn = fn;
  119. try {
  120. return cacheTimeoutScript.runInContext(cacheTimeoutContext, {
  121. __proto__: null,
  122. displayErrors: false,
  123. timeout
  124. });
  125. } finally {
  126. cacheTimeoutContext.fn = null;
  127. }
  128. }
  129. const bridgeScript = compileScript(`${__dirname}/bridge.js`,
  130. `(function(global) {"use strict"; const exports = {};${fs.readFileSync(`${__dirname}/bridge.js`, 'utf8')}\nreturn exports;})`);
  131. const setupSandboxScript = compileScript(`${__dirname}/setup-sandbox.js`,
  132. `(function(global, host, bridge, data, context) { ${fs.readFileSync(`${__dirname}/setup-sandbox.js`, 'utf8')}\n})`);
  133. const getGlobalScript = compileScript('get_global.js', 'this');
  134. let getGeneratorFunctionScript = null;
  135. let getAsyncFunctionScript = null;
  136. let getAsyncGeneratorFunctionScript = null;
  137. try {
  138. getGeneratorFunctionScript = compileScript('get_generator_function.js', '(function*(){}).constructor');
  139. } catch (ex) {}
  140. try {
  141. getAsyncFunctionScript = compileScript('get_async_function.js', '(async function(){}).constructor');
  142. } catch (ex) {}
  143. try {
  144. getAsyncGeneratorFunctionScript = compileScript('get_async_generator_function.js', '(async function*(){}).constructor');
  145. } catch (ex) {}
  146. /**
  147. * Class VM.
  148. *
  149. * @public
  150. */
  151. class VM extends EventEmitter {
  152. /**
  153. * The timeout for {@link VM#run} calls.
  154. *
  155. * @public
  156. * @since v3.9.0
  157. * @member {number} timeout
  158. * @memberOf VM#
  159. */
  160. /**
  161. * Get the global sandbox object.
  162. *
  163. * @public
  164. * @readonly
  165. * @since v3.9.0
  166. * @member {Object} sandbox
  167. * @memberOf VM#
  168. */
  169. /**
  170. * The compiler to use to get the JavaScript code.
  171. *
  172. * @public
  173. * @readonly
  174. * @since v3.9.0
  175. * @member {(string|compileCallback)} compiler
  176. * @memberOf VM#
  177. */
  178. /**
  179. * The resolved compiler to use to get the JavaScript code.
  180. *
  181. * @private
  182. * @readonly
  183. * @member {compileCallback} _compiler
  184. * @memberOf VM#
  185. */
  186. /**
  187. * Create a new VM instance.
  188. *
  189. * @public
  190. * @param {Object} [options] - VM options.
  191. * @param {number} [options.timeout] - The amount of time until a call to {@link VM#run} will timeout.
  192. * @param {Object} [options.sandbox] - Objects that will be copied into the global object of the sandbox.
  193. * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use.
  194. * @param {boolean} [options.eval=true] - Allow the dynamic evaluation of code via eval(code) or Function(code)().<br>
  195. * Only available for node v10+.
  196. * @param {boolean} [options.wasm=true] - Allow to run wasm code.<br>
  197. * Only available for node v10+.
  198. * @param {boolean} [options.allowAsync=true] - Allows for async functions.
  199. * @throws {VMError} If the compiler is unknown.
  200. */
  201. constructor(options = {}) {
  202. super();
  203. // Read all options
  204. const {
  205. timeout,
  206. sandbox,
  207. compiler = 'javascript',
  208. allowAsync: optAllowAsync = true
  209. } = options;
  210. const allowEval = options.eval !== false;
  211. const allowWasm = options.wasm !== false;
  212. const allowAsync = optAllowAsync && !options.fixAsync;
  213. // Early error if sandbox is not an object.
  214. if (sandbox && 'object' !== typeof sandbox) {
  215. throw new VMError('Sandbox must be object.');
  216. }
  217. // Early error if compiler can't be found.
  218. const resolvedCompiler = lookupCompiler(compiler);
  219. // Create a new context for this vm.
  220. const _context = createContext(undefined, {
  221. __proto__: null,
  222. codeGeneration: {
  223. __proto__: null,
  224. strings: allowEval,
  225. wasm: allowWasm
  226. }
  227. });
  228. const sandboxGlobal = getGlobalScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
  229. // Initialize the sandbox bridge
  230. const {
  231. createBridge: sandboxCreateBridge
  232. } = bridgeScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal);
  233. // Initialize the bridge
  234. const bridge = createBridge(sandboxCreateBridge, () => {});
  235. const data = {
  236. __proto__: null,
  237. allowAsync
  238. };
  239. if (getGeneratorFunctionScript) {
  240. data.GeneratorFunction = getGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
  241. }
  242. if (getAsyncFunctionScript) {
  243. data.AsyncFunction = getAsyncFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
  244. }
  245. if (getAsyncGeneratorFunctionScript) {
  246. data.AsyncGeneratorFunction = getAsyncGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
  247. }
  248. // Create the bridge between the host and the sandbox.
  249. const internal = setupSandboxScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal, HOST, bridge.other, data, _context);
  250. const runScript = (script) => {
  251. // This closure is intentional to hide _context and bridge since the allow to access the sandbox directly which is unsafe.
  252. let ret;
  253. try {
  254. ret = script.runInContext(_context, DEFAULT_RUN_OPTIONS);
  255. } catch (e) {
  256. throw bridge.from(e);
  257. }
  258. return bridge.from(ret);
  259. };
  260. const makeReadonly = (value, mock) => {
  261. try {
  262. internal.readonly(value, mock);
  263. } catch (e) {
  264. throw bridge.from(e);
  265. }
  266. return value;
  267. };
  268. const makeProtected = (value) => {
  269. const sandboxBridge = bridge.other;
  270. try {
  271. sandboxBridge.fromWithFactory(sandboxBridge.protectedFactory, value);
  272. } catch (e) {
  273. throw bridge.from(e);
  274. }
  275. return value;
  276. };
  277. const addProtoMapping = (hostProto, sandboxProto) => {
  278. const sandboxBridge = bridge.other;
  279. let otherProto;
  280. try {
  281. otherProto = sandboxBridge.from(sandboxProto);
  282. sandboxBridge.addProtoMapping(otherProto, hostProto);
  283. } catch (e) {
  284. throw bridge.from(e);
  285. }
  286. bridge.addProtoMapping(hostProto, otherProto);
  287. };
  288. const addProtoMappingFactory = (hostProto, sandboxProtoFactory) => {
  289. const sandboxBridge = bridge.other;
  290. const factory = () => {
  291. const proto = sandboxProtoFactory(this);
  292. bridge.addProtoMapping(hostProto, proto);
  293. return proto;
  294. };
  295. try {
  296. const otherProtoFactory = sandboxBridge.from(factory);
  297. sandboxBridge.addProtoMappingFactory(otherProtoFactory, hostProto);
  298. } catch (e) {
  299. throw bridge.from(e);
  300. }
  301. };
  302. // Define the properties of this object.
  303. // Use Object.defineProperties here to be able to
  304. // hide and set properties read-only.
  305. objectDefineProperties(this, {
  306. __proto__: null,
  307. timeout: {
  308. __proto__: null,
  309. value: timeout,
  310. writable: true,
  311. enumerable: true
  312. },
  313. compiler: {
  314. __proto__: null,
  315. value: compiler,
  316. enumerable: true
  317. },
  318. sandbox: {
  319. __proto__: null,
  320. value: bridge.from(sandboxGlobal),
  321. enumerable: true
  322. },
  323. _runScript: {__proto__: null, value: runScript},
  324. _makeReadonly: {__proto__: null, value: makeReadonly},
  325. _makeProtected: {__proto__: null, value: makeProtected},
  326. _addProtoMapping: {__proto__: null, value: addProtoMapping},
  327. _addProtoMappingFactory: {__proto__: null, value: addProtoMappingFactory},
  328. _compiler: {__proto__: null, value: resolvedCompiler},
  329. _allowAsync: {__proto__: null, value: allowAsync}
  330. });
  331. // prepare global sandbox
  332. if (sandbox) {
  333. this.setGlobals(sandbox);
  334. }
  335. }
  336. /**
  337. * Adds all the values to the globals.
  338. *
  339. * @public
  340. * @since v3.9.0
  341. * @param {Object} values - All values that will be added to the globals.
  342. * @return {this} This for chaining.
  343. * @throws {*} If the setter of a global throws an exception it is propagated. And the remaining globals will not be written.
  344. */
  345. setGlobals(values) {
  346. for (const name in values) {
  347. if (Object.prototype.hasOwnProperty.call(values, name)) {
  348. this.sandbox[name] = values[name];
  349. }
  350. }
  351. return this;
  352. }
  353. /**
  354. * Set a global value.
  355. *
  356. * @public
  357. * @since v3.9.0
  358. * @param {string} name - The name of the global.
  359. * @param {*} value - The value of the global.
  360. * @return {this} This for chaining.
  361. * @throws {*} If the setter of the global throws an exception it is propagated.
  362. */
  363. setGlobal(name, value) {
  364. this.sandbox[name] = value;
  365. return this;
  366. }
  367. /**
  368. * Get a global value.
  369. *
  370. * @public
  371. * @since v3.9.0
  372. * @param {string} name - The name of the global.
  373. * @return {*} The value of the global.
  374. * @throws {*} If the getter of the global throws an exception it is propagated.
  375. */
  376. getGlobal(name) {
  377. return this.sandbox[name];
  378. }
  379. /**
  380. * Freezes the object inside VM making it read-only. Not available for primitive values.
  381. *
  382. * @public
  383. * @param {*} value - Object to freeze.
  384. * @param {string} [globalName] - Whether to add the object to global.
  385. * @return {*} Object to freeze.
  386. * @throws {*} If the setter of the global throws an exception it is propagated.
  387. */
  388. freeze(value, globalName) {
  389. this.readonly(value);
  390. if (globalName) this.sandbox[globalName] = value;
  391. return value;
  392. }
  393. /**
  394. * Freezes the object inside VM making it read-only. Not available for primitive values.
  395. *
  396. * @public
  397. * @param {*} value - Object to freeze.
  398. * @param {*} [mock] - When the object does not have a property the mock is used before prototype lookup.
  399. * @return {*} Object to freeze.
  400. */
  401. readonly(value, mock) {
  402. return this._makeReadonly(value, mock);
  403. }
  404. /**
  405. * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values.
  406. *
  407. * @public
  408. * @param {*} value - Object to protect.
  409. * @param {string} [globalName] - Whether to add the object to global.
  410. * @return {*} Object to protect.
  411. * @throws {*} If the setter of the global throws an exception it is propagated.
  412. */
  413. protect(value, globalName) {
  414. this._makeProtected(value);
  415. if (globalName) this.sandbox[globalName] = value;
  416. return value;
  417. }
  418. /**
  419. * Run the code in VM.
  420. *
  421. * @public
  422. * @param {(string|VMScript)} code - Code to run.
  423. * @param {(string|Object)} [options] - Options map or filename.
  424. * @param {string} [options.filename="vm.js"] - Filename that shows up in any stack traces produced from this script.<br>
  425. * This is only used if code is a String.
  426. * @return {*} Result of executed code.
  427. * @throws {SyntaxError} If there is a syntax error in the script.
  428. * @throws {Error} An error is thrown when the script took to long and there is a timeout.
  429. * @throws {*} If the script execution terminated with an exception it is propagated.
  430. */
  431. run(code, options) {
  432. let script;
  433. let filename;
  434. if (typeof options === 'object') {
  435. filename = options.filename;
  436. } else {
  437. filename = options;
  438. }
  439. if (code instanceof VMScript) {
  440. script = code._compileVM();
  441. checkAsync(this._allowAsync || !code._hasAsync);
  442. } else {
  443. const useFileName = filename || 'vm.js';
  444. let scriptCode = this._compiler(code, useFileName);
  445. const ret = transformer(null, scriptCode, false, false, useFileName);
  446. scriptCode = ret.code;
  447. checkAsync(this._allowAsync || !ret.hasAsync);
  448. // Compile the script here so that we don't need to create a instance of VMScript.
  449. script = new Script(scriptCode, {
  450. __proto__: null,
  451. filename: useFileName,
  452. displayErrors: false
  453. });
  454. }
  455. if (!this.timeout) {
  456. return this._runScript(script);
  457. }
  458. return doWithTimeout(() => {
  459. return this._runScript(script);
  460. }, this.timeout);
  461. }
  462. /**
  463. * Run the code in VM.
  464. *
  465. * @public
  466. * @since v3.9.0
  467. * @param {string} filename - Filename of file to load and execute in a NodeVM.
  468. * @return {*} Result of executed code.
  469. * @throws {Error} If filename is not a valid filename.
  470. * @throws {SyntaxError} If there is a syntax error in the script.
  471. * @throws {Error} An error is thrown when the script took to long and there is a timeout.
  472. * @throws {*} If the script execution terminated with an exception it is propagated.
  473. */
  474. runFile(filename) {
  475. const resolvedFilename = pa.resolve(filename);
  476. if (!fs.existsSync(resolvedFilename)) {
  477. throw new VMError(`Script '${filename}' not found.`);
  478. }
  479. if (fs.statSync(resolvedFilename).isDirectory()) {
  480. throw new VMError('Script must be file, got directory.');
  481. }
  482. return this.run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename);
  483. }
  484. }
  485. exports.VM = VM;