├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── stale.yml └── workflows │ └── node-test.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── vm2 ├── index.d.ts ├── index.js ├── lib ├── bridge.js ├── builtin.js ├── cli.js ├── compiler.js ├── events.js ├── filesystem.js ├── main.js ├── nodevm.js ├── resolver-compat.js ├── resolver.js ├── script.js ├── setup-node-sandbox.js ├── setup-sandbox.js ├── transformer.js └── vm.js ├── package-lock.json ├── package.json └── test ├── additional-modules ├── my-es-module │ ├── index.cjs │ ├── index.js │ └── package.json └── my-module │ └── index.js ├── data ├── custom_extension.ts └── json.json ├── mocha.opts ├── node_modules ├── foobar │ └── index.js ├── module-main-without-extension │ ├── main.js │ └── package.json ├── module-with-wrong-main │ ├── index.js │ └── package.json ├── module1 │ └── index.js ├── module2 │ └── index.js ├── require │ └── index.js └── with-exports │ ├── main.js │ └── package.json ├── nodevm.js └── vm.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [/lib/events.js] 12 | indent_size = 2 13 | 14 | [*.ts] 15 | charset = utf-8 16 | indent_style = space 17 | indent_size = 2 18 | end_of_line = lf 19 | insert_final_newline = true 20 | trim_trailing_whitespace = true 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /test.js 2 | /node-* 3 | /lib/events.js 4 | /test/additional-modules/my-es-module/index.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | extends: [ 7 | 'integromat' 8 | ], 9 | parserOptions: { 10 | 'ecmaVersion': 2017, 11 | 'ecmaFeatures': { 12 | 'globalReturn': true 13 | } 14 | }, 15 | globals: { 16 | }, 17 | rules: { 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - bug 8 | - confirmed 9 | - help wanted 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.github/workflows/node-test.yml: -------------------------------------------------------------------------------- 1 | # From https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs-or-python 2 | 3 | name: Node.js CI 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [18, 20, 22] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: Install dependencies 20 | run: npm ci 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | .svn 4 | /node-* 5 | /test.js 6 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | .svn 4 | .travis.yml 5 | /test.js 6 | /test 7 | .vscode 8 | .github 9 | .editorconfig -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "6" 5 | - "8" 6 | - "10" 7 | - "12" 8 | - "14" 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | (discontinued) (2023-07-09) 2 | --------------------------- 3 | Discontinued do to security issues without proper fixes. 4 | 5 | v3.9.19 (2023-05-16) 6 | -------------------- 7 | [fix] Fix resolver issue. 8 | 9 | v3.9.18 (2023-05-15) 10 | -------------------- 11 | [fix] Multiple security fixes. 12 | [new] Add resolver API to create a shared resolver for multiple `NodeVM` instances allowing to cache scripts and increase sandbox startup times. 13 | [new] Allow to pass a function to `require.context` which is called with the filename allowing to specify the context pre file. 14 | 15 | v3.9.17 (2023-04-17) 16 | -------------------- 17 | [fix] Multiple security fixes. 18 | 19 | v3.9.16 (2023-04-11) 20 | -------------------- 21 | [fix] Security fix (see https://github.com/patriksimek/vm2/issues/516). 22 | 23 | v3.9.15 (2023-04-06) 24 | -------------------- 25 | [fix] Security fix (see https://github.com/patriksimek/vm2/issues/515). 26 | 27 | v3.9.14 (2023-02-05) 28 | -------------------- 29 | [new] Support conditional export resolution with custom resolver. (nick-klaviyo) 30 | 31 | v3.9.13 (2022-12-08) 32 | -------------------- 33 | [fix] Fix typescript errors in index.d.ts 34 | 35 | v3.9.12 (2022-11-29) 36 | -------------------- 37 | [new] Add file system API. 38 | [fix] Fix parsing error with object pattern in catch clause. 39 | 40 | v3.9.11 (2022-08-28) 41 | -------------------- 42 | [new] Add option `require.strict` to allow to load required modules in non strict mode. 43 | [fix] Security fix. 44 | 45 | v3.9.10 (2022-07-05) 46 | ------------------- 47 | [new] Add uptime to process. 48 | [fix] Security fix. 49 | [fix] Fix inspection with showProxy. 50 | 51 | v3.9.9 (2022-02-24) 52 | ------------------- 53 | [fix] Bump parser ECMA version to 2022. 54 | 55 | v3.9.8 (2022-02-16) 56 | ------------------- 57 | [fix] Add function type check for arguments, caller, and callee property check (GeoffRen) 58 | [fix] Fix find best extension handler 59 | 60 | v3.9.7 (2022-02-10) 61 | ------------------- 62 | [fix] Allow relative require from base script 63 | [fix] Fix issue with modules with exports clause in package JSON 64 | [fix] Added missing whitelist check before custom require 65 | [fix] Revert plain object toString behavior 66 | [fix] Root path check improved 67 | 68 | v3.9.6 (2022-02-08) 69 | ------------------- 70 | [fix] Security fixes (XmiliaH) 71 | 72 | v3.9.5 (2021-10-17) 73 | ------------------- 74 | [new] Editor config (aubelsb2) 75 | [fix] Fix for Promise.then breaking 76 | [fix] Fix for missing properties on CallSite 77 | 78 | v3.9.4 (2021-10-12) 79 | ------------------- 80 | [new] Added strict option 81 | [fix] Security fixes (XmiliaH) 82 | [fix] Fixed bound function causes TypeError (XmiliaH) 83 | [fix] Allow extending of frozen objects 84 | 85 | v3.9.3 (2020-04-07) 86 | ------------------- 87 | [fix] Security fixes 88 | [fix] Fixed problems when Promise object is deleted (XmiliaH) 89 | [fix] Fixed oversight that write ability can change on non configurable properties (XmiliaH) 90 | [fix] Support shebang as node does (XmiliaH) 91 | [fix] Property typos (Shigma) 92 | 93 | 94 | v3.9.2 (2020-04-29) 95 | ------------------- 96 | [new] Added NodeVM options to pass argv & env to process object (XmiliaH) 97 | [fix] Fixed breakouts in NodeVM (XmiliaH) 98 | [fix] Made async check more robust (XmiliaH) 99 | 100 | v3.9.1 (2020-03-29) 101 | ------------------- 102 | [fix] Require helpers statically in main (XmiliaH) 103 | [fix] Fix for non-configurable property access (XmiliaH) 104 | 105 | v3.9.0 (2020-03-21) 106 | ------------------- 107 | [new] Added vm.Script `lineOffset` and `columnOffset` options (azu) 108 | [new] Allow to specify a compiler per VMScript (XmiliaH) 109 | [new] Add option to disable async (XmiliaH) 110 | [new] Added allot of jsdoc (XmiliaH) 111 | [fix] Fix access to frozen or unconfigurable properties (XmiliaH) 112 | [fix] Double wrap Objects to prevent breakout via inspect (XmiliaH) 113 | [fix] Compile now compiles VM code (XmiliaH) 114 | 115 | v3.8.4 (2019-09-13) 116 | ------------------- 117 | [fix] Do not allow precompiling VMScript (XmiliaH) 118 | [fix] Security fixes (XmiliaH) 119 | 120 | v3.8.3 (2019-07-31) 121 | ------------------- 122 | [fix] Security fixes 123 | 124 | v3.8.2 (2019-06-13) 125 | ------------------- 126 | [fix] toString() on builtin objects 127 | 128 | v3.8.1 (2019-05-02) 129 | ------------------- 130 | [fix] Module resolver fixes 131 | [fix] require('events') works correctly in Node 12 132 | [fix] SyntaxError not being instanceOf Error 133 | 134 | v3.8.0 (2019-04-21) 135 | ------------------- 136 | [new] Allow prohibiting access to eval/wasm in sandbox context 137 | [new] Allow transitive external dependencies in sandbox context (Idan Attias) 138 | [new] Allow using wildcards in module-names passed using the external attribute (Harel Moshe) 139 | [fix] Default to index.js when specified "main" does not exist (Harel Moshe) 140 | [fix] Security fixes 141 | 142 | v3.7.0 (2019-04-15) 143 | ------------------- 144 | [new] Add require.resolve (Idan Attias) 145 | [new] Support multiple root paths (Idan Attias) 146 | 147 | v3.6.11 (2019-04-08) 148 | ------------------- 149 | [fix] Contextification of EvalError and URIError 150 | [fix] Security fixes 151 | 152 | v3.6.10 (2019-01-28) 153 | ------------------- 154 | [fix] Add missing console.debug function in NodeVM 155 | [fix] Security fixes 156 | 157 | v3.6.9 (2019-01-26) 158 | ------------------- 159 | [fix] Security fixes 160 | 161 | v3.6.8 (2019-01-26) 162 | ------------------- 163 | [fix] Security fixes 164 | 165 | v3.6.7 (2019-01-26) 166 | ------------------- 167 | [fix] Security fixes 168 | 169 | v3.6.6 (2019-01-01) 170 | ------------------- 171 | [fix] Security fixes 172 | 173 | v3.6.5 (2018-12-31) 174 | ------------------- 175 | [fix] Security fixes 176 | 177 | v3.6.4 (2018-10-17) 178 | ------------------- 179 | [fix] Added new to vmwerror when trying to load coffeescipt but can't (dotconnor) 180 | [fix] Add arguments to process.nextTick proxy (Patrick Engström) 181 | 182 | v3.6.3 (2018-08-06) 183 | ------------------- 184 | [fix] Security fixes 185 | 186 | v3.6.2 (2018-07-05) 187 | ------------------- 188 | [fix] Security fixes 189 | 190 | v3.6.1 (2018-06-27) 191 | ------------------- 192 | [fix] Security fixes 193 | 194 | v3.6.0 (2018-05-11) 195 | ------------------- 196 | [new] Support for custom source extensions 197 | [new] WIP support for disallowing Promise 198 | [fix] Prevent slow unsafe alloc for Buffers 199 | [fix] Refactors around defaults 200 | [fix] Types definition update 201 | 202 | v3.5.2 (2017-10-04) 203 | ------------------- 204 | [fix] Prevent slow unsafe alloc for Buffers 205 | 206 | v3.5.1 (2017-10-04) 207 | ------------------- 208 | [fix] Prevent unsafe alloc for Buffers 209 | 210 | v3.5.0 (2017-08-31) 211 | ------------------- 212 | [new] Allow a custom compiler to receive the filetype (Orta Therox) 213 | [new] Allow in-sandbox requires to also get called through the compiler (Orta Therox) 214 | [new] Support whitelisting modules inside a VM (Orta Therox) 215 | [new] Add TypeScript definition (Orta Therox) 216 | 217 | v3.4.0 (2017-03-28) 218 | ------------------- 219 | [new] Added experimental VM.protect method 220 | 221 | v3.3.1 (2017-03-27) 222 | ------------------- 223 | [new] Added VM.freeze method 224 | 225 | v3.2.0 (2017-02-10) 226 | ------------------- 227 | [new] Added support for pre-compiled scripts via VMScript 228 | 229 | v3.1.0 (2016-09-03) 230 | ------------------- 231 | [new] Added option wrapper (Alizain Feerasta) 232 | 233 | v3.0.1 (2016-07-20) 234 | ------------------- 235 | Initial release 236 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2022 Patrik Simek and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vm2 [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Package Quality][quality-image]][quality-url] [![Node.js CI](https://github.com/patriksimek/vm2/actions/workflows/node-test.yml/badge.svg)](https://github.com/patriksimek/vm2/actions/workflows/node-test.yml) [![Known Vulnerabilities][snyk-image]][snyk-url] 2 | 3 | ## ‼️ Project Discontinued ‼️ 4 | 5 | **TL;DR The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to [isolated-vm](https://www.npmjs.com/package/isolated-vm).** 6 | 7 | Dear community, 8 | 9 | It's been a truly remarkable journey for me since the vm2 project started nine years ago. The original intent was to devise a method for running untrusted code in Node, with a keen focus on maintaining in-process performance. Proxies, an emerging feature in JavaScript at that time, became our tool of choice for this task. 10 | 11 | From the get-go, we recognized the arduous task that lay ahead, as we tried to safeguard against the myriad of escape scenarios JavaScript presented. However, the thrill of the chase kept us going, hopeful that we could overcome these hurdles. 12 | 13 | Through the years, this project has seen numerous contributions from passionate individuals. I wish to extend my deepest gratitude to all of you. Special thanks go to @XmiliaH, whose unwavering dedication in maintaining and improving this library over the last 4 years was instrumental to its sustained relevance. 14 | 15 | Unfortunately, the growing complexity of Node has brought us to a crossroads. We now find ourselves facing an escape so complicated that fixing it seems impossible. And this isn't about one isolated issue. Recent reports have highlighted that sustaining this project in its current form is not viable in the long term. 16 | 17 | Therefore, we must announce the discontinuation of this project. 18 | 19 | You may wonder, "What now?" 20 | 21 | While this may seem like an end, I see it as an opportunity for you to transition your projects and adapt to a new solution. We would recommend migrating your code to the [isolated-vm](https://www.npmjs.com/package/isolated-vm), a library which employs a slightly different, yet equally effective, approach to sandboxing untrusted code. 22 | 23 | Thank you all for your support and understanding during this journey. 24 | 25 | Warm Regards, 26 | Patrik Simek 27 | 28 | --- 29 | 30 |
31 | The original Readme is available here. 32 | 33 | vm2 is a sandbox that can run untrusted code with whitelisted Node's built-in modules. ~~Securely!~~ 34 | 35 | ## Features 36 | 37 | * Runs untrusted code securely in a single process with your code side by side 38 | * Full control over the sandbox's console output 39 | * The sandbox has limited access to the process's methods 40 | * It is possible to require modules (built-in and external) from the sandbox 41 | * You can limit access to certain (or all) built-in modules 42 | * You can securely call methods and exchange data and callbacks between sandboxes 43 | * Is immune to all known methods of attacks 44 | * Transpiler support 45 | 46 | ## How does it work 47 | 48 | * It uses the internal VM module to create a secure context. 49 | * It uses [Proxies](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) to prevent escaping from the sandbox. 50 | * It overrides the built-in require to control access to modules. 51 | 52 | ## What is the difference between Node's vm and vm2? 53 | 54 | Try it yourself: 55 | 56 | ```js 57 | const vm = require('vm'); 58 | vm.runInNewContext('this.constructor.constructor("return process")().exit()'); 59 | console.log('Never gets executed.'); 60 | ``` 61 | 62 | ```js 63 | const {VM} = require('vm2'); 64 | new VM().run('this.constructor.constructor("return process")().exit()'); 65 | // Throws ReferenceError: process is not defined 66 | ``` 67 | 68 | ## Installation 69 | 70 | **IMPORTANT**: VM2 requires Node.js 6 or newer. 71 | 72 | ```sh 73 | npm install vm2 74 | ``` 75 | 76 | ## Quick Example 77 | 78 | ```js 79 | const {VM} = require('vm2'); 80 | const vm = new VM(); 81 | 82 | vm.run(`process.exit()`); // TypeError: process.exit is not a function 83 | ``` 84 | 85 | ```js 86 | const {NodeVM} = require('vm2'); 87 | const vm = new NodeVM({ 88 | require: { 89 | external: true, 90 | root: './' 91 | } 92 | }); 93 | 94 | vm.run(` 95 | var request = require('request'); 96 | request('http://www.google.com', function (error, response, body) { 97 | console.error(error); 98 | if (!error && response.statusCode == 200) { 99 | console.log(body); // Show the HTML for the Google homepage. 100 | } 101 | }); 102 | `, 'vm.js'); 103 | ``` 104 | 105 | ## Documentation 106 | 107 | * [VM](#vm) 108 | * [NodeVM](#nodevm) 109 | * [VMScript](#vmscript) 110 | * [Error handling](#error-handling) 111 | * [Debugging a sandboxed code](#debugging-a-sandboxed-code) 112 | * [Read-only objects](#read-only-objects-experimental) 113 | * [Protected objects](#protected-objects-experimental) 114 | * [Cross-sandbox relationships](#cross-sandbox-relationships) 115 | * [CLI](#cli) 116 | * [2.x to 3.x changes](https://github.com/patriksimek/vm2/wiki/2.x-to-3.x-changes) 117 | * [1.x and 2.x docs](https://github.com/patriksimek/vm2/wiki/1.x-and-2.x-docs) 118 | * [Contributing](https://github.com/patriksimek/vm2/wiki/Contributing) 119 | 120 | ## VM 121 | 122 | VM is a simple sandbox to synchronously run untrusted code without the `require` feature. Only JavaScript built-in objects and Node's `Buffer` are available. Scheduling functions (`setInterval`, `setTimeout` and `setImmediate`) are not available by default. 123 | 124 | **Options:** 125 | 126 | * `timeout` - Script timeout in milliseconds. **WARNING**: You might want to use this option together with `allowAsync=false`. Further, operating on returned objects from the sandbox can run arbitrary code and circumvent the timeout. One should test if the returned object is a primitive with `typeof` and fully discard it (doing logging or creating error messages with such an object might also run arbitrary code again) in the other case. 127 | * `sandbox` - VM's global object. 128 | * `compiler` - `javascript` (default) or `coffeescript` or custom compiler function. The library expects you to have coffee-script pre-installed if the compiler is set to `coffeescript`. 129 | * `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`). 130 | * `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`). 131 | * `allowAsync` - If set to `false` any attempt to run code using `async` will throw a `VMError` (default: `true`). 132 | 133 | **IMPORTANT**: Timeout is only effective on synchronous code that you run through `run`. Timeout does **NOT** work on any method returned by VM. There are some situations when timeout doesn't work - see [#244](https://github.com/patriksimek/vm2/pull/244). 134 | 135 | ```js 136 | const {VM} = require('vm2'); 137 | 138 | const vm = new VM({ 139 | timeout: 1000, 140 | allowAsync: false, 141 | sandbox: {} 142 | }); 143 | 144 | vm.run('process.exit()'); // throws ReferenceError: process is not defined 145 | ``` 146 | 147 | You can also retrieve values from VM. 148 | 149 | ```js 150 | let number = vm.run('1337'); // returns 1337 151 | ``` 152 | 153 | **TIP**: See tests for more usage examples. 154 | 155 | ## NodeVM 156 | 157 | Unlike `VM`, `NodeVM` allows you to require modules in the same way that you would in the regular Node's context. 158 | 159 | **Options:** 160 | 161 | * `console` - `inherit` to enable console, `redirect` to redirect to events, `off` to disable console (default: `inherit`). 162 | * `sandbox` - VM's global object. 163 | * `compiler` - `javascript` (default) or `coffeescript` or custom compiler function (which receives the code, and it's file path). The library expects you to have coffee-script pre-installed if the compiler is set to `coffeescript`. 164 | * `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`). 165 | * `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`). 166 | * `sourceExtensions` - Array of file extensions to treat as source code (default: `['js']`). 167 | * `require` - `true`, an object or a Resolver to enable `require` method (default: `false`). 168 | * `require.external` - Values can be `true`, an array of allowed external modules, or an object (default: `false`). All paths matching `/node_modules/${any_allowed_external_module}/(?!/node_modules/)` are allowed to be required. 169 | * `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. The `*` wildcard does not match path separators. 170 | * `require.external.transitive` - Boolean which indicates if transitive dependencies of external modules are allowed (default: `false`). **WARNING**: When a module is required transitively, any module is then able to require it normally, even if this was not possible before it was loaded. 171 | * `require.builtin` - Array of allowed built-in modules, accepts ["\*"] for all (default: none). **WARNING**: "\*" can be dangerous as new built-ins can be added. 172 | * `require.root` - Restricted path(s) where local modules can be required (default: every path). 173 | * `require.mock` - Collection of mock modules (both external or built-in). 174 | * `require.context` - `host` (default) to require modules in the host and proxy them into the sandbox. `sandbox` to load, compile, and require modules in the sandbox. `callback(moduleFilename, ext)` to dynamically choose a context per module. The default will be sandbox is nothing is specified. Except for `events`, built-in modules are always required in the host and proxied into the sandbox. 175 | * `require.import` - An array of modules to be loaded into NodeVM on start. 176 | * `require.resolve` - An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. 177 | * `require.customRequire` - Use instead of the `require` function to load modules from the host. 178 | * `require.strict` - `false` to not force strict mode on modules loaded by require (default: `true`). 179 | * `require.fs` - Custom file system implementation. 180 | * `nesting` - **WARNING**: Allowing this is a security risk as scripts can create a NodeVM which can require any host module. `true` to enable VMs nesting (default: `false`). 181 | * `wrapper` - `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script. 182 | * `argv` - Array to be passed to `process.argv`. 183 | * `env` - Object to be passed to `process.env`. 184 | * `strict` - `true` to loaded modules in strict mode (default: `false`). 185 | 186 | **IMPORTANT**: Timeout is not effective for NodeVM so it is not immune to `while (true) {}` or similar evil. 187 | 188 | **REMEMBER**: The more modules you allow, the more fragile your sandbox becomes. 189 | 190 | ```js 191 | const {NodeVM} = require('vm2'); 192 | 193 | const vm = new NodeVM({ 194 | console: 'inherit', 195 | sandbox: {}, 196 | require: { 197 | external: true, 198 | builtin: ['fs', 'path'], 199 | root: './', 200 | mock: { 201 | fs: { 202 | readFileSync: () => 'Nice try!' 203 | } 204 | } 205 | } 206 | }); 207 | 208 | // Sync 209 | 210 | let functionInSandbox = vm.run('module.exports = function(who) { console.log("hello "+ who); }'); 211 | functionInSandbox('world'); 212 | 213 | // Async 214 | 215 | let functionWithCallbackInSandbox = vm.run('module.exports = function(who, callback) { callback("hello "+ who); }'); 216 | functionWithCallbackInSandbox('world', (greeting) => { 217 | console.log(greeting); 218 | }); 219 | ``` 220 | 221 | When `wrapper` is set to `none`, `NodeVM` behaves more like `VM` for synchronous code. 222 | 223 | ```js 224 | assert.ok(vm.run('return true') === true); 225 | ``` 226 | 227 | **TIP**: See tests for more usage examples. 228 | 229 | ### Loading modules by relative path 230 | 231 | To load modules by relative path, you must pass the full path of the script you're running as a second argument to vm's `run` method if the script is a string. The filename is then displayed in any stack traces generated by the script. 232 | 233 | ```js 234 | vm.run('require("foobar")', '/data/myvmscript.js'); 235 | ``` 236 | 237 | If the script you are running is a VMScript, the path is given in the VMScript constructor. 238 | 239 | ```js 240 | const script = new VMScript('require("foobar")', {filename: '/data/myvmscript.js'}); 241 | vm.run(script); 242 | ``` 243 | 244 | ### Resolver 245 | 246 | A resolver can be created via `makeResolverFromLegacyOptions` and be used for multiple `NodeVM` instances allowing to share compiled module code potentially speeding up load times. The first example of `NodeVM` can be rewritten using `makeResolverFromLegacyOptions` as follows. 247 | 248 | ```js 249 | const resolver = makeResolverFromLegacyOptions({ 250 | external: true, 251 | builtin: ['fs', 'path'], 252 | root: './', 253 | mock: { 254 | fs: { 255 | readFileSync: () => 'Nice try!' 256 | } 257 | } 258 | }); 259 | const vm = new NodeVM({ 260 | console: 'inherit', 261 | sandbox: {}, 262 | require: resolver 263 | }); 264 | ``` 265 | 266 | ## VMScript 267 | 268 | You can increase performance by using precompiled scripts. The precompiled VMScript can be run multiple times. It is important to note that the code is not bound to any VM (context); rather, it is bound before each run, just for that run. 269 | 270 | ```js 271 | const {VM, VMScript} = require('vm2'); 272 | 273 | const vm = new VM(); 274 | const script = new VMScript('Math.random()'); 275 | console.log(vm.run(script)); 276 | console.log(vm.run(script)); 277 | ``` 278 | 279 | It works for both `VM` and `NodeVM`. 280 | 281 | ```js 282 | const {NodeVM, VMScript} = require('vm2'); 283 | 284 | const vm = new NodeVM(); 285 | const script = new VMScript('module.exports = Math.random()'); 286 | console.log(vm.run(script)); 287 | console.log(vm.run(script)); 288 | ``` 289 | 290 | Code is compiled automatically the first time it runs. One can compile the code anytime with `script.compile()`. Once the code is compiled, the method has no effect. 291 | 292 | ## Error handling 293 | 294 | Errors in code compilation and synchronous code execution can be handled by `try-catch`. Errors in asynchronous code execution can be handled by attaching `uncaughtException` event handler to Node's `process`. 295 | 296 | ```js 297 | try { 298 | var script = new VMScript('Math.random()').compile(); 299 | } catch (err) { 300 | console.error('Failed to compile script.', err); 301 | } 302 | 303 | try { 304 | vm.run(script); 305 | } catch (err) { 306 | console.error('Failed to execute script.', err); 307 | } 308 | 309 | process.on('uncaughtException', (err) => { 310 | console.error('Asynchronous error caught.', err); 311 | }); 312 | ``` 313 | 314 | ## Debugging a sandboxed code 315 | 316 | You can debug or inspect code running in the sandbox as if it was running in a normal process. 317 | 318 | * You can use breakpoints (which requires you to specify a script file name) 319 | * You can use `debugger` keyword. 320 | * You can use step-in to step inside the code running in the sandbox. 321 | 322 | ### Example 323 | 324 | /tmp/main.js: 325 | 326 | ```js 327 | const {VM, VMScript} = require('.'); 328 | const fs = require('fs'); 329 | const file = `${__dirname}/sandbox.js`; 330 | 331 | // By providing a file name as second argument you enable breakpoints 332 | const script = new VMScript(fs.readFileSync(file), file); 333 | 334 | new VM().run(script); 335 | ``` 336 | 337 | /tmp/sandbox.js 338 | 339 | ```js 340 | const foo = 'ahoj'; 341 | 342 | // The debugger keyword works just fine everywhere. 343 | // Even without specifying a file name to the VMScript object. 344 | debugger; 345 | ``` 346 | 347 | ## Read-only objects (experimental) 348 | 349 | To prevent sandboxed scripts from adding, changing, or deleting properties from the proxied objects, you can use `freeze` methods to make the object read-only. This is only effective inside VM. Frozen objects are affected deeply. Primitive types cannot be frozen. 350 | 351 | **Example without using `freeze`:** 352 | 353 | ```js 354 | const util = { 355 | add: (a, b) => a + b 356 | } 357 | 358 | const vm = new VM({ 359 | sandbox: {util} 360 | }); 361 | 362 | vm.run('util.add = (a, b) => a - b'); 363 | console.log(util.add(1, 1)); // returns 0 364 | ``` 365 | 366 | **Example with using `freeze`:** 367 | 368 | ```js 369 | const vm = new VM(); // Objects specified in the sandbox cannot be frozen. 370 | vm.freeze(util, 'util'); // Second argument adds object to global. 371 | 372 | vm.run('util.add = (a, b) => a - b'); // Fails silently when not in strict mode. 373 | console.log(util.add(1, 1)); // returns 2 374 | ``` 375 | 376 | **IMPORTANT:** It is not possible to freeze objects that have already been proxied to the VM. 377 | 378 | ## Protected objects (experimental) 379 | 380 | Unlike `freeze`, this method allows sandboxed scripts to add, change, or delete properties on objects, with one exception - it is not possible to attach functions. Sandboxed scripts are therefore not able to modify methods like `toJSON`, `toString` or `inspect`. 381 | 382 | **IMPORTANT:** It is not possible to protect objects that have already been proxied to the VM. 383 | 384 | ## Cross-sandbox relationships 385 | 386 | ```js 387 | const assert = require('assert'); 388 | const {VM} = require('vm2'); 389 | 390 | const sandbox = { 391 | object: new Object(), 392 | func: new Function(), 393 | buffer: new Buffer([0x01, 0x05]) 394 | } 395 | 396 | const vm = new VM({sandbox}); 397 | 398 | assert.ok(vm.run(`object`) === sandbox.object); 399 | assert.ok(vm.run(`object instanceof Object`)); 400 | assert.ok(vm.run(`object`) instanceof Object); 401 | assert.ok(vm.run(`object.__proto__ === Object.prototype`)); 402 | assert.ok(vm.run(`object`).__proto__ === Object.prototype); 403 | 404 | assert.ok(vm.run(`func`) === sandbox.func); 405 | assert.ok(vm.run(`func instanceof Function`)); 406 | assert.ok(vm.run(`func`) instanceof Function); 407 | assert.ok(vm.run(`func.__proto__ === Function.prototype`)); 408 | assert.ok(vm.run(`func`).__proto__ === Function.prototype); 409 | 410 | assert.ok(vm.run(`new func() instanceof func`)); 411 | assert.ok(vm.run(`new func()`) instanceof sandbox.func); 412 | assert.ok(vm.run(`new func().__proto__ === func.prototype`)); 413 | assert.ok(vm.run(`new func()`).__proto__ === sandbox.func.prototype); 414 | 415 | assert.ok(vm.run(`buffer`) === sandbox.buffer); 416 | assert.ok(vm.run(`buffer instanceof Buffer`)); 417 | assert.ok(vm.run(`buffer`) instanceof Buffer); 418 | assert.ok(vm.run(`buffer.__proto__ === Buffer.prototype`)); 419 | assert.ok(vm.run(`buffer`).__proto__ === Buffer.prototype); 420 | assert.ok(vm.run(`buffer.slice(0, 1) instanceof Buffer`)); 421 | assert.ok(vm.run(`buffer.slice(0, 1)`) instanceof Buffer); 422 | ``` 423 | 424 | ## CLI 425 | 426 | Before you can use vm2 in the command line, install it globally with `npm install vm2 -g`. 427 | 428 | ```sh 429 | vm2 ./script.js 430 | ``` 431 | 432 | ## Known Issues 433 | 434 | * **There are known security issues to circumvent the sandbox.** 435 | * It is not possible to define a class that extends a proxied class. This includes using a proxied class in `Object.create`. 436 | * Direct eval does not work. 437 | * Logging sandbox arrays will repeat the array part in the properties. 438 | * Source code transformations can result a different source string for a function. 439 | * There are ways to crash the node process from inside the sandbox. 440 | 441 | ## Deployment 442 | 443 | 1. Update the `CHANGELOG.md` 444 | 2. Update the `package.json` version number 445 | 3. Commit the changes 446 | 4. Run `npm publish` 447 | 448 | ## Sponsors 449 | 450 | [![Integromat][integromat-image]][integromat-url] 451 | 452 | [npm-image]: https://img.shields.io/npm/v/vm2.svg?style=flat-square 453 | [npm-url]: https://www.npmjs.com/package/vm2 454 | [downloads-image]: https://img.shields.io/npm/dm/vm2.svg?style=flat-square 455 | [downloads-url]: https://www.npmjs.com/package/vm2 456 | [quality-image]: http://npm.packagequality.com/shield/vm2.svg?style=flat-square 457 | [quality-url]: http://packagequality.com/#?package=vm2 458 | [travis-image]: https://img.shields.io/travis/patriksimek/vm2/master.svg?style=flat-square&label=unit 459 | [travis-url]: https://travis-ci.org/patriksimek/vm2 460 | [snyk-image]: https://snyk.io/test/github/patriksimek/vm2/badge.svg 461 | [snyk-url]: https://snyk.io/test/github/patriksimek/vm2 462 | [integromat-image]: https://static.integromat.com/logo/45_text.png 463 | [integromat-url]: https://www.integromat.com 464 | 465 |
466 | -------------------------------------------------------------------------------- /bin/vm2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require(__dirname +'/../lib/cli.js'); 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import fs from 'fs'; 3 | import pa from 'path'; 4 | 5 | /** 6 | * Interface for nodes fs module 7 | */ 8 | export interface VMFS { 9 | /** Implements fs.statSync */ 10 | statSync: typeof fs.statSync; 11 | /** Implements fs.readFileSync */ 12 | readFileSync: typeof fs.readFileSync; 13 | } 14 | 15 | /** 16 | * Interface for nodes path module 17 | */ 18 | export interface VMPath { 19 | /** Implements path.resolve */ 20 | resolve: typeof pa.resolve; 21 | /** Implements path.isAbsolute */ 22 | isAbsolute: typeof pa.isAbsolute; 23 | /** Implements path.join */ 24 | join: typeof pa.join; 25 | /** Implements path.basename */ 26 | basename: typeof pa.basename; 27 | /** Implements path.dirname */ 28 | dirname: typeof pa.dirname; 29 | } 30 | 31 | /** 32 | * Custom file system which abstracts functions from node's fs and path modules. 33 | */ 34 | export interface VMFileSystemInterface extends VMFS, VMPath { 35 | /** Implements (sep) => sep === path.sep */ 36 | isSeparator(char: string): boolean; 37 | } 38 | 39 | /** 40 | * Implementation of a default file system. 41 | */ 42 | export class VMFileSystem implements VMFileSystemInterface { 43 | constructor(options?: { fs?: VMFS, path?: VMPath }); 44 | /** Implements fs.statSync */ 45 | statSync: typeof fs.statSync; 46 | /** Implements fs.readFileSync */ 47 | readFileSync: typeof fs.readFileSync; 48 | /** Implements path.resolve */ 49 | resolve: typeof pa.resolve; 50 | /** Implements path.isAbsolute */ 51 | isAbsolute: typeof pa.isAbsolute; 52 | /** Implements path.join */ 53 | join: typeof pa.join; 54 | /** Implements path.basename */ 55 | basename: typeof pa.basename; 56 | /** Implements path.dirname */ 57 | dirname: typeof pa.dirname; 58 | /** Implements (sep) => sep === path.sep */ 59 | isSeparator(char: string): boolean; 60 | } 61 | 62 | /** 63 | * Function that will be called to load a built-in into a vm. 64 | */ 65 | export type BuiltinLoad = (vm: NodeVM) => any; 66 | /** 67 | * Either a function that will be called to load a built-in into a vm or an object with a init method and a load method to load the built-in. 68 | */ 69 | export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad}; 70 | /** 71 | * Require method 72 | */ 73 | export type HostRequire = (id: string) => any; 74 | 75 | /** 76 | * This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided. 77 | */ 78 | export type PathContextCallback = (modulePath: string, extensionType: string) => 'host' | 'sandbox'; 79 | 80 | /** 81 | * Require options for a VM 82 | */ 83 | export interface VMRequire { 84 | /** 85 | * Array of allowed built-in modules, accepts ["*"] for all. Using "*" increases the attack surface and potential 86 | * new modules allow to escape the sandbox. (default: none) 87 | */ 88 | builtin?: readonly string[]; 89 | /* 90 | * `host` (default) to require modules in host and proxy them to sandbox. `sandbox` to load, compile and 91 | * require modules in sandbox or a callback which chooses the context based on the filename. 92 | * Built-in modules except `events` always required in host and proxied to sandbox 93 | */ 94 | context?: "host" | "sandbox" | PathContextCallback; 95 | /** `true`, an array of allowed external modules or an object with external options (default: `false`) */ 96 | external?: boolean | readonly string[] | { modules: readonly string[], transitive: boolean }; 97 | /** Array of modules to be loaded into NodeVM on start. */ 98 | import?: readonly string[]; 99 | /** Restricted path(s) where local modules can be required (default: every path). */ 100 | root?: string | readonly string[]; 101 | /** Collection of mock modules (both external or built-in). */ 102 | mock?: any; 103 | /* An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. */ 104 | resolve?: (moduleName: string, parentDirname: string) => string | { path: string, module?: string } | undefined; 105 | /** Custom require to require host and built-in modules. */ 106 | customRequire?: HostRequire; 107 | /** Load modules in strict mode. (default: true) */ 108 | strict?: boolean; 109 | /** FileSystem to load files from */ 110 | fs?: VMFileSystemInterface; 111 | } 112 | 113 | /** 114 | * A custom compiler function for all of the JS that comes 115 | * into the VM 116 | */ 117 | export type CompilerFunction = (code: string, filename: string) => string; 118 | 119 | export abstract class Resolver { 120 | private constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Map); 121 | } 122 | 123 | /** 124 | * Create a resolver as normal `NodeVM` does given `VMRequire` options. 125 | * 126 | * @param options The options that would have been given to `NodeVM`. 127 | * @param override Custom overrides for built-ins. 128 | * @param compiler Compiler to be used for loaded modules. 129 | */ 130 | export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver; 131 | 132 | /** 133 | * Options for creating a VM 134 | */ 135 | export interface VMOptions { 136 | /** 137 | * `javascript` (default) or `coffeescript` or custom compiler function (which receives the code, and it's file path). 138 | * The library expects you to have coffee-script pre-installed if the compiler is set to `coffeescript`. 139 | */ 140 | compiler?: "javascript" | "coffeescript" | CompilerFunction; 141 | /** VM's global object. */ 142 | sandbox?: any; 143 | /** 144 | * Script timeout in milliseconds. Timeout is only effective on code you run through `run`. 145 | * Timeout is NOT effective on any method returned by VM. 146 | */ 147 | timeout?: number; 148 | /** 149 | * If set to `false` any calls to eval or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an 150 | * `EvalError` (default: `true`). 151 | */ 152 | eval?: boolean; 153 | /** 154 | * If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`). 155 | */ 156 | wasm?: boolean; 157 | /** 158 | * If set to `true` any attempt to run code using async will throw a `VMError` (default: `false`). 159 | * @deprecated Use `allowAsync` instead. 160 | */ 161 | fixAsync?: boolean; 162 | 163 | /** 164 | * If set to `false` any attempt to run code using async will throw a `VMError` (default: `true`). 165 | */ 166 | allowAsync?: boolean; 167 | } 168 | 169 | /** 170 | * Options for creating a NodeVM 171 | */ 172 | export interface NodeVMOptions extends VMOptions { 173 | /** `inherit` to enable console, `redirect` to redirect to events, `off` to disable console (default: `inherit`). */ 174 | console?: "inherit" | "redirect" | "off"; 175 | /** `true` or an object to enable `require` options (default: `false`). */ 176 | require?: boolean | VMRequire | Resolver; 177 | /** 178 | * **WARNING**: This should be disabled. It allows to create a NodeVM form within the sandbox which could return any host module. 179 | * `true` to enable VMs nesting (default: `false`). 180 | */ 181 | nesting?: boolean; 182 | /** `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script. */ 183 | wrapper?: "commonjs" | "none"; 184 | /** File extensions that the internal module resolver should accept. */ 185 | sourceExtensions?: readonly string[]; 186 | /** 187 | * Array of arguments passed to `process.argv`. 188 | * This object will not be copied and the script can change this object. 189 | */ 190 | argv?: string[]; 191 | /** 192 | * Environment map passed to `process.env`. 193 | * This object will not be copied and the script can change this object. 194 | */ 195 | env?: any; 196 | /** Run modules in strict mode. Required modules are always strict. */ 197 | strict?: boolean; 198 | } 199 | 200 | /** 201 | * VM is a simple sandbox, without `require` feature, to synchronously run an untrusted code. 202 | * Only JavaScript built-in objects + Buffer are available. Scheduling functions 203 | * (`setInterval`, `setTimeout` and `setImmediate`) are not available by default. 204 | */ 205 | export class VM { 206 | constructor(options?: VMOptions); 207 | /** Direct access to the global sandbox object */ 208 | readonly sandbox: any; 209 | /** Timeout to use for the run methods */ 210 | timeout?: number; 211 | /** Runs the code */ 212 | run(script: string | VMScript, options?: string | { filename?: string }): any; 213 | /** Runs the code in the specific file */ 214 | runFile(filename: string): any; 215 | /** Loads all the values into the global object with the same names */ 216 | setGlobals(values: any): this; 217 | /** Make a object visible as a global with a specific name */ 218 | setGlobal(name: string, value: any): this; 219 | /** Get the global object with the specific name */ 220 | getGlobal(name: string): any; 221 | /** Freezes the object inside VM making it read-only. Not available for primitive values. */ 222 | freeze(object: any, name?: string): any; 223 | /** Freezes the object inside VM making it read-only. Not available for primitive values. */ 224 | readonly(object: any): any; 225 | /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values */ 226 | protect(object: any, name?: string): any; 227 | } 228 | 229 | /** 230 | * A VM with behavior more similar to running inside Node. 231 | */ 232 | export class NodeVM extends EventEmitter implements VM { 233 | constructor(options?: NodeVMOptions); 234 | 235 | /** Require a module in VM and return it's exports. */ 236 | require(module: string): any; 237 | 238 | /** 239 | * Create NodeVM and run code inside it. 240 | * 241 | * @param {string} script JavaScript code. 242 | * @param {string} [filename] File name (used in stack traces only). 243 | * @param {Object} [options] VM options. 244 | */ 245 | static code(script: string, filename?: string, options?: NodeVMOptions): any; 246 | 247 | /** 248 | * Create NodeVM and run script from file inside it. 249 | * 250 | * @param {string} [filename] File name (used in stack traces only). 251 | * @param {Object} [options] VM options. 252 | */ 253 | static file(filename: string, options?: NodeVMOptions): any; 254 | 255 | /** Direct access to the global sandbox object */ 256 | readonly sandbox: any; 257 | /** Only here because of implements VM. Does nothing. */ 258 | timeout?: number; 259 | /** The resolver used to resolve modules */ 260 | readonly resolver: Resolver; 261 | /** Runs the code */ 262 | run(js: string | VMScript, options?: string | { filename?: string, wrapper?: "commonjs" | "none", strict?: boolean }): any; 263 | /** Runs the code in the specific file */ 264 | runFile(filename: string): any; 265 | /** Loads all the values into the global object with the same names */ 266 | setGlobals(values: any): this; 267 | /** Make a object visible as a global with a specific name */ 268 | setGlobal(name: string, value: any): this; 269 | /** Get the global object with the specific name */ 270 | getGlobal(name: string): any; 271 | /** Freezes the object inside VM making it read-only. Not available for primitive values. */ 272 | freeze(object: any, name?: string): any; 273 | /** Freezes the object inside VM making it read-only. Not available for primitive values. */ 274 | readonly(object: any): any; 275 | /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values */ 276 | protect(object: any, name?: string): any; 277 | } 278 | 279 | /** 280 | * You can increase performance by using pre-compiled scripts. 281 | * The pre-compiled VMScript can be run later multiple times. It is important to note that the code is not bound 282 | * to any VM (context); rather, it is bound before each run, just for that run. 283 | */ 284 | export class VMScript { 285 | constructor(code: string, path: string, options?: { 286 | lineOffset?: number; 287 | columnOffset?: number; 288 | compiler?: "javascript" | "coffeescript" | CompilerFunction; 289 | }); 290 | constructor(code: string, options?: { 291 | filename?: string, 292 | lineOffset?: number; 293 | columnOffset?: number; 294 | compiler?: "javascript" | "coffeescript" | CompilerFunction; 295 | }); 296 | readonly code: string; 297 | readonly filename: string; 298 | readonly lineOffset: number; 299 | readonly columnOffset: number; 300 | readonly compiler: "javascript" | "coffeescript" | CompilerFunction; 301 | /** 302 | * Wraps the code 303 | * @deprecated 304 | */ 305 | wrap(prefix: string, postfix: string): this; 306 | /** Compiles the code. If called multiple times, the code is only compiled once. */ 307 | compile(): this; 308 | } 309 | 310 | /** Custom Error class */ 311 | export class VMError extends Error { } 312 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | if (parseInt(process.versions.node.split('.')[0]) < 6) throw new Error('vm2 requires Node.js version 6 or newer.'); 2 | 3 | module.exports = require('./lib/main'); 4 | -------------------------------------------------------------------------------- /lib/builtin.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const nmod = require('module'); 4 | const {EventEmitter} = require('events'); 5 | const util = require('util'); 6 | const {VMScript} = require('./script'); 7 | const {VM} = require('./vm'); 8 | 9 | const eventsModules = new WeakMap(); 10 | 11 | function defaultBuiltinLoaderEvents(vm) { 12 | return eventsModules.get(vm); 13 | } 14 | 15 | let cacheBufferScript; 16 | 17 | function defaultBuiltinLoaderBuffer(vm) { 18 | if (!cacheBufferScript) { 19 | cacheBufferScript = new VMScript('return buffer=>({Buffer: buffer});', {__proto__: null, filename: 'buffer.js'}); 20 | } 21 | const makeBuffer = vm.run(cacheBufferScript, {__proto__: null, strict: true, wrapper: 'none'}); 22 | return makeBuffer(Buffer); 23 | } 24 | 25 | let cacheUtilScript; 26 | 27 | function defaultBuiltinLoaderUtil(vm) { 28 | if (!cacheUtilScript) { 29 | cacheUtilScript = new VMScript(`return function inherits(ctor, superCtor) { 30 | ctor.super_ = superCtor; 31 | Object.setPrototypeOf(ctor.prototype, superCtor.prototype); 32 | }`, {__proto__: null, filename: 'util.js'}); 33 | } 34 | const inherits = vm.run(cacheUtilScript, {__proto__: null, strict: true, wrapper: 'none'}); 35 | const copy = Object.assign({}, util); 36 | copy.inherits = inherits; 37 | return vm.readonly(copy); 38 | } 39 | 40 | const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives'))).filter(s=>!s.startsWith('internal/')); 41 | 42 | let EventEmitterReferencingAsyncResourceClass = null; 43 | if (EventEmitter.EventEmitterAsyncResource) { 44 | // eslint-disable-next-line global-require 45 | const {AsyncResource} = require('async_hooks'); 46 | const kEventEmitter = Symbol('kEventEmitter'); 47 | class EventEmitterReferencingAsyncResource extends AsyncResource { 48 | constructor(ee, type, options) { 49 | super(type, options); 50 | this[kEventEmitter] = ee; 51 | } 52 | get eventEmitter() { 53 | return this[kEventEmitter]; 54 | } 55 | } 56 | EventEmitterReferencingAsyncResourceClass = EventEmitterReferencingAsyncResource; 57 | } 58 | 59 | let cacheEventsScript; 60 | 61 | const SPECIAL_MODULES = { 62 | events: { 63 | init(vm) { 64 | if (!cacheEventsScript) { 65 | const eventsSource = fs.readFileSync(`${__dirname}/events.js`, 'utf8'); 66 | cacheEventsScript = new VMScript(`(function (fromhost) { const module = {}; module.exports={};{ ${eventsSource} 67 | } return module.exports;})`, {filename: 'events.js'}); 68 | } 69 | const closure = VM.prototype.run.call(vm, cacheEventsScript); 70 | const eventsInstance = closure(vm.readonly({ 71 | kErrorMonitor: EventEmitter.errorMonitor, 72 | once: EventEmitter.once, 73 | on: EventEmitter.on, 74 | getEventListeners: EventEmitter.getEventListeners, 75 | EventEmitterReferencingAsyncResource: EventEmitterReferencingAsyncResourceClass 76 | })); 77 | eventsModules.set(vm, eventsInstance); 78 | vm._addProtoMapping(EventEmitter.prototype, eventsInstance.EventEmitter.prototype); 79 | }, 80 | load: defaultBuiltinLoaderEvents 81 | }, 82 | buffer: defaultBuiltinLoaderBuffer, 83 | util: defaultBuiltinLoaderUtil 84 | }; 85 | 86 | function addDefaultBuiltin(builtins, key, hostRequire) { 87 | if (builtins.has(key)) return; 88 | const special = SPECIAL_MODULES[key]; 89 | builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key))); 90 | } 91 | 92 | 93 | function makeBuiltinsFromLegacyOptions(builtins, hostRequire, mocks, overrides) { 94 | const res = new Map(); 95 | if (mocks) { 96 | const keys = Object.getOwnPropertyNames(mocks); 97 | for (let i = 0; i < keys.length; i++) { 98 | const key = keys[i]; 99 | res.set(key, (tvm) => tvm.readonly(mocks[key])); 100 | } 101 | } 102 | if (overrides) { 103 | const keys = Object.getOwnPropertyNames(overrides); 104 | for (let i = 0; i < keys.length; i++) { 105 | const key = keys[i]; 106 | res.set(key, overrides[key]); 107 | } 108 | } 109 | if (Array.isArray(builtins)) { 110 | const def = builtins.indexOf('*') >= 0; 111 | if (def) { 112 | for (let i = 0; i < BUILTIN_MODULES.length; i++) { 113 | const name = BUILTIN_MODULES[i]; 114 | if (builtins.indexOf(`-${name}`) === -1) { 115 | addDefaultBuiltin(res, name, hostRequire); 116 | } 117 | } 118 | } else { 119 | for (let i = 0; i < BUILTIN_MODULES.length; i++) { 120 | const name = BUILTIN_MODULES[i]; 121 | if (builtins.indexOf(name) !== -1) { 122 | addDefaultBuiltin(res, name, hostRequire); 123 | } 124 | } 125 | } 126 | } else if (builtins) { 127 | for (let i = 0; i < BUILTIN_MODULES.length; i++) { 128 | const name = BUILTIN_MODULES[i]; 129 | if (builtins[name]) { 130 | addDefaultBuiltin(res, name, hostRequire); 131 | } 132 | } 133 | } 134 | return res; 135 | } 136 | 137 | function makeBuiltins(builtins, hostRequire) { 138 | const res = new Map(); 139 | for (let i = 0; i < builtins.length; i++) { 140 | const name = builtins[i]; 141 | addDefaultBuiltin(res, name, hostRequire); 142 | } 143 | return res; 144 | } 145 | 146 | exports.makeBuiltinsFromLegacyOptions = makeBuiltinsFromLegacyOptions; 147 | exports.makeBuiltins = makeBuiltins; 148 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pa = require('path'); 4 | 5 | const {NodeVM, VMError} = require('../'); 6 | 7 | if (process.argv[2]) { 8 | const path = pa.resolve(process.argv[2]); 9 | 10 | console.log(`\x1B[90m[vm] creating VM for ${path}\x1B[39m`); 11 | const started = Date.now(); 12 | 13 | try { 14 | NodeVM.file(path, { 15 | verbose: true, 16 | require: { 17 | external: true 18 | } 19 | }); 20 | 21 | console.log(`\x1B[90m[vm] VM completed in ${Date.now() - started}ms\x1B[39m`); 22 | } catch (ex) { 23 | if (ex instanceof VMError) { 24 | console.error(`\x1B[31m[vm:error] ${ex.message}\x1B[39m`); 25 | } else { 26 | const {stack} = ex; 27 | 28 | if (stack) { 29 | console.error(`\x1B[31m[vm:error] ${stack}\x1B[39m`); 30 | } else { 31 | console.error(`\x1B[31m[vm:error] ${ex}\x1B[39m`); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | VMError 5 | } = require('./bridge'); 6 | 7 | let cacheCoffeeScriptCompiler; 8 | 9 | /** 10 | * Returns the cached coffee script compiler or loads it 11 | * if it is not found in the cache. 12 | * 13 | * @private 14 | * @return {compileCallback} The coffee script compiler. 15 | * @throws {VMError} If the coffee-script module can't be found. 16 | */ 17 | function getCoffeeScriptCompiler() { 18 | if (!cacheCoffeeScriptCompiler) { 19 | try { 20 | // The warning generated by webpack can be disabled by setting: 21 | // ignoreWarnings[].message = /Can't resolve 'coffee-script'/ 22 | /* eslint-disable-next-line global-require */ 23 | const coffeeScript = require('coffee-script'); 24 | cacheCoffeeScriptCompiler = (code, filename) => { 25 | return coffeeScript.compile(code, {header: false, bare: true}); 26 | }; 27 | } catch (e) { 28 | throw new VMError('Coffee-Script compiler is not installed.'); 29 | } 30 | } 31 | return cacheCoffeeScriptCompiler; 32 | } 33 | 34 | /** 35 | * Remove the shebang from source code. 36 | * 37 | * @private 38 | * @param {string} code - Code from which to remove the shebang. 39 | * @return {string} code without the shebang. 40 | */ 41 | function removeShebang(code) { 42 | if (!code.startsWith('#!')) return code; 43 | return '//' + code.substring(2); 44 | } 45 | 46 | 47 | /** 48 | * The JavaScript compiler, just a identity function. 49 | * 50 | * @private 51 | * @type {compileCallback} 52 | * @param {string} code - The JavaScript code. 53 | * @param {string} filename - Filename of this script. 54 | * @return {string} The code. 55 | */ 56 | function jsCompiler(code, filename) { 57 | return removeShebang(code); 58 | } 59 | 60 | /** 61 | * Look up the compiler for a specific name. 62 | * 63 | * @private 64 | * @param {(string|compileCallback)} compiler - A compile callback or the name of the compiler. 65 | * @return {compileCallback} The resolved compiler. 66 | * @throws {VMError} If the compiler is unknown or the coffee script module was needed and couldn't be found. 67 | */ 68 | function lookupCompiler(compiler) { 69 | if ('function' === typeof compiler) return compiler; 70 | switch (compiler) { 71 | case 'coffeescript': 72 | case 'coffee-script': 73 | case 'cs': 74 | case 'text/coffeescript': 75 | return getCoffeeScriptCompiler(); 76 | case 'javascript': 77 | case 'java-script': 78 | case 'js': 79 | case 'text/javascript': 80 | return jsCompiler; 81 | default: 82 | throw new VMError(`Unsupported compiler '${compiler}'.`); 83 | } 84 | } 85 | 86 | exports.removeShebang = removeShebang; 87 | exports.lookupCompiler = lookupCompiler; 88 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | // Modified by the vm2 team to make this a standalone module to be loaded into the sandbox. 23 | 24 | 'use strict'; 25 | 26 | const host = fromhost; 27 | 28 | const { 29 | Boolean, 30 | Error, 31 | String, 32 | Symbol 33 | } = globalThis; 34 | 35 | const ReflectApply = Reflect.apply; 36 | const ReflectOwnKeys = Reflect.ownKeys; 37 | 38 | const ErrorCaptureStackTrace = Error.captureStackTrace; 39 | 40 | const NumberIsNaN = Number.isNaN; 41 | 42 | const ObjectCreate = Object.create; 43 | const ObjectDefineProperty = Object.defineProperty; 44 | const ObjectDefineProperties = Object.defineProperties; 45 | const ObjectGetPrototypeOf = Object.getPrototypeOf; 46 | 47 | const SymbolFor = Symbol.for; 48 | 49 | function uncurryThis(func) { 50 | return (thiz, ...args) => ReflectApply(func, thiz, args); 51 | } 52 | 53 | const ArrayPrototypeIndexOf = uncurryThis(Array.prototype.indexOf); 54 | const ArrayPrototypeJoin = uncurryThis(Array.prototype.join); 55 | const ArrayPrototypeSlice = uncurryThis(Array.prototype.slice); 56 | const ArrayPrototypeSplice = uncurryThis(Array.prototype.splice); 57 | const ArrayPrototypeUnshift = uncurryThis(Array.prototype.unshift); 58 | 59 | const kRejection = SymbolFor('nodejs.rejection'); 60 | 61 | function inspect(obj) { 62 | return typeof obj === 'symbol' ? obj.toString() : `${obj}`; 63 | } 64 | 65 | function spliceOne(list, index) { 66 | for (; index + 1 < list.length; index++) 67 | list[index] = list[index + 1]; 68 | list.pop(); 69 | } 70 | 71 | function assert(what, message) { 72 | if (!what) throw new Error(message); 73 | } 74 | 75 | function E(key, msg, Base) { 76 | return function NodeError(...args) { 77 | const error = new Base(); 78 | const message = ReflectApply(msg, error, args); 79 | ObjectDefineProperties(error, { 80 | message: { 81 | value: message, 82 | enumerable: false, 83 | writable: true, 84 | configurable: true, 85 | }, 86 | toString: { 87 | value() { 88 | return `${this.name} [${key}]: ${this.message}`; 89 | }, 90 | enumerable: false, 91 | writable: true, 92 | configurable: true, 93 | }, 94 | }); 95 | error.code = key; 96 | return error; 97 | }; 98 | } 99 | 100 | 101 | const ERR_INVALID_ARG_TYPE = E('ERR_INVALID_ARG_TYPE', 102 | (name, expected, actual) => { 103 | assert(typeof name === 'string', "'name' must be a string"); 104 | if (!ArrayIsArray(expected)) { 105 | expected = [expected]; 106 | } 107 | 108 | let msg = 'The '; 109 | if (StringPrototypeEndsWith(name, ' argument')) { 110 | // For cases like 'first argument' 111 | msg += `${name} `; 112 | } else { 113 | const type = StringPrototypeIncludes(name, '.') ? 'property' : 'argument'; 114 | msg += `"${name}" ${type} `; 115 | } 116 | msg += 'must be '; 117 | 118 | const types = []; 119 | const instances = []; 120 | const other = []; 121 | 122 | for (const value of expected) { 123 | assert(typeof value === 'string', 124 | 'All expected entries have to be of type string'); 125 | if (ArrayPrototypeIncludes(kTypes, value)) { 126 | ArrayPrototypePush(types, StringPrototypeToLowerCase(value)); 127 | } else if (RegExpPrototypeTest(classRegExp, value)) { 128 | ArrayPrototypePush(instances, value); 129 | } else { 130 | assert(value !== 'object', 131 | 'The value "object" should be written as "Object"'); 132 | ArrayPrototypePush(other, value); 133 | } 134 | } 135 | 136 | // Special handle `object` in case other instances are allowed to outline 137 | // the differences between each other. 138 | if (instances.length > 0) { 139 | const pos = ArrayPrototypeIndexOf(types, 'object'); 140 | if (pos !== -1) { 141 | ArrayPrototypeSplice(types, pos, 1); 142 | ArrayPrototypePush(instances, 'Object'); 143 | } 144 | } 145 | 146 | if (types.length > 0) { 147 | if (types.length > 2) { 148 | const last = ArrayPrototypePop(types); 149 | msg += `one of type ${ArrayPrototypeJoin(types, ', ')}, or ${last}`; 150 | } else if (types.length === 2) { 151 | msg += `one of type ${types[0]} or ${types[1]}`; 152 | } else { 153 | msg += `of type ${types[0]}`; 154 | } 155 | if (instances.length > 0 || other.length > 0) 156 | msg += ' or '; 157 | } 158 | 159 | if (instances.length > 0) { 160 | if (instances.length > 2) { 161 | const last = ArrayPrototypePop(instances); 162 | msg += 163 | `an instance of ${ArrayPrototypeJoin(instances, ', ')}, or ${last}`; 164 | } else { 165 | msg += `an instance of ${instances[0]}`; 166 | if (instances.length === 2) { 167 | msg += ` or ${instances[1]}`; 168 | } 169 | } 170 | if (other.length > 0) 171 | msg += ' or '; 172 | } 173 | 174 | if (other.length > 0) { 175 | if (other.length > 2) { 176 | const last = ArrayPrototypePop(other); 177 | msg += `one of ${ArrayPrototypeJoin(other, ', ')}, or ${last}`; 178 | } else if (other.length === 2) { 179 | msg += `one of ${other[0]} or ${other[1]}`; 180 | } else { 181 | if (StringPrototypeToLowerCase(other[0]) !== other[0]) 182 | msg += 'an '; 183 | msg += `${other[0]}`; 184 | } 185 | } 186 | 187 | if (actual == null) { 188 | msg += `. Received ${actual}`; 189 | } else if (typeof actual === 'function' && actual.name) { 190 | msg += `. Received function ${actual.name}`; 191 | } else if (typeof actual === 'object') { 192 | if (actual.constructor && actual.constructor.name) { 193 | msg += `. Received an instance of ${actual.constructor.name}`; 194 | } else { 195 | const inspected = inspect(actual, { depth: -1 }); 196 | msg += `. Received ${inspected}`; 197 | } 198 | } else { 199 | let inspected = inspect(actual, { colors: false }); 200 | if (inspected.length > 25) 201 | inspected = `${StringPrototypeSlice(inspected, 0, 25)}...`; 202 | msg += `. Received type ${typeof actual} (${inspected})`; 203 | } 204 | return msg; 205 | }, TypeError); 206 | 207 | const ERR_INVALID_THIS = E('ERR_INVALID_THIS', s => `Value of "this" must be of type ${s}`, TypeError); 208 | 209 | const ERR_OUT_OF_RANGE = E('ERR_OUT_OF_RANGE', 210 | (str, range, input, replaceDefaultBoolean = false) => { 211 | assert(range, 'Missing "range" argument'); 212 | let msg = replaceDefaultBoolean ? str : 213 | `The value of "${str}" is out of range.`; 214 | const received = inspect(input); 215 | msg += ` It must be ${range}. Received ${received}`; 216 | return msg; 217 | }, RangeError); 218 | 219 | const ERR_UNHANDLED_ERROR = E('ERR_UNHANDLED_ERROR', 220 | err => { 221 | const msg = 'Unhandled error.'; 222 | if (err === undefined) return msg; 223 | return `${msg} (${err})`; 224 | }, Error); 225 | 226 | function validateBoolean(value, name) { 227 | if (typeof value !== 'boolean') 228 | throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value); 229 | } 230 | 231 | function validateFunction(value, name) { 232 | if (typeof value !== 'function') 233 | throw new ERR_INVALID_ARG_TYPE(name, 'Function', value); 234 | } 235 | 236 | function validateString(value, name) { 237 | if (typeof value !== 'string') 238 | throw new ERR_INVALID_ARG_TYPE(name, 'string', value); 239 | } 240 | 241 | function nc(cond, e) { 242 | return cond === undefined || cond === null ? e : cond; 243 | } 244 | 245 | function oc(base, key) { 246 | return base === undefined || base === null ? undefined : base[key]; 247 | } 248 | 249 | const kCapture = Symbol('kCapture'); 250 | const kErrorMonitor = host.kErrorMonitor || Symbol('events.errorMonitor'); 251 | const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners'); 252 | const kMaxEventTargetListenersWarned = 253 | Symbol('events.maxEventTargetListenersWarned'); 254 | 255 | const kIsEventTarget = SymbolFor('nodejs.event_target'); 256 | 257 | function isEventTarget(obj) { 258 | return oc(oc(obj, 'constructor'), kIsEventTarget); 259 | } 260 | 261 | /** 262 | * Creates a new `EventEmitter` instance. 263 | * @param {{ captureRejections?: boolean; }} [opts] 264 | * @constructs {EventEmitter} 265 | */ 266 | function EventEmitter(opts) { 267 | EventEmitter.init.call(this, opts); 268 | } 269 | module.exports = EventEmitter; 270 | if (host.once) module.exports.once = host.once; 271 | if (host.on) module.exports.on = host.on; 272 | if (host.getEventListeners) module.exports.getEventListeners = host.getEventListeners; 273 | // Backwards-compat with node 0.10.x 274 | EventEmitter.EventEmitter = EventEmitter; 275 | 276 | EventEmitter.usingDomains = false; 277 | 278 | EventEmitter.captureRejectionSymbol = kRejection; 279 | ObjectDefineProperty(EventEmitter, 'captureRejections', { 280 | get() { 281 | return EventEmitter.prototype[kCapture]; 282 | }, 283 | set(value) { 284 | validateBoolean(value, 'EventEmitter.captureRejections'); 285 | 286 | EventEmitter.prototype[kCapture] = value; 287 | }, 288 | enumerable: true 289 | }); 290 | 291 | if (host.EventEmitterReferencingAsyncResource) { 292 | const kAsyncResource = Symbol('kAsyncResource'); 293 | const EventEmitterReferencingAsyncResource = host.EventEmitterReferencingAsyncResource; 294 | 295 | class EventEmitterAsyncResource extends EventEmitter { 296 | /** 297 | * @param {{ 298 | * name?: string, 299 | * triggerAsyncId?: number, 300 | * requireManualDestroy?: boolean, 301 | * }} [options] 302 | */ 303 | constructor(options = undefined) { 304 | let name; 305 | if (typeof options === 'string') { 306 | name = options; 307 | options = undefined; 308 | } else { 309 | if (new.target === EventEmitterAsyncResource) { 310 | validateString(oc(options, 'name'), 'options.name'); 311 | } 312 | name = oc(options, 'name') || new.target.name; 313 | } 314 | super(options); 315 | 316 | this[kAsyncResource] = 317 | new EventEmitterReferencingAsyncResource(this, name, options); 318 | } 319 | 320 | /** 321 | * @param {symbol,string} event 322 | * @param {...any} args 323 | * @returns {boolean} 324 | */ 325 | emit(event, ...args) { 326 | if (this[kAsyncResource] === undefined) 327 | throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); 328 | const { asyncResource } = this; 329 | ArrayPrototypeUnshift(args, super.emit, this, event); 330 | return ReflectApply(asyncResource.runInAsyncScope, asyncResource, 331 | args); 332 | } 333 | 334 | /** 335 | * @returns {void} 336 | */ 337 | emitDestroy() { 338 | if (this[kAsyncResource] === undefined) 339 | throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); 340 | this.asyncResource.emitDestroy(); 341 | } 342 | 343 | /** 344 | * @type {number} 345 | */ 346 | get asyncId() { 347 | if (this[kAsyncResource] === undefined) 348 | throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); 349 | return this.asyncResource.asyncId(); 350 | } 351 | 352 | /** 353 | * @type {number} 354 | */ 355 | get triggerAsyncId() { 356 | if (this[kAsyncResource] === undefined) 357 | throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); 358 | return this.asyncResource.triggerAsyncId(); 359 | } 360 | 361 | /** 362 | * @type {EventEmitterReferencingAsyncResource} 363 | */ 364 | get asyncResource() { 365 | if (this[kAsyncResource] === undefined) 366 | throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); 367 | return this[kAsyncResource]; 368 | } 369 | } 370 | EventEmitter.EventEmitterAsyncResource = EventEmitterAsyncResource; 371 | } 372 | 373 | EventEmitter.errorMonitor = kErrorMonitor; 374 | 375 | // The default for captureRejections is false 376 | ObjectDefineProperty(EventEmitter.prototype, kCapture, { 377 | value: false, 378 | writable: true, 379 | enumerable: false 380 | }); 381 | 382 | EventEmitter.prototype._events = undefined; 383 | EventEmitter.prototype._eventsCount = 0; 384 | EventEmitter.prototype._maxListeners = undefined; 385 | 386 | // By default EventEmitters will print a warning if more than 10 listeners are 387 | // added to it. This is a useful default which helps finding memory leaks. 388 | let defaultMaxListeners = 10; 389 | 390 | function checkListener(listener) { 391 | validateFunction(listener, 'listener'); 392 | } 393 | 394 | ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', { 395 | enumerable: true, 396 | get: function() { 397 | return defaultMaxListeners; 398 | }, 399 | set: function(arg) { 400 | if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { 401 | throw new ERR_OUT_OF_RANGE('defaultMaxListeners', 402 | 'a non-negative number', 403 | arg); 404 | } 405 | defaultMaxListeners = arg; 406 | } 407 | }); 408 | 409 | ObjectDefineProperties(EventEmitter, { 410 | kMaxEventTargetListeners: { 411 | value: kMaxEventTargetListeners, 412 | enumerable: false, 413 | configurable: false, 414 | writable: false, 415 | }, 416 | kMaxEventTargetListenersWarned: { 417 | value: kMaxEventTargetListenersWarned, 418 | enumerable: false, 419 | configurable: false, 420 | writable: false, 421 | } 422 | }); 423 | 424 | /** 425 | * Sets the max listeners. 426 | * @param {number} n 427 | * @param {EventTarget[] | EventEmitter[]} [eventTargets] 428 | * @returns {void} 429 | */ 430 | EventEmitter.setMaxListeners = 431 | function(n = defaultMaxListeners, ...eventTargets) { 432 | if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) 433 | throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n); 434 | if (eventTargets.length === 0) { 435 | defaultMaxListeners = n; 436 | } else { 437 | for (let i = 0; i < eventTargets.length; i++) { 438 | const target = eventTargets[i]; 439 | if (isEventTarget(target)) { 440 | target[kMaxEventTargetListeners] = n; 441 | target[kMaxEventTargetListenersWarned] = false; 442 | } else if (typeof target.setMaxListeners === 'function') { 443 | target.setMaxListeners(n); 444 | } else { 445 | throw new ERR_INVALID_ARG_TYPE( 446 | 'eventTargets', 447 | ['EventEmitter', 'EventTarget'], 448 | target); 449 | } 450 | } 451 | } 452 | }; 453 | 454 | // If you're updating this function definition, please also update any 455 | // re-definitions, such as the one in the Domain module (lib/domain.js). 456 | EventEmitter.init = function(opts) { 457 | 458 | if (this._events === undefined || 459 | this._events === ObjectGetPrototypeOf(this)._events) { 460 | this._events = ObjectCreate(null); 461 | this._eventsCount = 0; 462 | } 463 | 464 | this._maxListeners = this._maxListeners || undefined; 465 | 466 | 467 | if (oc(opts, 'captureRejections')) { 468 | validateBoolean(opts.captureRejections, 'options.captureRejections'); 469 | this[kCapture] = Boolean(opts.captureRejections); 470 | } else { 471 | // Assigning the kCapture property directly saves an expensive 472 | // prototype lookup in a very sensitive hot path. 473 | this[kCapture] = EventEmitter.prototype[kCapture]; 474 | } 475 | }; 476 | 477 | function addCatch(that, promise, type, args) { 478 | if (!that[kCapture]) { 479 | return; 480 | } 481 | 482 | // Handle Promises/A+ spec, then could be a getter 483 | // that throws on second use. 484 | try { 485 | const then = promise.then; 486 | 487 | if (typeof then === 'function') { 488 | then.call(promise, undefined, function(err) { 489 | // The callback is called with nextTick to avoid a follow-up 490 | // rejection from this promise. 491 | process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); 492 | }); 493 | } 494 | } catch (err) { 495 | that.emit('error', err); 496 | } 497 | } 498 | 499 | function emitUnhandledRejectionOrErr(ee, err, type, args) { 500 | if (typeof ee[kRejection] === 'function') { 501 | ee[kRejection](err, type, ...args); 502 | } else { 503 | // We have to disable the capture rejections mechanism, otherwise 504 | // we might end up in an infinite loop. 505 | const prev = ee[kCapture]; 506 | 507 | // If the error handler throws, it is not catchable and it 508 | // will end up in 'uncaughtException'. We restore the previous 509 | // value of kCapture in case the uncaughtException is present 510 | // and the exception is handled. 511 | try { 512 | ee[kCapture] = false; 513 | ee.emit('error', err); 514 | } finally { 515 | ee[kCapture] = prev; 516 | } 517 | } 518 | } 519 | 520 | /** 521 | * Increases the max listeners of the event emitter. 522 | * @param {number} n 523 | * @returns {EventEmitter} 524 | */ 525 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { 526 | if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { 527 | throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n); 528 | } 529 | this._maxListeners = n; 530 | return this; 531 | }; 532 | 533 | function _getMaxListeners(that) { 534 | if (that._maxListeners === undefined) 535 | return EventEmitter.defaultMaxListeners; 536 | return that._maxListeners; 537 | } 538 | 539 | /** 540 | * Returns the current max listener value for the event emitter. 541 | * @returns {number} 542 | */ 543 | EventEmitter.prototype.getMaxListeners = function getMaxListeners() { 544 | return _getMaxListeners(this); 545 | }; 546 | 547 | /** 548 | * Synchronously calls each of the listeners registered 549 | * for the event. 550 | * @param {string | symbol} type 551 | * @param {...any} [args] 552 | * @returns {boolean} 553 | */ 554 | EventEmitter.prototype.emit = function emit(type, ...args) { 555 | let doError = (type === 'error'); 556 | 557 | const events = this._events; 558 | if (events !== undefined) { 559 | if (doError && events[kErrorMonitor] !== undefined) 560 | this.emit(kErrorMonitor, ...args); 561 | doError = (doError && events.error === undefined); 562 | } else if (!doError) 563 | return false; 564 | 565 | // If there is no 'error' event listener then throw. 566 | if (doError) { 567 | let er; 568 | if (args.length > 0) 569 | er = args[0]; 570 | if (er instanceof Error) { 571 | try { 572 | const capture = {}; 573 | ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); 574 | } catch (e) {} 575 | 576 | // Note: The comments on the `throw` lines are intentional, they show 577 | // up in Node's output if this results in an unhandled exception. 578 | throw er; // Unhandled 'error' event 579 | } 580 | 581 | let stringifiedEr; 582 | try { 583 | stringifiedEr = inspect(er); 584 | } catch (e) { 585 | stringifiedEr = er; 586 | } 587 | 588 | // At least give some kind of context to the user 589 | const err = new ERR_UNHANDLED_ERROR(stringifiedEr); 590 | err.context = er; 591 | throw err; // Unhandled 'error' event 592 | } 593 | 594 | const handler = events[type]; 595 | 596 | if (handler === undefined) 597 | return false; 598 | 599 | if (typeof handler === 'function') { 600 | const result = handler.apply(this, args); 601 | 602 | // We check if result is undefined first because that 603 | // is the most common case so we do not pay any perf 604 | // penalty 605 | if (result !== undefined && result !== null) { 606 | addCatch(this, result, type, args); 607 | } 608 | } else { 609 | const len = handler.length; 610 | const listeners = arrayClone(handler); 611 | for (let i = 0; i < len; ++i) { 612 | const result = listeners[i].apply(this, args); 613 | 614 | // We check if result is undefined first because that 615 | // is the most common case so we do not pay any perf 616 | // penalty. 617 | // This code is duplicated because extracting it away 618 | // would make it non-inlineable. 619 | if (result !== undefined && result !== null) { 620 | addCatch(this, result, type, args); 621 | } 622 | } 623 | } 624 | 625 | return true; 626 | }; 627 | 628 | function _addListener(target, type, listener, prepend) { 629 | let m; 630 | let events; 631 | let existing; 632 | 633 | checkListener(listener); 634 | 635 | events = target._events; 636 | if (events === undefined) { 637 | events = target._events = ObjectCreate(null); 638 | target._eventsCount = 0; 639 | } else { 640 | // To avoid recursion in the case that type === "newListener"! Before 641 | // adding it to the listeners, first emit "newListener". 642 | if (events.newListener !== undefined) { 643 | target.emit('newListener', type, 644 | nc(listener.listener, listener)); 645 | 646 | // Re-assign `events` because a newListener handler could have caused the 647 | // this._events to be assigned to a new object 648 | events = target._events; 649 | } 650 | existing = events[type]; 651 | } 652 | 653 | if (existing === undefined) { 654 | // Optimize the case of one listener. Don't need the extra array object. 655 | events[type] = listener; 656 | ++target._eventsCount; 657 | } else { 658 | if (typeof existing === 'function') { 659 | // Adding the second element, need to change to array. 660 | existing = events[type] = 661 | prepend ? [listener, existing] : [existing, listener]; 662 | // If we've already got an array, just append. 663 | } else if (prepend) { 664 | existing.unshift(listener); 665 | } else { 666 | existing.push(listener); 667 | } 668 | 669 | // Check for listener leak 670 | m = _getMaxListeners(target); 671 | if (m > 0 && existing.length > m && !existing.warned) { 672 | existing.warned = true; 673 | // No error code for this since it is a Warning 674 | // eslint-disable-next-line no-restricted-syntax 675 | const w = new Error('Possible EventEmitter memory leak detected. ' + 676 | `${existing.length} ${String(type)} listeners ` + 677 | `added to ${inspect(target, { depth: -1 })}. Use ` + 678 | 'emitter.setMaxListeners() to increase limit'); 679 | w.name = 'MaxListenersExceededWarning'; 680 | w.emitter = target; 681 | w.type = type; 682 | w.count = existing.length; 683 | process.emitWarning(w); 684 | } 685 | } 686 | 687 | return target; 688 | } 689 | 690 | /** 691 | * Adds a listener to the event emitter. 692 | * @param {string | symbol} type 693 | * @param {Function} listener 694 | * @returns {EventEmitter} 695 | */ 696 | EventEmitter.prototype.addListener = function addListener(type, listener) { 697 | return _addListener(this, type, listener, false); 698 | }; 699 | 700 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 701 | 702 | /** 703 | * Adds the `listener` function to the beginning of 704 | * the listeners array. 705 | * @param {string | symbol} type 706 | * @param {Function} listener 707 | * @returns {EventEmitter} 708 | */ 709 | EventEmitter.prototype.prependListener = 710 | function prependListener(type, listener) { 711 | return _addListener(this, type, listener, true); 712 | }; 713 | 714 | function onceWrapper() { 715 | if (!this.fired) { 716 | this.target.removeListener(this.type, this.wrapFn); 717 | this.fired = true; 718 | if (arguments.length === 0) 719 | return this.listener.call(this.target); 720 | return this.listener.apply(this.target, arguments); 721 | } 722 | } 723 | 724 | function _onceWrap(target, type, listener) { 725 | const state = { fired: false, wrapFn: undefined, target, type, listener }; 726 | const wrapped = onceWrapper.bind(state); 727 | wrapped.listener = listener; 728 | state.wrapFn = wrapped; 729 | return wrapped; 730 | } 731 | 732 | /** 733 | * Adds a one-time `listener` function to the event emitter. 734 | * @param {string | symbol} type 735 | * @param {Function} listener 736 | * @returns {EventEmitter} 737 | */ 738 | EventEmitter.prototype.once = function once(type, listener) { 739 | checkListener(listener); 740 | 741 | this.on(type, _onceWrap(this, type, listener)); 742 | return this; 743 | }; 744 | 745 | /** 746 | * Adds a one-time `listener` function to the beginning of 747 | * the listeners array. 748 | * @param {string | symbol} type 749 | * @param {Function} listener 750 | * @returns {EventEmitter} 751 | */ 752 | EventEmitter.prototype.prependOnceListener = 753 | function prependOnceListener(type, listener) { 754 | checkListener(listener); 755 | 756 | this.prependListener(type, _onceWrap(this, type, listener)); 757 | return this; 758 | }; 759 | 760 | 761 | /** 762 | * Removes the specified `listener` from the listeners array. 763 | * @param {string | symbol} type 764 | * @param {Function} listener 765 | * @returns {EventEmitter} 766 | */ 767 | EventEmitter.prototype.removeListener = 768 | function removeListener(type, listener) { 769 | checkListener(listener); 770 | 771 | const events = this._events; 772 | if (events === undefined) 773 | return this; 774 | 775 | const list = events[type]; 776 | if (list === undefined) 777 | return this; 778 | 779 | if (list === listener || list.listener === listener) { 780 | if (--this._eventsCount === 0) 781 | this._events = ObjectCreate(null); 782 | else { 783 | delete events[type]; 784 | if (events.removeListener) 785 | this.emit('removeListener', type, list.listener || listener); 786 | } 787 | } else if (typeof list !== 'function') { 788 | let position = -1; 789 | 790 | for (let i = list.length - 1; i >= 0; i--) { 791 | if (list[i] === listener || list[i].listener === listener) { 792 | position = i; 793 | break; 794 | } 795 | } 796 | 797 | if (position < 0) 798 | return this; 799 | 800 | if (position === 0) 801 | list.shift(); 802 | else { 803 | spliceOne(list, position); 804 | } 805 | 806 | if (list.length === 1) 807 | events[type] = list[0]; 808 | 809 | if (events.removeListener !== undefined) 810 | this.emit('removeListener', type, listener); 811 | } 812 | 813 | return this; 814 | }; 815 | 816 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 817 | 818 | /** 819 | * Removes all listeners from the event emitter. (Only 820 | * removes listeners for a specific event name if specified 821 | * as `type`). 822 | * @param {string | symbol} [type] 823 | * @returns {EventEmitter} 824 | */ 825 | EventEmitter.prototype.removeAllListeners = 826 | function removeAllListeners(type) { 827 | const events = this._events; 828 | if (events === undefined) 829 | return this; 830 | 831 | // Not listening for removeListener, no need to emit 832 | if (events.removeListener === undefined) { 833 | if (arguments.length === 0) { 834 | this._events = ObjectCreate(null); 835 | this._eventsCount = 0; 836 | } else if (events[type] !== undefined) { 837 | if (--this._eventsCount === 0) 838 | this._events = ObjectCreate(null); 839 | else 840 | delete events[type]; 841 | } 842 | return this; 843 | } 844 | 845 | // Emit removeListener for all listeners on all events 846 | if (arguments.length === 0) { 847 | for (const key of ReflectOwnKeys(events)) { 848 | if (key === 'removeListener') continue; 849 | this.removeAllListeners(key); 850 | } 851 | this.removeAllListeners('removeListener'); 852 | this._events = ObjectCreate(null); 853 | this._eventsCount = 0; 854 | return this; 855 | } 856 | 857 | const listeners = events[type]; 858 | 859 | if (typeof listeners === 'function') { 860 | this.removeListener(type, listeners); 861 | } else if (listeners !== undefined) { 862 | // LIFO order 863 | for (let i = listeners.length - 1; i >= 0; i--) { 864 | this.removeListener(type, listeners[i]); 865 | } 866 | } 867 | 868 | return this; 869 | }; 870 | 871 | function _listeners(target, type, unwrap) { 872 | const events = target._events; 873 | 874 | if (events === undefined) 875 | return []; 876 | 877 | const evlistener = events[type]; 878 | if (evlistener === undefined) 879 | return []; 880 | 881 | if (typeof evlistener === 'function') 882 | return unwrap ? [evlistener.listener || evlistener] : [evlistener]; 883 | 884 | return unwrap ? 885 | unwrapListeners(evlistener) : arrayClone(evlistener); 886 | } 887 | 888 | /** 889 | * Returns a copy of the array of listeners for the event name 890 | * specified as `type`. 891 | * @param {string | symbol} type 892 | * @returns {Function[]} 893 | */ 894 | EventEmitter.prototype.listeners = function listeners(type) { 895 | return _listeners(this, type, true); 896 | }; 897 | 898 | /** 899 | * Returns a copy of the array of listeners and wrappers for 900 | * the event name specified as `type`. 901 | * @param {string | symbol} type 902 | * @returns {Function[]} 903 | */ 904 | EventEmitter.prototype.rawListeners = function rawListeners(type) { 905 | return _listeners(this, type, false); 906 | }; 907 | 908 | /** 909 | * Returns the number of listeners listening to the event name 910 | * specified as `type`. 911 | * @deprecated since v3.2.0 912 | * @param {EventEmitter} emitter 913 | * @param {string | symbol} type 914 | * @returns {number} 915 | */ 916 | EventEmitter.listenerCount = function(emitter, type) { 917 | if (typeof emitter.listenerCount === 'function') { 918 | return emitter.listenerCount(type); 919 | } 920 | return emitter.listenerCount(type); 921 | }; 922 | 923 | EventEmitter.prototype.listenerCount = listenerCount; 924 | 925 | /** 926 | * Returns the number of listeners listening to event name 927 | * specified as `type`. 928 | * @param {string | symbol} type 929 | * @returns {number} 930 | */ 931 | function listenerCount(type) { 932 | const events = this._events; 933 | 934 | if (events !== undefined) { 935 | const evlistener = events[type]; 936 | 937 | if (typeof evlistener === 'function') { 938 | return 1; 939 | } else if (evlistener !== undefined) { 940 | return evlistener.length; 941 | } 942 | } 943 | 944 | return 0; 945 | } 946 | 947 | /** 948 | * Returns an array listing the events for which 949 | * the emitter has registered listeners. 950 | * @returns {any[]} 951 | */ 952 | EventEmitter.prototype.eventNames = function eventNames() { 953 | return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; 954 | }; 955 | 956 | function arrayClone(arr) { 957 | // At least since V8 8.3, this implementation is faster than the previous 958 | // which always used a simple for-loop 959 | switch (arr.length) { 960 | case 2: return [arr[0], arr[1]]; 961 | case 3: return [arr[0], arr[1], arr[2]]; 962 | case 4: return [arr[0], arr[1], arr[2], arr[3]]; 963 | case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]]; 964 | case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; 965 | } 966 | return ArrayPrototypeSlice(arr); 967 | } 968 | 969 | function unwrapListeners(arr) { 970 | const ret = arrayClone(arr); 971 | for (let i = 0; i < ret.length; ++i) { 972 | const orig = ret[i].listener; 973 | if (typeof orig === 'function') 974 | ret[i] = orig; 975 | } 976 | return ret; 977 | } 978 | -------------------------------------------------------------------------------- /lib/filesystem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pa = require('path'); 4 | const fs = require('fs'); 5 | 6 | class DefaultFileSystem { 7 | 8 | resolve(path) { 9 | return pa.resolve(path); 10 | } 11 | 12 | isSeparator(char) { 13 | return char === '/' || char === pa.sep; 14 | } 15 | 16 | isAbsolute(path) { 17 | return pa.isAbsolute(path); 18 | } 19 | 20 | join(...paths) { 21 | return pa.join(...paths); 22 | } 23 | 24 | basename(path) { 25 | return pa.basename(path); 26 | } 27 | 28 | dirname(path) { 29 | return pa.dirname(path); 30 | } 31 | 32 | statSync(path, options) { 33 | return fs.statSync(path, options); 34 | } 35 | 36 | readFileSync(path, options) { 37 | return fs.readFileSync(path, options); 38 | } 39 | 40 | } 41 | 42 | class VMFileSystem { 43 | 44 | constructor({fs: fsModule = fs, path: pathModule = pa} = {}) { 45 | this.fs = fsModule; 46 | this.path = pathModule; 47 | } 48 | 49 | resolve(path) { 50 | return this.path.resolve(path); 51 | } 52 | 53 | isSeparator(char) { 54 | return char === '/' || char === this.path.sep; 55 | } 56 | 57 | isAbsolute(path) { 58 | return this.path.isAbsolute(path); 59 | } 60 | 61 | join(...paths) { 62 | return this.path.join(...paths); 63 | } 64 | 65 | basename(path) { 66 | return this.path.basename(path); 67 | } 68 | 69 | dirname(path) { 70 | return this.path.dirname(path); 71 | } 72 | 73 | statSync(path, options) { 74 | return this.fs.statSync(path, options); 75 | } 76 | 77 | readFileSync(path, options) { 78 | return this.fs.readFileSync(path, options); 79 | } 80 | 81 | } 82 | 83 | exports.DefaultFileSystem = DefaultFileSystem; 84 | exports.VMFileSystem = VMFileSystem; 85 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | VMError 5 | } = require('./bridge'); 6 | const { 7 | VMScript 8 | } = require('./script'); 9 | const { 10 | VM 11 | } = require('./vm'); 12 | const { 13 | NodeVM 14 | } = require('./nodevm'); 15 | const { 16 | VMFileSystem 17 | } = require('./filesystem'); 18 | const { 19 | Resolver 20 | } = require('./resolver'); 21 | const { 22 | makeResolverFromLegacyOptions 23 | } = require('./resolver-compat'); 24 | 25 | exports.VMError = VMError; 26 | exports.VMScript = VMScript; 27 | exports.NodeVM = NodeVM; 28 | exports.VM = VM; 29 | exports.VMFileSystem = VMFileSystem; 30 | exports.Resolver = Resolver; 31 | exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions; 32 | -------------------------------------------------------------------------------- /lib/nodevm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This callback will be called to resolve a module if it couldn't be found. 5 | * 6 | * @callback resolveCallback 7 | * @param {string} moduleName - Name of the module used to resolve. 8 | * @param {string} dirname - Name of the current directory. 9 | * @return {(string|undefined)} The file or directory to use to load the requested module. 10 | */ 11 | 12 | /** 13 | * This callback will be called to require a module instead of node's require. 14 | * 15 | * @callback customRequire 16 | * @param {string} moduleName - Name of the module requested. 17 | * @return {*} The required module object. 18 | */ 19 | 20 | /** 21 | * This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided. 22 | * 23 | * NOTE: many interoperating modules must live in the same context. 24 | * 25 | * @callback pathContextCallback 26 | * @param {string} modulePath - The full path to the module filename being requested. 27 | * @param {string} extensionType - The module type (node = native, js = cjs/esm module) 28 | * @return {("host"|"sandbox")} The context for this module. 29 | */ 30 | 31 | const fs = require('fs'); 32 | const pa = require('path'); 33 | const { 34 | Script 35 | } = require('vm'); 36 | const { 37 | VMError 38 | } = require('./bridge'); 39 | const { 40 | VMScript, 41 | MODULE_PREFIX, 42 | STRICT_MODULE_PREFIX, 43 | MODULE_SUFFIX 44 | } = require('./script'); 45 | const { 46 | transformer 47 | } = require('./transformer'); 48 | const { 49 | VM 50 | } = require('./vm'); 51 | const { 52 | makeResolverFromLegacyOptions 53 | } = require('./resolver-compat'); 54 | const { Resolver } = require('./resolver'); 55 | 56 | const objectDefineProperty = Object.defineProperty; 57 | const objectDefineProperties = Object.defineProperties; 58 | 59 | /** 60 | * Host objects 61 | * 62 | * @private 63 | */ 64 | const HOST = Object.freeze({ 65 | __proto__: null, 66 | version: parseInt(process.versions.node.split('.')[0]), 67 | process, 68 | console, 69 | setTimeout, 70 | setInterval, 71 | setImmediate, 72 | clearTimeout, 73 | clearInterval, 74 | clearImmediate 75 | }); 76 | 77 | /** 78 | * Compile a script. 79 | * 80 | * @private 81 | * @param {string} filename - Filename of the script. 82 | * @param {string} script - Script. 83 | * @return {vm.Script} The compiled script. 84 | */ 85 | function compileScript(filename, script) { 86 | return new Script(script, { 87 | __proto__: null, 88 | filename, 89 | displayErrors: false 90 | }); 91 | } 92 | 93 | let cacheSandboxScript = null; 94 | let cacheMakeNestingScript = null; 95 | 96 | const NESTING_OVERRIDE = Object.freeze({ 97 | __proto__: null, 98 | vm2: vm2NestingLoader 99 | }); 100 | 101 | function makeCustomExtensions(vm, resolver, sourceExtensions) { 102 | const extensions = { __proto__: null }; 103 | const loadJS = resolver.makeExtensionHandler(vm, 'loadJS'); 104 | 105 | for (let i = 0; i < sourceExtensions.length; i++) { 106 | extensions['.' + sourceExtensions[i]] = loadJS; 107 | } 108 | 109 | if (!extensions['.json']) extensions['.json'] = resolver.makeExtensionHandler(vm, 'loadJSON'); 110 | if (!extensions['.node']) extensions['.node'] = resolver.makeExtensionHandler(vm, 'loadNode'); 111 | return extensions; 112 | } 113 | 114 | function makeSafePaths(unsafePaths) { 115 | if (unsafePaths === undefined) return undefined; 116 | if (!Array.isArray(unsafePaths)) return true; 117 | const paths = [...unsafePaths]; 118 | if (paths.some(path => typeof path !== 'string')) return true; 119 | return paths; 120 | } 121 | 122 | function makeSafeOptions(unsafeOptions) { 123 | if (unsafeOptions === undefined || unsafeOptions == null) return unsafeOptions; 124 | if (typeof unsafeOptions !== 'object' && typeof unsafeOptions !== 'function') return unsafeOptions; 125 | return { 126 | unsafeOptions, 127 | paths: makeSafePaths(unsafeOptions.paths) 128 | }; 129 | } 130 | 131 | /** 132 | * Event caused by a console.debug call if options.console="redirect" is specified. 133 | * 134 | * @public 135 | * @event NodeVM."console.debug" 136 | * @type {...*} 137 | */ 138 | 139 | /** 140 | * Event caused by a console.log call if options.console="redirect" is specified. 141 | * 142 | * @public 143 | * @event NodeVM."console.log" 144 | * @type {...*} 145 | */ 146 | 147 | /** 148 | * Event caused by a console.info call if options.console="redirect" is specified. 149 | * 150 | * @public 151 | * @event NodeVM."console.info" 152 | * @type {...*} 153 | */ 154 | 155 | /** 156 | * Event caused by a console.warn call if options.console="redirect" is specified. 157 | * 158 | * @public 159 | * @event NodeVM."console.warn" 160 | * @type {...*} 161 | */ 162 | 163 | /** 164 | * Event caused by a console.error call if options.console="redirect" is specified. 165 | * 166 | * @public 167 | * @event NodeVM."console.error" 168 | * @type {...*} 169 | */ 170 | 171 | /** 172 | * Event caused by a console.dir call if options.console="redirect" is specified. 173 | * 174 | * @public 175 | * @event NodeVM."console.dir" 176 | * @type {...*} 177 | */ 178 | 179 | /** 180 | * Event caused by a console.trace call if options.console="redirect" is specified. 181 | * 182 | * @public 183 | * @event NodeVM."console.trace" 184 | * @type {...*} 185 | */ 186 | 187 | /** 188 | * Class NodeVM. 189 | * 190 | * @public 191 | * @extends {VM} 192 | * @extends {EventEmitter} 193 | */ 194 | class NodeVM extends VM { 195 | 196 | /** 197 | * Create a new NodeVM instance.
198 | * 199 | * Unlike VM, NodeVM lets you use require same way like in regular node.
200 | * 201 | * However, it does not use the timeout. 202 | * 203 | * @public 204 | * @param {Object} [options] - VM options. 205 | * @param {Object} [options.sandbox] - Objects that will be copied into the global object of the sandbox. 206 | * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. 207 | * @param {boolean} [options.eval=true] - Allow the dynamic evaluation of code via eval(code) or Function(code)().
208 | * Only available for node v10+. 209 | * @param {boolean} [options.wasm=true] - Allow to run wasm code.
210 | * Only available for node v10+. 211 | * @param {("inherit"|"redirect"|"off")} [options.console="inherit"] - Sets the behavior of the console in the sandbox. 212 | * inherit to enable console, redirect to redirect to events, off to disable console. 213 | * @param {Object|boolean|Resolver} [options.require=false] - Allow require inside the sandbox. 214 | * @param {(boolean|string[]|Object)} [options.require.external=false] - WARNING: When allowing require the option options.require.root 215 | * should be set to restrict the script from requiring any module. Values can be true, an array of allowed external modules or an object. 216 | * @param {(string[])} [options.require.external.modules] - Array of allowed external modules. Also supports wildcards, so specifying ['@scope/*-ver-??], 217 | * for instance, will allow using all modules having a name of the form @scope/something-ver-aa, @scope/other-ver-11, etc. 218 | * @param {boolean} [options.require.external.transitive=false] - Boolean which indicates if transitive dependencies of external modules are allowed. 219 | * @param {string[]} [options.require.builtin=[]] - Array of allowed built-in modules, accepts ["*"] for all. 220 | * @param {(string|string[])} [options.require.root] - Restricted path(s) where local modules can be required. If omitted every path is allowed. 221 | * @param {Object} [options.require.mock] - Collection of mock modules (both external or built-in). 222 | * @param {("host"|"sandbox"|pathContextCallback)} [options.require.context="host"] - 223 | * host to require modules in host and proxy them to sandbox. 224 | * sandbox to load, compile and require modules in sandbox. 225 | * pathContext(modulePath, ext) to choose a mode per module (full path provided). 226 | * Builtin modules except events always required in host and proxied to sandbox. 227 | * @param {string[]} [options.require.import] - Array of modules to be loaded into NodeVM on start. 228 | * @param {resolveCallback} [options.require.resolve] - An additional lookup function in case a module wasn't 229 | * found in one of the traditional node lookup paths. 230 | * @param {customRequire} [options.require.customRequire=require] - Custom require to require host and built-in modules. 231 | * @param {boolean} [options.require.strict=true] - Load required modules in strict mode. 232 | * @param {boolean} [options.nesting=false] - 233 | * WARNING: Allowing this is a security risk as scripts can create a NodeVM which can require any host module. 234 | * Allow nesting of VMs. 235 | * @param {("commonjs"|"none")} [options.wrapper="commonjs"] - commonjs to wrap script into CommonJS wrapper, 236 | * none to retrieve value returned by the script. 237 | * @param {string[]} [options.sourceExtensions=["js"]] - Array of file extensions to treat as source code. 238 | * @param {string[]} [options.argv=[]] - Array of arguments passed to process.argv. 239 | * This object will not be copied and the script can change this object. 240 | * @param {Object} [options.env={}] - Environment map passed to process.env. 241 | * This object will not be copied and the script can change this object. 242 | * @param {boolean} [options.strict=false] - If modules should be loaded in strict mode. 243 | * @throws {VMError} If the compiler is unknown. 244 | */ 245 | constructor(options = {}) { 246 | const { 247 | compiler, 248 | eval: allowEval, 249 | wasm, 250 | console: consoleType = 'inherit', 251 | require: requireOpts = false, 252 | nesting = false, 253 | wrapper = 'commonjs', 254 | sourceExtensions = ['js'], 255 | argv, 256 | env, 257 | strict = false, 258 | sandbox 259 | } = options; 260 | 261 | // Throw this early 262 | if (sandbox && 'object' !== typeof sandbox) { 263 | throw new VMError('Sandbox must be an object.'); 264 | } 265 | 266 | super({__proto__: null, compiler: compiler, eval: allowEval, wasm}); 267 | 268 | const customResolver = requireOpts instanceof Resolver; 269 | const resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions(requireOpts, nesting && NESTING_OVERRIDE, this._compiler); 270 | 271 | // This is only here for backwards compatibility. 272 | objectDefineProperty(this, 'options', {__proto__: null, value: { 273 | console: consoleType, 274 | require: requireOpts, 275 | nesting, 276 | wrapper, 277 | sourceExtensions, 278 | strict 279 | }}); 280 | 281 | objectDefineProperty(this, 'resolver', {__proto__: null, value: resolver, enumerable: true}); 282 | 283 | if (!cacheSandboxScript) { 284 | cacheSandboxScript = compileScript(`${__dirname}/setup-node-sandbox.js`, 285 | `(function (host, data) { ${fs.readFileSync(`${__dirname}/setup-node-sandbox.js`, 'utf8')}\n})`); 286 | } 287 | 288 | const closure = this._runScript(cacheSandboxScript); 289 | 290 | const extensions = makeCustomExtensions(this, resolver, sourceExtensions); 291 | 292 | this.readonly(HOST); 293 | 294 | const { 295 | Module, 296 | jsonParse, 297 | createRequireForModule, 298 | requireImpl 299 | } = closure(HOST, { 300 | __proto__: null, 301 | argv, 302 | env, 303 | console: consoleType, 304 | extensions, 305 | emitArgs: (event, args) => { 306 | if (typeof event !== 'string' && typeof event !== 'symbol') throw new Error('Event is not a string'); 307 | return this.emit(event, ...args); 308 | }, 309 | globalPaths: [...resolver.globalPaths], 310 | getLookupPathsFor: (path) => { 311 | if (typeof path !== 'string') return []; 312 | return [...resolver.genLookupPaths(path)]; 313 | }, 314 | resolve: (mod, id, opt, ext, direct) => { 315 | if (typeof id !== 'string') throw new Error('Id is not a string'); 316 | const extList = Object.getOwnPropertyNames(ext); 317 | return resolver.resolve(mod, id, makeSafeOptions(opt), extList, !!direct); 318 | }, 319 | lookupPaths: (mod, id) => { 320 | if (typeof id !== 'string') throw new Error('Id is not a string'); 321 | return [...resolver.lookupPaths(mod, id)]; 322 | }, 323 | loadBuiltinModule: (id) => { 324 | if (typeof id !== 'string') throw new Error('Id is not a string'); 325 | return resolver.loadBuiltinModule(this, id); 326 | }, 327 | registerModule: (mod, filename, path, parent, direct) => { 328 | return resolver.registerModule(mod, filename, path, parent, direct); 329 | }, 330 | builtinModules: [...resolver.getBuiltinModulesList(this)], 331 | dirname: (path) => { 332 | if (typeof path !== 'string') return path; 333 | return resolver.fs.dirname(path); 334 | }, 335 | basename: (path) => { 336 | if (typeof path !== 'string') return path; 337 | return resolver.fs.basename(path); 338 | } 339 | }); 340 | 341 | objectDefineProperties(this, { 342 | __proto__: null, 343 | _Module: {__proto__: null, value: Module}, 344 | _jsonParse: {__proto__: null, value: jsonParse}, 345 | _createRequireForModule: {__proto__: null, value: createRequireForModule}, 346 | _requireImpl: {__proto__: null, value: requireImpl}, 347 | _cacheRequireModule: {__proto__: null, value: null, writable: true} 348 | }); 349 | 350 | 351 | resolver.init(this); 352 | 353 | // prepare global sandbox 354 | if (sandbox) { 355 | this.setGlobals(sandbox); 356 | } 357 | 358 | if (!customResolver && requireOpts && requireOpts.import) { 359 | if (Array.isArray(requireOpts.import)) { 360 | for (let i = 0, l = requireOpts.import.length; i < l; i++) { 361 | this.require(requireOpts.import[i]); 362 | } 363 | } else { 364 | this.require(requireOpts.import); 365 | } 366 | } 367 | } 368 | 369 | /** 370 | * @ignore 371 | * @deprecated 372 | */ 373 | get _resolver() { 374 | return this.resolver; 375 | } 376 | 377 | /** 378 | * @ignore 379 | * @deprecated Just call the method yourself like method(args); 380 | * @param {function} method - Function to invoke. 381 | * @param {...*} args - Arguments to pass to the function. 382 | * @return {*} Return value of the function. 383 | * @todo Can we remove this function? It even had a bug that would use args as this parameter. 384 | * @throws {*} Rethrows anything the method throws. 385 | * @throws {VMError} If method is not a function. 386 | * @throws {Error} If method is a class. 387 | */ 388 | call(method, ...args) { 389 | if ('function' === typeof method) { 390 | return method(...args); 391 | } else { 392 | throw new VMError('Unrecognized method type.'); 393 | } 394 | } 395 | 396 | /** 397 | * Require a module in VM and return it's exports. 398 | * 399 | * @public 400 | * @param {string} module - Module name. 401 | * @return {*} Exported module. 402 | * @throws {*} If the module couldn't be found or loading it threw an error. 403 | */ 404 | require(module) { 405 | const path = this.resolver.fs.resolve('.'); 406 | let mod = this._cacheRequireModule; 407 | if (!mod || mod.path !== path) { 408 | const filename = this.resolver.fs.join(path, '/vm.js'); 409 | mod = new (this._Module)(filename, path); 410 | this.resolver.registerModule(mod, filename, path, null, false); 411 | this._cacheRequireModule = mod; 412 | } 413 | return this._requireImpl(mod, module, true); 414 | } 415 | 416 | /** 417 | * Run the code in NodeVM. 418 | * 419 | * First time you run this method, code is executed same way like in node's regular `require` - it's executed with 420 | * `module`, `require`, `exports`, `__dirname`, `__filename` variables and expect result in `module.exports'. 421 | * 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.
425 | * This is only used if code is a String. 426 | * @param {boolean} [options.strict] - If modules should be loaded in strict mode. Defaults to NodeVM options. 427 | * @param {("commonjs"|"none")} [options.wrapper] - commonjs to wrap script into CommonJS wrapper, 428 | * none to retrieve value returned by the script. Defaults to NodeVM options. 429 | * @return {*} Result of executed code. 430 | * @throws {SyntaxError} If there is a syntax error in the script. 431 | * @throws {*} If the script execution terminated with an exception it is propagated. 432 | * @fires NodeVM."console.debug" 433 | * @fires NodeVM."console.log" 434 | * @fires NodeVM."console.info" 435 | * @fires NodeVM."console.warn" 436 | * @fires NodeVM."console.error" 437 | * @fires NodeVM."console.dir" 438 | * @fires NodeVM."console.trace" 439 | */ 440 | run(code, options) { 441 | let script; 442 | let filename; 443 | 444 | if (typeof options === 'object') { 445 | filename = options.filename; 446 | } else { 447 | filename = options; 448 | options = {__proto__: null}; 449 | } 450 | 451 | const { 452 | strict = this.options.strict, 453 | wrapper = this.options.wrapper, 454 | module: customModule, 455 | require: customRequire, 456 | dirname: customDirname = null 457 | } = options; 458 | 459 | let sandboxModule = customModule; 460 | let dirname = customDirname; 461 | 462 | if (code instanceof VMScript) { 463 | script = strict ? code._compileNodeVMStrict() : code._compileNodeVM(); 464 | if (!sandboxModule) { 465 | const resolvedFilename = this.resolver.fs.resolve(code.filename); 466 | dirname = this.resolver.fs.dirname(resolvedFilename); 467 | sandboxModule = new (this._Module)(resolvedFilename, dirname); 468 | this.resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); 469 | } 470 | } else { 471 | const unresolvedFilename = filename || 'vm.js'; 472 | if (!sandboxModule) { 473 | if (filename) { 474 | const resolvedFilename = this.resolver.fs.resolve(filename); 475 | dirname = this.resolver.fs.dirname(resolvedFilename); 476 | sandboxModule = new (this._Module)(resolvedFilename, dirname); 477 | this.resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); 478 | } else { 479 | sandboxModule = new (this._Module)(null, null); 480 | sandboxModule.id = unresolvedFilename; 481 | } 482 | } 483 | const prefix = strict ? STRICT_MODULE_PREFIX : MODULE_PREFIX; 484 | let scriptCode = this._compiler(code, unresolvedFilename); 485 | scriptCode = transformer(null, scriptCode, false, false, unresolvedFilename).code; 486 | script = new Script(prefix + scriptCode + MODULE_SUFFIX, { 487 | __proto__: null, 488 | filename: unresolvedFilename, 489 | displayErrors: false 490 | }); 491 | } 492 | 493 | const closure = this._runScript(script); 494 | 495 | const usedRequire = customRequire || this._createRequireForModule(sandboxModule); 496 | 497 | const ret = Reflect.apply(closure, this.sandbox, [sandboxModule.exports, usedRequire, sandboxModule, filename, dirname]); 498 | return wrapper === 'commonjs' ? sandboxModule.exports : ret; 499 | } 500 | 501 | /** 502 | * Create NodeVM and run code inside it. 503 | * 504 | * @public 505 | * @static 506 | * @param {string} script - Code to execute. 507 | * @param {string} [filename] - File name (used in stack traces only). 508 | * @param {Object} [options] - VM options. 509 | * @param {string} [options.filename] - File name (used in stack traces only). Used if filename is omitted. 510 | * @return {*} Result of executed code. 511 | * @see {@link NodeVM} for the options. 512 | * @throws {SyntaxError} If there is a syntax error in the script. 513 | * @throws {*} If the script execution terminated with an exception it is propagated. 514 | */ 515 | static code(script, filename, options) { 516 | let unresolvedFilename; 517 | if (filename != null) { 518 | if ('object' === typeof filename) { 519 | options = filename; 520 | unresolvedFilename = options.filename; 521 | } else if ('string' === typeof filename) { 522 | unresolvedFilename = filename; 523 | } else { 524 | throw new VMError('Invalid arguments.'); 525 | } 526 | } else if ('object' === typeof options) { 527 | unresolvedFilename = options.filename; 528 | } 529 | 530 | if (arguments.length > 3) { 531 | throw new VMError('Invalid number of arguments.'); 532 | } 533 | 534 | const resolvedFilename = typeof unresolvedFilename === 'string' ? pa.resolve(unresolvedFilename) : undefined; 535 | 536 | return new NodeVM(options).run(script, resolvedFilename); 537 | } 538 | 539 | /** 540 | * Create NodeVM and run script from file inside it. 541 | * 542 | * @public 543 | * @static 544 | * @param {string} filename - Filename of file to load and execute in a NodeVM. 545 | * @param {Object} [options] - NodeVM options. 546 | * @return {*} Result of executed code. 547 | * @see {@link NodeVM} for the options. 548 | * @throws {Error} If filename is not a valid filename. 549 | * @throws {SyntaxError} If there is a syntax error in the script. 550 | * @throws {*} If the script execution terminated with an exception it is propagated. 551 | */ 552 | static file(filename, options) { 553 | const resolvedFilename = pa.resolve(filename); 554 | 555 | if (!fs.existsSync(resolvedFilename)) { 556 | throw new VMError(`Script '${filename}' not found.`); 557 | } 558 | 559 | if (fs.statSync(resolvedFilename).isDirectory()) { 560 | throw new VMError('Script must be file, got directory.'); 561 | } 562 | 563 | return new NodeVM(options).run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename); 564 | } 565 | } 566 | 567 | function vm2NestingLoader(vm) { 568 | if (!cacheMakeNestingScript) { 569 | cacheMakeNestingScript = compileScript('nesting.js', '(vm, nodevm) => ({VM: vm, NodeVM: nodevm})'); 570 | } 571 | const makeNesting = vm._runScript(cacheMakeNestingScript); 572 | return makeNesting(vm.readonly(VM), vm.readonly(NodeVM)); 573 | } 574 | 575 | exports.NodeVM = NodeVM; 576 | -------------------------------------------------------------------------------- /lib/resolver-compat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Translate the old options to the new Resolver functionality. 4 | const { 5 | Resolver, 6 | DefaultResolver 7 | } = require('./resolver'); 8 | const {VMError} = require('./bridge'); 9 | const {DefaultFileSystem} = require('./filesystem'); 10 | const {makeBuiltinsFromLegacyOptions} = require('./builtin'); 11 | const {jsCompiler} = require('./compiler'); 12 | 13 | /** 14 | * Require wrapper to be able to annotate require with webpackIgnore. 15 | * 16 | * @private 17 | * @param {string} moduleName - Name of module to load. 18 | * @return {*} Module exports. 19 | */ 20 | function defaultRequire(moduleName) { 21 | // Set module.parser.javascript.commonjsMagicComments=true in your webpack config. 22 | // eslint-disable-next-line global-require 23 | return require(/* webpackIgnore: true */ moduleName); 24 | } 25 | 26 | // source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping 27 | function escapeRegExp(string) { 28 | return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 29 | } 30 | 31 | function makeExternalMatcherRegex(obj) { 32 | return escapeRegExp(obj).replace(/\\\\|\//g, '[\\\\/]') 33 | .replace(/\\\*\\\*/g, '.*').replace(/\\\*/g, '[^\\\\/]*').replace(/\\\?/g, '[^\\\\/]'); 34 | } 35 | 36 | function makeExternalMatcher(obj) { 37 | const regexString = makeExternalMatcherRegex(obj); 38 | return new RegExp(`[\\\\/]node_modules[\\\\/]${regexString}(?:[\\\\/](?!(?:.*[\\\\/])?node_modules[\\\\/]).*)?$`); 39 | } 40 | 41 | class CustomResolver extends DefaultResolver { 42 | 43 | constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict) { 44 | super(fileSystem, globalPaths, builtinModules); 45 | this.rootPaths = rootPaths; 46 | this.pathContext = pathContext; 47 | this.customResolver = customResolver; 48 | this.hostRequire = hostRequire; 49 | this.compiler = compiler; 50 | this.strict = strict; 51 | } 52 | 53 | isPathAllowed(filename) { 54 | return this.rootPaths === undefined || this.rootPaths.some(path => { 55 | if (!filename.startsWith(path)) return false; 56 | const len = path.length; 57 | if (filename.length === len || (len > 0 && this.fs.isSeparator(path[len-1]))) return true; 58 | return this.fs.isSeparator(filename[len]); 59 | }); 60 | } 61 | 62 | loadJS(vm, mod, filename) { 63 | if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(vm, mod, filename); 64 | const m = this.hostRequire(filename); 65 | mod.exports = vm.readonly(m); 66 | } 67 | 68 | loadNode(vm, mod, filename) { 69 | if (this.pathContext(filename, 'node') !== 'host') return super.loadNode(vm, mod, filename); 70 | const m = this.hostRequire(filename); 71 | mod.exports = vm.readonly(m); 72 | } 73 | 74 | customResolve(x, path, extList) { 75 | if (this.customResolver === undefined) return undefined; 76 | const resolved = this.customResolver(x, path); 77 | if (!resolved) return undefined; 78 | if (typeof resolved === 'string') { 79 | return this.loadAsFileOrDirectory(resolved, extList); 80 | } 81 | const {module=x, path: resolvedPath} = resolved; 82 | return this.loadNodeModules(module, [resolvedPath], extList); 83 | } 84 | 85 | getCompiler(filename) { 86 | return this.compiler; 87 | } 88 | 89 | isStrict(filename) { 90 | return this.strict; 91 | } 92 | 93 | } 94 | 95 | class LegacyResolver extends CustomResolver { 96 | 97 | constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) { 98 | super(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict); 99 | this.externals = externals.map(makeExternalMatcher); 100 | this.externalCache = externals.map(pattern => new RegExp(makeExternalMatcherRegex(pattern))); 101 | this.currMod = undefined; 102 | this.trustedMods = new WeakMap(); 103 | this.allowTransitive = allowTransitive; 104 | } 105 | 106 | isPathAllowed(path) { 107 | return this.isPathAllowedForModule(path, this.currMod); 108 | } 109 | 110 | isPathAllowedForModule(path, mod) { 111 | if (!super.isPathAllowed(path)) return false; 112 | if (mod) { 113 | if (mod.allowTransitive) return true; 114 | if (path.startsWith(mod.path)) { 115 | const rem = path.slice(mod.path.length); 116 | if (!/(?:^|[\\\\/])node_modules(?:$|[\\\\/])/.test(rem)) return true; 117 | } 118 | } 119 | return this.externals.some(regex => regex.test(path)); 120 | } 121 | 122 | registerModule(mod, filename, path, parent, direct) { 123 | const trustedParent = this.trustedMods.get(parent); 124 | this.trustedMods.set(mod, { 125 | filename, 126 | path, 127 | paths: this.genLookupPaths(path), 128 | allowTransitive: this.allowTransitive && 129 | ((direct && trustedParent && trustedParent.allowTransitive) || this.externals.some(regex => regex.test(filename))) 130 | }); 131 | } 132 | 133 | resolveFull(mod, x, options, extList, direct) { 134 | this.currMod = undefined; 135 | if (!direct) return super.resolveFull(mod, x, options, extList, false); 136 | const trustedMod = this.trustedMods.get(mod); 137 | if (!trustedMod || mod.path !== trustedMod.path) return super.resolveFull(mod, x, options, extList, false); 138 | const paths = [...mod.paths]; 139 | if (paths.length !== trustedMod.paths.length) return super.resolveFull(mod, x, options, extList, false); 140 | for (let i = 0; i < paths.length; i++) { 141 | if (paths[i] !== trustedMod.paths[i]) { 142 | return super.resolveFull(mod, x, options, extList, false); 143 | } 144 | } 145 | try { 146 | this.currMod = trustedMod; 147 | return super.resolveFull(trustedMod, x, options, extList, true); 148 | } finally { 149 | this.currMod = undefined; 150 | } 151 | } 152 | 153 | checkAccess(mod, filename) { 154 | const trustedMod = this.trustedMods.get(mod); 155 | if ((!trustedMod || trustedMod.filename !== filename) && !this.isPathAllowedForModule(filename, undefined)) { 156 | throw new VMError(`Module '${filename}' is not allowed to be required. The path is outside the border!`, 'EDENIED'); 157 | } 158 | } 159 | 160 | loadJS(vm, mod, filename) { 161 | if (this.pathContext(filename, 'js') !== 'host') { 162 | const trustedMod = this.trustedMods.get(mod); 163 | const script = this.readScript(filename); 164 | vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path}); 165 | } else { 166 | const m = this.hostRequire(filename); 167 | mod.exports = vm.readonly(m); 168 | } 169 | } 170 | 171 | customResolve(x, path, extList) { 172 | if (this.customResolver === undefined) return undefined; 173 | if (!(this.pathIsAbsolute(x) || this.pathIsRelative(x))) { 174 | if (!this.externalCache.some(regex => regex.test(x))) return undefined; 175 | } 176 | const resolved = this.customResolver(x, path); 177 | if (!resolved) return undefined; 178 | if (typeof resolved === 'string') { 179 | this.externals.push(new RegExp('^' + escapeRegExp(resolved))); 180 | return this.loadAsFileOrDirectory(resolved, extList); 181 | } 182 | const {module=x, path: resolvedPath} = resolved; 183 | this.externals.push(new RegExp('^' + escapeRegExp(resolvedPath))); 184 | return this.loadNodeModules(module, [resolvedPath], extList); 185 | } 186 | 187 | } 188 | 189 | const DEFAULT_FS = new DefaultFileSystem(); 190 | 191 | const DENY_RESOLVER = new Resolver(DEFAULT_FS, [], new Map()); 192 | 193 | function makeResolverFromLegacyOptions(options, override, compiler) { 194 | if (!options) { 195 | if (!override) return DENY_RESOLVER; 196 | const builtins = makeBuiltinsFromLegacyOptions(undefined, defaultRequire, undefined, override); 197 | return new Resolver(DEFAULT_FS, [], builtins); 198 | } 199 | 200 | const { 201 | builtin: builtinOpt, 202 | mock: mockOpt, 203 | external: externalOpt, 204 | root: rootPaths, 205 | resolve: customResolver, 206 | customRequire: hostRequire = defaultRequire, 207 | context = 'host', 208 | strict = true, 209 | fs: fsOpt = DEFAULT_FS, 210 | } = options; 211 | 212 | const builtins = makeBuiltinsFromLegacyOptions(builtinOpt, hostRequire, mockOpt, override); 213 | 214 | if (!externalOpt) return new Resolver(fsOpt, [], builtins); 215 | 216 | if (!compiler) compiler = jsCompiler; 217 | 218 | const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined; 219 | 220 | const pathContext = typeof context === 'function' ? context : (() => context); 221 | 222 | if (typeof externalOpt !== 'object') { 223 | return new CustomResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict); 224 | } 225 | 226 | let transitive = false; 227 | let external = undefined; 228 | if (Array.isArray(externalOpt)) { 229 | external = externalOpt; 230 | } else { 231 | external = externalOpt.modules; 232 | transitive = context !== 'host' && externalOpt.transitive; 233 | } 234 | return new LegacyResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict, external, transitive); 235 | } 236 | 237 | exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions; 238 | -------------------------------------------------------------------------------- /lib/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {Script} = require('vm'); 4 | const { 5 | lookupCompiler, 6 | removeShebang 7 | } = require('./compiler'); 8 | const { 9 | transformer 10 | } = require('./transformer'); 11 | 12 | const objectDefineProperties = Object.defineProperties; 13 | 14 | const MODULE_PREFIX = '(function (exports, require, module, __filename, __dirname) { '; 15 | const STRICT_MODULE_PREFIX = MODULE_PREFIX + '"use strict"; '; 16 | const MODULE_SUFFIX = '\n});'; 17 | 18 | /** 19 | * Class Script 20 | * 21 | * @public 22 | */ 23 | class VMScript { 24 | 25 | /** 26 | * The script code with wrapping. If set will invalidate the cache.
27 | * Writable only for backwards compatibility. 28 | * 29 | * @public 30 | * @readonly 31 | * @member {string} code 32 | * @memberOf VMScript# 33 | */ 34 | 35 | /** 36 | * The filename used for this script. 37 | * 38 | * @public 39 | * @readonly 40 | * @since v3.9.0 41 | * @member {string} filename 42 | * @memberOf VMScript# 43 | */ 44 | 45 | /** 46 | * The line offset use for stack traces. 47 | * 48 | * @public 49 | * @readonly 50 | * @since v3.9.0 51 | * @member {number} lineOffset 52 | * @memberOf VMScript# 53 | */ 54 | 55 | /** 56 | * The column offset use for stack traces. 57 | * 58 | * @public 59 | * @readonly 60 | * @since v3.9.0 61 | * @member {number} columnOffset 62 | * @memberOf VMScript# 63 | */ 64 | 65 | /** 66 | * The compiler to use to get the JavaScript code. 67 | * 68 | * @public 69 | * @readonly 70 | * @since v3.9.0 71 | * @member {(string|compileCallback)} compiler 72 | * @memberOf VMScript# 73 | */ 74 | 75 | /** 76 | * The prefix for the script. 77 | * 78 | * @private 79 | * @member {string} _prefix 80 | * @memberOf VMScript# 81 | */ 82 | 83 | /** 84 | * The suffix for the script. 85 | * 86 | * @private 87 | * @member {string} _suffix 88 | * @memberOf VMScript# 89 | */ 90 | 91 | /** 92 | * The compiled vm.Script for the VM or if not compiled null. 93 | * 94 | * @private 95 | * @member {?vm.Script} _compiledVM 96 | * @memberOf VMScript# 97 | */ 98 | 99 | /** 100 | * The compiled vm.Script for the NodeVM or if not compiled null. 101 | * 102 | * @private 103 | * @member {?vm.Script} _compiledNodeVM 104 | * @memberOf VMScript# 105 | */ 106 | 107 | /** 108 | * The compiled vm.Script for the NodeVM in strict mode or if not compiled null. 109 | * 110 | * @private 111 | * @member {?vm.Script} _compiledNodeVMStrict 112 | * @memberOf VMScript# 113 | */ 114 | 115 | /** 116 | * The resolved compiler to use to get the JavaScript code. 117 | * 118 | * @private 119 | * @readonly 120 | * @member {compileCallback} _compiler 121 | * @memberOf VMScript# 122 | */ 123 | 124 | /** 125 | * The script to run without wrapping. 126 | * 127 | * @private 128 | * @member {string} _code 129 | * @memberOf VMScript# 130 | */ 131 | 132 | /** 133 | * Whether or not the script contains async functions. 134 | * 135 | * @private 136 | * @member {boolean} _hasAsync 137 | * @memberOf VMScript# 138 | */ 139 | 140 | /** 141 | * Create VMScript instance. 142 | * 143 | * @public 144 | * @param {string} code - Code to run. 145 | * @param {(string|Object)} [options] - Options map or filename. 146 | * @param {string} [options.filename="vm.js"] - Filename that shows up in any stack traces produced from this script. 147 | * @param {number} [options.lineOffset=0] - Passed to vm.Script options. 148 | * @param {number} [options.columnOffset=0] - Passed to vm.Script options. 149 | * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. 150 | * @throws {VMError} If the compiler is unknown or if coffee-script was requested but the module not found. 151 | */ 152 | constructor(code, options) { 153 | const sCode = `${code}`; 154 | let useFileName; 155 | let useOptions; 156 | if (arguments.length === 2) { 157 | if (typeof options === 'object') { 158 | useOptions = options || {__proto__: null}; 159 | useFileName = useOptions.filename; 160 | } else { 161 | useOptions = {__proto__: null}; 162 | useFileName = options; 163 | } 164 | } else if (arguments.length > 2) { 165 | // We do it this way so that there are no more arguments in the function. 166 | // eslint-disable-next-line prefer-rest-params 167 | useOptions = arguments[2] || {__proto__: null}; 168 | useFileName = options || useOptions.filename; 169 | } else { 170 | useOptions = {__proto__: null}; 171 | } 172 | 173 | const { 174 | compiler = 'javascript', 175 | lineOffset = 0, 176 | columnOffset = 0 177 | } = useOptions; 178 | 179 | // Throw if the compiler is unknown. 180 | const resolvedCompiler = lookupCompiler(compiler); 181 | 182 | objectDefineProperties(this, { 183 | __proto__: null, 184 | code: { 185 | __proto__: null, 186 | // Put this here so that it is enumerable, and looks like a property. 187 | get() { 188 | return this._prefix + this._code + this._suffix; 189 | }, 190 | set(value) { 191 | const strNewCode = String(value); 192 | if (strNewCode === this._code && this._prefix === '' && this._suffix === '') return; 193 | this._code = strNewCode; 194 | this._prefix = ''; 195 | this._suffix = ''; 196 | this._compiledVM = null; 197 | this._compiledNodeVM = null; 198 | this._compiledCode = null; 199 | }, 200 | enumerable: true 201 | }, 202 | filename: { 203 | __proto__: null, 204 | value: useFileName || 'vm.js', 205 | enumerable: true 206 | }, 207 | lineOffset: { 208 | __proto__: null, 209 | value: lineOffset, 210 | enumerable: true 211 | }, 212 | columnOffset: { 213 | __proto__: null, 214 | value: columnOffset, 215 | enumerable: true 216 | }, 217 | compiler: { 218 | __proto__: null, 219 | value: compiler, 220 | enumerable: true 221 | }, 222 | _code: { 223 | __proto__: null, 224 | value: sCode, 225 | writable: true 226 | }, 227 | _prefix: { 228 | __proto__: null, 229 | value: '', 230 | writable: true 231 | }, 232 | _suffix: { 233 | __proto__: null, 234 | value: '', 235 | writable: true 236 | }, 237 | _compiledVM: { 238 | __proto__: null, 239 | value: null, 240 | writable: true 241 | }, 242 | _compiledNodeVM: { 243 | __proto__: null, 244 | value: null, 245 | writable: true 246 | }, 247 | _compiledNodeVMStrict: { 248 | __proto__: null, 249 | value: null, 250 | writable: true 251 | }, 252 | _compiledCode: { 253 | __proto__: null, 254 | value: null, 255 | writable: true 256 | }, 257 | _hasAsync: { 258 | __proto__: null, 259 | value: false, 260 | writable: true 261 | }, 262 | _compiler: {__proto__: null, value: resolvedCompiler} 263 | }); 264 | } 265 | 266 | /** 267 | * Wraps the code.
268 | * This will replace the old wrapping.
269 | * Will invalidate the code cache. 270 | * 271 | * @public 272 | * @deprecated Since v3.9.0. Wrap your code before passing it into the VMScript object. 273 | * @param {string} prefix - String that will be appended before the script code. 274 | * @param {script} suffix - String that will be appended behind the script code. 275 | * @return {this} This for chaining. 276 | * @throws {TypeError} If prefix or suffix is a Symbol. 277 | */ 278 | wrap(prefix, suffix) { 279 | const strPrefix = `${prefix}`; 280 | const strSuffix = `${suffix}`; 281 | if (this._prefix === strPrefix && this._suffix === strSuffix) return this; 282 | this._prefix = strPrefix; 283 | this._suffix = strSuffix; 284 | this._compiledVM = null; 285 | this._compiledNodeVM = null; 286 | this._compiledNodeVMStrict = null; 287 | return this; 288 | } 289 | 290 | /** 291 | * Compile this script.
292 | * This is useful to detect syntax errors in the script. 293 | * 294 | * @public 295 | * @return {this} This for chaining. 296 | * @throws {SyntaxError} If there is a syntax error in the script. 297 | */ 298 | compile() { 299 | this._compileVM(); 300 | return this; 301 | } 302 | 303 | /** 304 | * Get the compiled code. 305 | * 306 | * @private 307 | * @return {string} The code. 308 | */ 309 | getCompiledCode() { 310 | if (!this._compiledCode) { 311 | const comp = this._compiler(this._prefix + removeShebang(this._code) + this._suffix, this.filename); 312 | const res = transformer(null, comp, false, false, this.filename); 313 | this._compiledCode = res.code; 314 | this._hasAsync = res.hasAsync; 315 | } 316 | return this._compiledCode; 317 | } 318 | 319 | /** 320 | * Compiles this script to a vm.Script. 321 | * 322 | * @private 323 | * @param {string} prefix - JavaScript code that will be used as prefix. 324 | * @param {string} suffix - JavaScript code that will be used as suffix. 325 | * @return {vm.Script} The compiled vm.Script. 326 | * @throws {SyntaxError} If there is a syntax error in the script. 327 | */ 328 | _compile(prefix, suffix) { 329 | return new Script(prefix + this.getCompiledCode() + suffix, { 330 | __proto__: null, 331 | filename: this.filename, 332 | displayErrors: false, 333 | lineOffset: this.lineOffset, 334 | columnOffset: this.columnOffset 335 | }); 336 | } 337 | 338 | /** 339 | * Will return the cached version of the script intended for VM or compile it. 340 | * 341 | * @private 342 | * @return {vm.Script} The compiled script 343 | * @throws {SyntaxError} If there is a syntax error in the script. 344 | */ 345 | _compileVM() { 346 | let script = this._compiledVM; 347 | if (!script) { 348 | this._compiledVM = script = this._compile('', ''); 349 | } 350 | return script; 351 | } 352 | 353 | /** 354 | * Will return the cached version of the script intended for NodeVM or compile it. 355 | * 356 | * @private 357 | * @return {vm.Script} The compiled script 358 | * @throws {SyntaxError} If there is a syntax error in the script. 359 | */ 360 | _compileNodeVM() { 361 | let script = this._compiledNodeVM; 362 | if (!script) { 363 | this._compiledNodeVM = script = this._compile(MODULE_PREFIX, MODULE_SUFFIX); 364 | } 365 | return script; 366 | } 367 | 368 | /** 369 | * Will return the cached version of the script intended for NodeVM in strict mode or compile it. 370 | * 371 | * @private 372 | * @return {vm.Script} The compiled script 373 | * @throws {SyntaxError} If there is a syntax error in the script. 374 | */ 375 | _compileNodeVMStrict() { 376 | let script = this._compiledNodeVMStrict; 377 | if (!script) { 378 | this._compiledNodeVMStrict = script = this._compile(STRICT_MODULE_PREFIX, MODULE_SUFFIX); 379 | } 380 | return script; 381 | } 382 | 383 | } 384 | 385 | exports.MODULE_PREFIX = MODULE_PREFIX; 386 | exports.STRICT_MODULE_PREFIX = STRICT_MODULE_PREFIX; 387 | exports.MODULE_SUFFIX = MODULE_SUFFIX; 388 | exports.VMScript = VMScript; 389 | -------------------------------------------------------------------------------- /lib/setup-node-sandbox.js: -------------------------------------------------------------------------------- 1 | /* global host, data, VMError */ 2 | 3 | 'use strict'; 4 | 5 | const LocalError = Error; 6 | const LocalTypeError = TypeError; 7 | const LocalWeakMap = WeakMap; 8 | 9 | const { 10 | apply: localReflectApply, 11 | defineProperty: localReflectDefineProperty 12 | } = Reflect; 13 | 14 | const { 15 | set: localWeakMapSet, 16 | get: localWeakMapGet 17 | } = LocalWeakMap.prototype; 18 | 19 | const { 20 | isArray: localArrayIsArray 21 | } = Array; 22 | 23 | function uncurryThis(func) { 24 | return (thiz, ...args) => localReflectApply(func, thiz, args); 25 | } 26 | 27 | const localArrayPrototypeSlice = uncurryThis(Array.prototype.slice); 28 | const localArrayPrototypeIncludes = uncurryThis(Array.prototype.includes); 29 | const localArrayPrototypePush = uncurryThis(Array.prototype.push); 30 | const localArrayPrototypeIndexOf = uncurryThis(Array.prototype.indexOf); 31 | const localArrayPrototypeSplice = uncurryThis(Array.prototype.splice); 32 | const localStringPrototypeStartsWith = uncurryThis(String.prototype.startsWith); 33 | const localStringPrototypeSlice = uncurryThis(String.prototype.slice); 34 | const localStringPrototypeIndexOf = uncurryThis(String.prototype.indexOf); 35 | 36 | const { 37 | argv: optionArgv, 38 | env: optionEnv, 39 | console: optionConsole, 40 | extensions, 41 | emitArgs, 42 | globalPaths, 43 | getLookupPathsFor, 44 | resolve: resolve0, 45 | lookupPaths, 46 | loadBuiltinModule, 47 | registerModule, 48 | builtinModules, 49 | dirname, 50 | basename 51 | } = data; 52 | 53 | function ensureSandboxArray(a) { 54 | return localArrayPrototypeSlice(a); 55 | } 56 | 57 | class Module { 58 | 59 | constructor(id, path, parent) { 60 | this.id = id; 61 | this.filename = id; 62 | this.path = path; 63 | this.parent = parent; 64 | this.loaded = false; 65 | this.paths = path ? ensureSandboxArray(getLookupPathsFor(path)) : []; 66 | this.children = []; 67 | this.exports = {}; 68 | } 69 | 70 | _updateChildren(child, isNew) { 71 | const children = this.children; 72 | if (children && (isNew || !localArrayPrototypeIncludes(children, child))) { 73 | localArrayPrototypePush(children, child); 74 | } 75 | } 76 | 77 | require(id) { 78 | return requireImpl(this, id, false); 79 | } 80 | 81 | } 82 | 83 | const originalRequire = Module.prototype.require; 84 | const cacheBuiltins = {__proto__: null}; 85 | 86 | function requireImpl(mod, id, direct) { 87 | if (direct && mod.require !== originalRequire) { 88 | return mod.require(id); 89 | } 90 | const filename = resolve0(mod, id, undefined, Module._extensions, direct); 91 | if (localStringPrototypeStartsWith(filename, 'node:')) { 92 | id = localStringPrototypeSlice(filename, 5); 93 | let nmod = cacheBuiltins[id]; 94 | if (!nmod) { 95 | nmod = loadBuiltinModule(id); 96 | if (!nmod) throw new VMError(`Cannot find module '${filename}'`, 'ENOTFOUND'); 97 | cacheBuiltins[id] = nmod; 98 | } 99 | return nmod; 100 | } 101 | 102 | const cachedModule = Module._cache[filename]; 103 | if (cachedModule !== undefined) { 104 | mod._updateChildren(cachedModule, false); 105 | return cachedModule.exports; 106 | } 107 | 108 | let nmod = cacheBuiltins[id]; 109 | if (nmod) return nmod; 110 | nmod = loadBuiltinModule(id); 111 | if (nmod) { 112 | cacheBuiltins[id] = nmod; 113 | return nmod; 114 | } 115 | 116 | const path = dirname(filename); 117 | const module = new Module(filename, path, mod); 118 | registerModule(module, filename, path, mod, direct); 119 | mod._updateChildren(module, true); 120 | try { 121 | Module._cache[filename] = module; 122 | const handler = findBestExtensionHandler(filename); 123 | handler(module, filename); 124 | module.loaded = true; 125 | } catch (e) { 126 | delete Module._cache[filename]; 127 | const children = mod.children; 128 | if (localArrayIsArray(children)) { 129 | const index = localArrayPrototypeIndexOf(children, module); 130 | if (index !== -1) { 131 | localArrayPrototypeSplice(children, index, 1); 132 | } 133 | } 134 | throw e; 135 | } 136 | 137 | return module.exports; 138 | } 139 | 140 | Module.builtinModules = ensureSandboxArray(builtinModules); 141 | Module.globalPaths = ensureSandboxArray(globalPaths); 142 | Module._extensions = {__proto__: null}; 143 | Module._cache = {__proto__: null}; 144 | 145 | { 146 | const keys = Object.getOwnPropertyNames(extensions); 147 | for (let i = 0; i < keys.length; i++) { 148 | const key = keys[i]; 149 | const handler = extensions[key]; 150 | Module._extensions[key] = (mod, filename) => handler(mod, filename); 151 | } 152 | } 153 | 154 | function findBestExtensionHandler(filename) { 155 | const name = basename(filename); 156 | for (let i = 0; (i = localStringPrototypeIndexOf(name, '.', i + 1)) !== -1;) { 157 | const ext = localStringPrototypeSlice(name, i); 158 | const handler = Module._extensions[ext]; 159 | if (handler) return handler; 160 | } 161 | const js = Module._extensions['.js']; 162 | if (js) return js; 163 | const keys = Object.getOwnPropertyNames(Module._extensions); 164 | if (keys.length === 0) throw new VMError(`Failed to load '${filename}': Unknown type.`, 'ELOADFAIL'); 165 | return Module._extensions[keys[0]]; 166 | } 167 | 168 | function createRequireForModule(mod) { 169 | // eslint-disable-next-line no-shadow 170 | function require(id) { 171 | return requireImpl(mod, id, true); 172 | } 173 | function resolve(id, options) { 174 | return resolve0(mod, id, options, Module._extensions, true); 175 | } 176 | require.resolve = resolve; 177 | function paths(id) { 178 | return ensureSandboxArray(lookupPaths(mod, id)); 179 | } 180 | resolve.paths = paths; 181 | 182 | require.extensions = Module._extensions; 183 | 184 | require.cache = Module._cache; 185 | 186 | return require; 187 | } 188 | 189 | /** 190 | * Prepare sandbox. 191 | */ 192 | 193 | const TIMERS = new LocalWeakMap(); 194 | 195 | class Timeout { 196 | } 197 | 198 | class Interval { 199 | } 200 | 201 | class Immediate { 202 | } 203 | 204 | function clearTimer(timer) { 205 | const obj = localReflectApply(localWeakMapGet, TIMERS, [timer]); 206 | if (obj) { 207 | obj.clear(obj.value); 208 | } 209 | } 210 | 211 | // This is a function and not an arrow function, since the original is also a function 212 | // eslint-disable-next-line no-shadow 213 | global.setTimeout = function setTimeout(callback, delay, ...args) { 214 | if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function'); 215 | const obj = new Timeout(callback, args); 216 | const cb = () => { 217 | localReflectApply(callback, null, args); 218 | }; 219 | const tmr = host.setTimeout(cb, delay); 220 | 221 | const ref = { 222 | __proto__: null, 223 | clear: host.clearTimeout, 224 | value: tmr 225 | }; 226 | 227 | localReflectApply(localWeakMapSet, TIMERS, [obj, ref]); 228 | return obj; 229 | }; 230 | 231 | // eslint-disable-next-line no-shadow 232 | global.setInterval = function setInterval(callback, interval, ...args) { 233 | if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function'); 234 | const obj = new Interval(); 235 | const cb = () => { 236 | localReflectApply(callback, null, args); 237 | }; 238 | const tmr = host.setInterval(cb, interval); 239 | 240 | const ref = { 241 | __proto__: null, 242 | clear: host.clearInterval, 243 | value: tmr 244 | }; 245 | 246 | localReflectApply(localWeakMapSet, TIMERS, [obj, ref]); 247 | return obj; 248 | }; 249 | 250 | // eslint-disable-next-line no-shadow 251 | global.setImmediate = function setImmediate(callback, ...args) { 252 | if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function'); 253 | const obj = new Immediate(); 254 | const cb = () => { 255 | localReflectApply(callback, null, args); 256 | }; 257 | const tmr = host.setImmediate(cb); 258 | 259 | const ref = { 260 | __proto__: null, 261 | clear: host.clearImmediate, 262 | value: tmr 263 | }; 264 | 265 | localReflectApply(localWeakMapSet, TIMERS, [obj, ref]); 266 | return obj; 267 | }; 268 | 269 | // eslint-disable-next-line no-shadow 270 | global.clearTimeout = function clearTimeout(timeout) { 271 | clearTimer(timeout); 272 | }; 273 | 274 | // eslint-disable-next-line no-shadow 275 | global.clearInterval = function clearInterval(interval) { 276 | clearTimer(interval); 277 | }; 278 | 279 | // eslint-disable-next-line no-shadow 280 | global.clearImmediate = function clearImmediate(immediate) { 281 | clearTimer(immediate); 282 | }; 283 | 284 | const localProcess = host.process; 285 | 286 | const LISTENERS = new LocalWeakMap(); 287 | const LISTENER_HANDLER = new LocalWeakMap(); 288 | 289 | /** 290 | * 291 | * @param {*} name 292 | * @param {*} handler 293 | * @this process 294 | * @return {this} 295 | */ 296 | function addListener(name, handler) { 297 | if (name !== 'beforeExit' && name !== 'exit') { 298 | throw new LocalError(`Access denied to listen for '${name}' event.`); 299 | } 300 | 301 | let cb = localReflectApply(localWeakMapGet, LISTENERS, [handler]); 302 | if (!cb) { 303 | cb = () => { 304 | handler(); 305 | }; 306 | localReflectApply(localWeakMapSet, LISTENER_HANDLER, [cb, handler]); 307 | localReflectApply(localWeakMapSet, LISTENERS, [handler, cb]); 308 | } 309 | 310 | localProcess.on(name, cb); 311 | 312 | return this; 313 | } 314 | 315 | /** 316 | * 317 | * @this process 318 | * @return {this} 319 | */ 320 | // eslint-disable-next-line no-shadow 321 | function process() { 322 | return this; 323 | } 324 | 325 | const baseUptime = localProcess.uptime(); 326 | 327 | // FIXME wrong class structure 328 | global.process = { 329 | __proto__: process.prototype, 330 | argv: optionArgv !== undefined ? optionArgv : [], 331 | title: localProcess.title, 332 | version: localProcess.version, 333 | versions: localProcess.versions, 334 | arch: localProcess.arch, 335 | platform: localProcess.platform, 336 | env: optionEnv !== undefined ? optionEnv : {}, 337 | pid: localProcess.pid, 338 | features: localProcess.features, 339 | nextTick: function nextTick(callback, ...args) { 340 | if (typeof callback !== 'function') { 341 | throw new LocalError('Callback must be a function.'); 342 | } 343 | 344 | localProcess.nextTick(()=>{ 345 | localReflectApply(callback, null, args); 346 | }); 347 | }, 348 | hrtime: function hrtime(time) { 349 | return localProcess.hrtime(time); 350 | }, 351 | uptime: function uptime() { 352 | return localProcess.uptime() - baseUptime; 353 | }, 354 | cwd: function cwd() { 355 | return localProcess.cwd(); 356 | }, 357 | addListener, 358 | on: addListener, 359 | 360 | once: function once(name, handler) { 361 | if (name !== 'beforeExit' && name !== 'exit') { 362 | throw new LocalError(`Access denied to listen for '${name}' event.`); 363 | } 364 | 365 | let triggered = false; 366 | const cb = () => { 367 | if (triggered) return; 368 | triggered = true; 369 | localProcess.removeListener(name, cb); 370 | handler(); 371 | }; 372 | localReflectApply(localWeakMapSet, LISTENER_HANDLER, [cb, handler]); 373 | 374 | localProcess.on(name, cb); 375 | 376 | return this; 377 | }, 378 | 379 | listeners: function listeners(name) { 380 | if (name !== 'beforeExit' && name !== 'exit') { 381 | // Maybe add ({__proto__:null})[name] to throw when name fails in https://tc39.es/ecma262/#sec-topropertykey. 382 | return []; 383 | } 384 | 385 | // Filter out listeners, which were not created in this sandbox 386 | const all = localProcess.listeners(name); 387 | const filtered = []; 388 | let j = 0; 389 | for (let i = 0; i < all.length; i++) { 390 | const h = localReflectApply(localWeakMapGet, LISTENER_HANDLER, [all[i]]); 391 | if (h) { 392 | if (!localReflectDefineProperty(filtered, j, { 393 | __proto__: null, 394 | value: h, 395 | writable: true, 396 | enumerable: true, 397 | configurable: true 398 | })) throw new LocalError('Unexpected'); 399 | j++; 400 | } 401 | } 402 | return filtered; 403 | }, 404 | 405 | removeListener: function removeListener(name, handler) { 406 | if (name !== 'beforeExit' && name !== 'exit') { 407 | return this; 408 | } 409 | 410 | const cb = localReflectApply(localWeakMapGet, LISTENERS, [handler]); 411 | if (cb) localProcess.removeListener(name, cb); 412 | 413 | return this; 414 | }, 415 | 416 | umask: function umask() { 417 | if (arguments.length) { 418 | throw new LocalError('Access denied to set umask.'); 419 | } 420 | 421 | return localProcess.umask(); 422 | } 423 | }; 424 | 425 | if (optionConsole === 'inherit') { 426 | global.console = host.console; 427 | } else if (optionConsole === 'redirect') { 428 | global.console = { 429 | debug(...args) { 430 | emitArgs('console.debug', args); 431 | }, 432 | log(...args) { 433 | emitArgs('console.log', args); 434 | }, 435 | info(...args) { 436 | emitArgs('console.info', args); 437 | }, 438 | warn(...args) { 439 | emitArgs('console.warn', args); 440 | }, 441 | error(...args) { 442 | emitArgs('console.error', args); 443 | }, 444 | dir(...args) { 445 | emitArgs('console.dir', args); 446 | }, 447 | time() {}, 448 | timeEnd() {}, 449 | trace(...args) { 450 | emitArgs('console.trace', args); 451 | } 452 | }; 453 | } 454 | 455 | return { 456 | __proto__: null, 457 | Module, 458 | jsonParse: JSON.parse, 459 | createRequireForModule, 460 | requireImpl 461 | }; 462 | -------------------------------------------------------------------------------- /lib/setup-sandbox.js: -------------------------------------------------------------------------------- 1 | /* global host, bridge, data, context */ 2 | 3 | 'use strict'; 4 | 5 | const { 6 | Object: localObject, 7 | Array: localArray, 8 | Error: LocalError, 9 | Reflect: localReflect, 10 | Proxy: LocalProxy, 11 | WeakMap: LocalWeakMap, 12 | Function: localFunction, 13 | eval: localEval 14 | } = global; 15 | 16 | const { 17 | freeze: localObjectFreeze 18 | } = localObject; 19 | 20 | const { 21 | getPrototypeOf: localReflectGetPrototypeOf, 22 | apply, 23 | construct: localReflectConstruct, 24 | deleteProperty: localReflectDeleteProperty, 25 | has: localReflectHas, 26 | defineProperty: localReflectDefineProperty, 27 | setPrototypeOf: localReflectSetPrototypeOf, 28 | getOwnPropertyDescriptor: localReflectGetOwnPropertyDescriptor 29 | } = localReflect; 30 | 31 | const speciesSymbol = Symbol.species; 32 | const globalPromise = global.Promise; 33 | class localPromise extends globalPromise {} 34 | 35 | const resetPromiseSpecies = (p) => { 36 | if (p instanceof globalPromise && ![globalPromise, localPromise].includes(p.constructor[speciesSymbol])) { 37 | Object.defineProperty(p.constructor, speciesSymbol, { value: localPromise }); 38 | } 39 | }; 40 | 41 | const globalPromiseThen = globalPromise.prototype.then; 42 | globalPromise.prototype.then = function then(onFulfilled, onRejected) { 43 | resetPromiseSpecies(this); 44 | return globalPromiseThen.call(this, onFulfilled, onRejected); 45 | }; 46 | 47 | const localReflectApply = (target, thisArg, args) => { 48 | resetPromiseSpecies(thisArg); 49 | return apply(target, thisArg, args); 50 | }; 51 | 52 | const { 53 | isArray: localArrayIsArray 54 | } = localArray; 55 | 56 | const { 57 | ensureThis, 58 | ReadOnlyHandler, 59 | from, 60 | fromWithFactory, 61 | readonlyFactory, 62 | connect, 63 | addProtoMapping, 64 | VMError, 65 | ReadOnlyMockHandler 66 | } = bridge; 67 | 68 | const { 69 | allowAsync, 70 | GeneratorFunction, 71 | AsyncFunction, 72 | AsyncGeneratorFunction 73 | } = data; 74 | 75 | const { 76 | get: localWeakMapGet, 77 | set: localWeakMapSet 78 | } = LocalWeakMap.prototype; 79 | 80 | function localUnexpected() { 81 | return new VMError('Should not happen'); 82 | } 83 | 84 | // global is originally prototype of host.Object so it can be used to climb up from the sandbox. 85 | if (!localReflectSetPrototypeOf(context, localObject.prototype)) throw localUnexpected(); 86 | 87 | Object.defineProperties(global, { 88 | global: {value: global, writable: true, configurable: true, enumerable: true}, 89 | globalThis: {value: global, writable: true, configurable: true}, 90 | GLOBAL: {value: global, writable: true, configurable: true}, 91 | root: {value: global, writable: true, configurable: true}, 92 | Error: {value: LocalError}, 93 | Promise: {value: localPromise}, 94 | Proxy: {value: undefined} 95 | }); 96 | 97 | if (!localReflectDefineProperty(global, 'VMError', { 98 | __proto__: null, 99 | value: VMError, 100 | writable: true, 101 | enumerable: false, 102 | configurable: true 103 | })) throw localUnexpected(); 104 | 105 | // Fixes buffer unsafe allocation 106 | /* eslint-disable no-use-before-define */ 107 | class BufferHandler extends ReadOnlyHandler { 108 | 109 | apply(target, thiz, args) { 110 | if (args.length > 0 && typeof args[0] === 'number') { 111 | return LocalBuffer.alloc(args[0]); 112 | } 113 | return localReflectApply(LocalBuffer.from, LocalBuffer, args); 114 | } 115 | 116 | construct(target, args, newTarget) { 117 | if (args.length > 0 && typeof args[0] === 'number') { 118 | return LocalBuffer.alloc(args[0]); 119 | } 120 | return localReflectApply(LocalBuffer.from, LocalBuffer, args); 121 | } 122 | 123 | } 124 | /* eslint-enable no-use-before-define */ 125 | 126 | const LocalBuffer = fromWithFactory(obj => new BufferHandler(obj), host.Buffer); 127 | 128 | 129 | if (!localReflectDefineProperty(global, 'Buffer', { 130 | __proto__: null, 131 | value: LocalBuffer, 132 | writable: true, 133 | enumerable: false, 134 | configurable: true 135 | })) throw localUnexpected(); 136 | 137 | addProtoMapping(LocalBuffer.prototype, host.Buffer.prototype, 'Uint8Array'); 138 | 139 | /** 140 | * 141 | * @param {*} size Size of new buffer 142 | * @this LocalBuffer 143 | * @return {LocalBuffer} 144 | */ 145 | function allocUnsafe(size) { 146 | return LocalBuffer.alloc(size); 147 | } 148 | 149 | connect(allocUnsafe, host.Buffer.allocUnsafe); 150 | 151 | /** 152 | * 153 | * @param {*} size Size of new buffer 154 | * @this LocalBuffer 155 | * @return {LocalBuffer} 156 | */ 157 | function allocUnsafeSlow(size) { 158 | return LocalBuffer.alloc(size); 159 | } 160 | 161 | connect(allocUnsafeSlow, host.Buffer.allocUnsafeSlow); 162 | 163 | /** 164 | * Replacement for Buffer inspect 165 | * 166 | * @param {*} recurseTimes 167 | * @param {*} ctx 168 | * @this LocalBuffer 169 | * @return {string} 170 | */ 171 | function inspect(recurseTimes, ctx) { 172 | // Mimic old behavior, could throw but didn't pass a test. 173 | const max = host.INSPECT_MAX_BYTES; 174 | const actualMax = Math.min(max, this.length); 175 | const remaining = this.length - max; 176 | let str = this.hexSlice(0, actualMax).replace(/(.{2})/g, '$1 ').trim(); 177 | if (remaining > 0) str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`; 178 | return `<${this.constructor.name} ${str}>`; 179 | } 180 | 181 | connect(inspect, host.Buffer.prototype.inspect); 182 | 183 | connect(localFunction.prototype.bind, host.Function.prototype.bind); 184 | 185 | connect(localObject.prototype.__defineGetter__, host.Object.prototype.__defineGetter__); 186 | connect(localObject.prototype.__defineSetter__, host.Object.prototype.__defineSetter__); 187 | connect(localObject.prototype.__lookupGetter__, host.Object.prototype.__lookupGetter__); 188 | connect(localObject.prototype.__lookupSetter__, host.Object.prototype.__lookupSetter__); 189 | 190 | /* 191 | * PrepareStackTrace sanitization 192 | */ 193 | 194 | const oldPrepareStackTraceDesc = localReflectGetOwnPropertyDescriptor(LocalError, 'prepareStackTrace'); 195 | 196 | let currentPrepareStackTrace = LocalError.prepareStackTrace; 197 | const wrappedPrepareStackTrace = new LocalWeakMap(); 198 | if (typeof currentPrepareStackTrace === 'function') { 199 | wrappedPrepareStackTrace.set(currentPrepareStackTrace, currentPrepareStackTrace); 200 | } 201 | 202 | let OriginalCallSite; 203 | LocalError.prepareStackTrace = (e, sst) => { 204 | OriginalCallSite = sst[0].constructor; 205 | }; 206 | new LocalError().stack; 207 | if (typeof OriginalCallSite === 'function') { 208 | LocalError.prepareStackTrace = undefined; 209 | 210 | function makeCallSiteGetters(list) { 211 | const callSiteGetters = []; 212 | for (let i=0; i { 219 | return localReflectApply(func, thiz, []); 220 | } 221 | }; 222 | } 223 | return callSiteGetters; 224 | } 225 | 226 | function applyCallSiteGetters(thiz, callSite, getters) { 227 | for (let i=0; i { 302 | const sandboxSst = ensureThis(sst); 303 | if (localArrayIsArray(sst)) { 304 | if (sst === sandboxSst) { 305 | for (let i=0; i < sst.length; i++) { 306 | const cs = sst[i]; 307 | if (typeof cs === 'object' && localReflectGetPrototypeOf(cs) === OriginalCallSite.prototype) { 308 | sst[i] = new CallSite(cs); 309 | } 310 | } 311 | } else { 312 | sst = []; 313 | for (let i=0; i < sandboxSst.length; i++) { 314 | const cs = sandboxSst[i]; 315 | localReflectDefineProperty(sst, i, { 316 | __proto__: null, 317 | value: new CallSite(cs), 318 | enumerable: true, 319 | configurable: true, 320 | writable: true 321 | }); 322 | } 323 | } 324 | } else { 325 | sst = sandboxSst; 326 | } 327 | return value(error, sst); 328 | }; 329 | localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [value, newWrapped]); 330 | localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [newWrapped, newWrapped]); 331 | currentPrepareStackTrace = newWrapped; 332 | } 333 | })) throw localUnexpected(); 334 | } else if (oldPrepareStackTraceDesc) { 335 | localReflectDefineProperty(LocalError, 'prepareStackTrace', oldPrepareStackTraceDesc); 336 | } else { 337 | localReflectDeleteProperty(LocalError, 'prepareStackTrace'); 338 | } 339 | 340 | /* 341 | * Exception sanitization 342 | */ 343 | 344 | const withProxy = localObjectFreeze({ 345 | __proto__: null, 346 | has(target, key) { 347 | if (key === host.INTERNAL_STATE_NAME) return false; 348 | return localReflectHas(target, key); 349 | } 350 | }); 351 | 352 | const interanState = localObjectFreeze({ 353 | __proto__: null, 354 | wrapWith(x) { 355 | if (x === null || x === undefined) return x; 356 | return new LocalProxy(localObject(x), withProxy); 357 | }, 358 | handleException: ensureThis, 359 | import(what) { 360 | throw new VMError('Dynamic Import not supported'); 361 | } 362 | }); 363 | 364 | if (!localReflectDefineProperty(global, host.INTERNAL_STATE_NAME, { 365 | __proto__: null, 366 | configurable: false, 367 | enumerable: false, 368 | writable: false, 369 | value: interanState 370 | })) throw localUnexpected(); 371 | 372 | /* 373 | * Eval sanitization 374 | */ 375 | 376 | function throwAsync() { 377 | return new VMError('Async not available'); 378 | } 379 | 380 | function makeFunction(inputArgs, isAsync, isGenerator) { 381 | const lastArgs = inputArgs.length - 1; 382 | let code = lastArgs >= 0 ? `${inputArgs[lastArgs]}` : ''; 383 | let args = lastArgs > 0 ? `${inputArgs[0]}` : ''; 384 | for (let i = 1; i < lastArgs; i++) { 385 | args += `,${inputArgs[i]}`; 386 | } 387 | try { 388 | code = host.transformAndCheck(args, code, isAsync, isGenerator, allowAsync); 389 | } catch (e) { 390 | throw bridge.from(e); 391 | } 392 | return localEval(code); 393 | } 394 | 395 | const FunctionHandler = { 396 | __proto__: null, 397 | apply(target, thiz, args) { 398 | return makeFunction(args, this.isAsync, this.isGenerator); 399 | }, 400 | construct(target, args, newTarget) { 401 | return makeFunction(args, this.isAsync, this.isGenerator); 402 | } 403 | }; 404 | 405 | const EvalHandler = { 406 | __proto__: null, 407 | apply(target, thiz, args) { 408 | if (args.length === 0) return undefined; 409 | let code = `${args[0]}`; 410 | try { 411 | code = host.transformAndCheck(null, code, false, false, allowAsync); 412 | } catch (e) { 413 | throw bridge.from(e); 414 | } 415 | return localEval(code); 416 | } 417 | }; 418 | 419 | const AsyncErrorHandler = { 420 | __proto__: null, 421 | apply(target, thiz, args) { 422 | throw throwAsync(); 423 | }, 424 | construct(target, args, newTarget) { 425 | throw throwAsync(); 426 | } 427 | }; 428 | 429 | function makeCheckFunction(isAsync, isGenerator) { 430 | if (isAsync && !allowAsync) return AsyncErrorHandler; 431 | return { 432 | __proto__: FunctionHandler, 433 | isAsync, 434 | isGenerator 435 | }; 436 | } 437 | 438 | function overrideWithProxy(obj, prop, value, handler) { 439 | const proxy = new LocalProxy(value, handler); 440 | if (!localReflectDefineProperty(obj, prop, {__proto__: null, value: proxy})) throw localUnexpected(); 441 | return proxy; 442 | } 443 | 444 | const proxiedFunction = overrideWithProxy(localFunction.prototype, 'constructor', localFunction, makeCheckFunction(false, false)); 445 | if (GeneratorFunction) { 446 | if (!localReflectSetPrototypeOf(GeneratorFunction, proxiedFunction)) throw localUnexpected(); 447 | overrideWithProxy(GeneratorFunction.prototype, 'constructor', GeneratorFunction, makeCheckFunction(false, true)); 448 | } 449 | if (AsyncFunction) { 450 | if (!localReflectSetPrototypeOf(AsyncFunction, proxiedFunction)) throw localUnexpected(); 451 | overrideWithProxy(AsyncFunction.prototype, 'constructor', AsyncFunction, makeCheckFunction(true, false)); 452 | } 453 | if (AsyncGeneratorFunction) { 454 | if (!localReflectSetPrototypeOf(AsyncGeneratorFunction, proxiedFunction)) throw localUnexpected(); 455 | overrideWithProxy(AsyncGeneratorFunction.prototype, 'constructor', AsyncGeneratorFunction, makeCheckFunction(true, true)); 456 | } 457 | 458 | function makeSafeHandlerArgs(args) { 459 | const sArgs = ensureThis(args); 460 | if (sArgs === args) return args; 461 | const a = []; 462 | for (let i=0; i < sArgs.length; i++) { 463 | localReflectDefineProperty(a, i, { 464 | __proto__: null, 465 | value: sArgs[i], 466 | enumerable: true, 467 | configurable: true, 468 | writable: true 469 | }); 470 | } 471 | return a; 472 | } 473 | 474 | const makeSafeArgs = Object.freeze({ 475 | __proto__: null, 476 | apply(target, thiz, args) { 477 | return localReflectApply(target, thiz, makeSafeHandlerArgs(args)); 478 | }, 479 | construct(target, args, newTarget) { 480 | return localReflectConstruct(target, makeSafeHandlerArgs(args), newTarget); 481 | } 482 | }); 483 | 484 | const proxyHandlerHandler = Object.freeze({ 485 | __proto__: null, 486 | get(target, name, receiver) { 487 | if (name === 'isProxy') return true; 488 | const value = target.handler[name]; 489 | if (typeof value !== 'function') return value; 490 | return new LocalProxy(value, makeSafeArgs); 491 | } 492 | }); 493 | 494 | function wrapProxyHandler(args) { 495 | if (args.length < 2) return args; 496 | const handler = args[1]; 497 | args[1] = new LocalProxy({__proto__: null, handler}, proxyHandlerHandler); 498 | return args; 499 | } 500 | 501 | const proxyHandler = Object.freeze({ 502 | __proto__: null, 503 | apply(target, thiz, args) { 504 | return localReflectApply(target, thiz, wrapProxyHandler(args)); 505 | }, 506 | construct(target, args, newTarget) { 507 | return localReflectConstruct(target, wrapProxyHandler(args), newTarget); 508 | } 509 | }); 510 | 511 | const proxiedProxy = new LocalProxy(LocalProxy, proxyHandler); 512 | 513 | overrideWithProxy(LocalProxy, 'revocable', LocalProxy.revocable, proxyHandler); 514 | 515 | global.Proxy = proxiedProxy; 516 | global.Function = proxiedFunction; 517 | global.eval = new LocalProxy(localEval, EvalHandler); 518 | 519 | /* 520 | * Promise sanitization 521 | */ 522 | 523 | if (localPromise) { 524 | 525 | const PromisePrototype = localPromise.prototype; 526 | 527 | if (!allowAsync) { 528 | 529 | overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, AsyncErrorHandler); 530 | // This seems not to work, and will produce 531 | // UnhandledPromiseRejectionWarning: TypeError: Method Promise.prototype.then called on incompatible receiver [object Object]. 532 | // This is likely caused since the host.Promise.prototype.then cannot use the VM Proxy object. 533 | // Contextify.connect(host.Promise.prototype.then, Promise.prototype.then); 534 | 535 | } else { 536 | 537 | overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, { 538 | __proto__: null, 539 | apply(target, thiz, args) { 540 | if (args.length > 1) { 541 | const onRejected = args[1]; 542 | if (typeof onRejected === 'function') { 543 | args[1] = function wrapper(error) { 544 | error = ensureThis(error); 545 | return localReflectApply(onRejected, this, [error]); 546 | }; 547 | } 548 | } 549 | return localReflectApply(target, thiz, args); 550 | } 551 | }); 552 | 553 | } 554 | 555 | Object.freeze(localPromise); 556 | Object.freeze(PromisePrototype); 557 | } 558 | 559 | localObject.defineProperty(localObject, 'setPrototypeOf', { 560 | value: () => { 561 | throw new VMError('Operation not allowed on contextified object.'); 562 | } 563 | }); 564 | 565 | function readonly(other, mock) { 566 | // Note: other@other(unsafe) mock@other(unsafe) returns@this(unsafe) throws@this(unsafe) 567 | if (!mock) return fromWithFactory(readonlyFactory, other); 568 | const tmock = from(mock); 569 | return fromWithFactory(obj=>new ReadOnlyMockHandler(obj, tmock), other); 570 | } 571 | 572 | return { 573 | __proto__: null, 574 | readonly, 575 | global 576 | }; 577 | -------------------------------------------------------------------------------- /lib/transformer.js: -------------------------------------------------------------------------------- 1 | 2 | const {Parser: AcornParser, isNewLine: acornIsNewLine, getLineInfo: acornGetLineInfo} = require('acorn'); 3 | const {full: acornWalkFull} = require('acorn-walk'); 4 | 5 | const INTERNAL_STATE_NAME = 'VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL'; 6 | 7 | function assertType(node, type) { 8 | if (!node) throw new Error(`None existent node expected '${type}'`); 9 | if (node.type !== type) throw new Error(`Invalid node type '${node.type}' expected '${type}'`); 10 | return node; 11 | } 12 | 13 | function makeNiceSyntaxError(message, code, filename, location, tokenizer) { 14 | const loc = acornGetLineInfo(code, location); 15 | let end = location; 16 | while (end < code.length && !acornIsNewLine(code.charCodeAt(end))) { 17 | end++; 18 | } 19 | let markerEnd = tokenizer.start === location ? tokenizer.end : location + 1; 20 | if (!markerEnd || markerEnd > end) markerEnd = end; 21 | let markerLen = markerEnd - location; 22 | if (markerLen <= 0) markerLen = 1; 23 | if (message === 'Unexpected token') { 24 | const type = tokenizer.type; 25 | if (type.label === 'name' || type.label === 'privateId') { 26 | message = 'Unexpected identifier'; 27 | } else if (type.label === 'eof') { 28 | message = 'Unexpected end of input'; 29 | } else if (type.label === 'num') { 30 | message = 'Unexpected number'; 31 | } else if (type.label === 'string') { 32 | message = 'Unexpected string'; 33 | } else if (type.label === 'regexp') { 34 | message = 'Unexpected token \'/\''; 35 | markerLen = 1; 36 | } else { 37 | const token = tokenizer.value || type.label; 38 | message = `Unexpected token '${token}'`; 39 | } 40 | } 41 | const error = new SyntaxError(message); 42 | if (!filename) return error; 43 | const line = code.slice(location - loc.column, end); 44 | const marker = line.slice(0, loc.column).replace(/\S/g, ' ') + '^'.repeat(markerLen); 45 | error.stack = `${filename}:${loc.line}\n${line}\n${marker}\n\n${error.stack}`; 46 | return error; 47 | } 48 | 49 | function transformer(args, body, isAsync, isGenerator, filename) { 50 | let code; 51 | let argsOffset; 52 | if (args === null) { 53 | code = body; 54 | // Note: Keywords are not allows to contain u escapes 55 | if (!/\b(?:catch|import|async)\b/.test(code)) { 56 | return {__proto__: null, code, hasAsync: false}; 57 | } 58 | } else { 59 | code = isAsync ? '(async function' : '(function'; 60 | if (isGenerator) code += '*'; 61 | code += ' anonymous('; 62 | code += args; 63 | argsOffset = code.length; 64 | code += '\n) {\n'; 65 | code += body; 66 | code += '\n})'; 67 | } 68 | 69 | const parser = new AcornParser({ 70 | __proto__: null, 71 | ecmaVersion: 2022, 72 | allowAwaitOutsideFunction: args === null && isAsync, 73 | allowReturnOutsideFunction: args === null 74 | }, code); 75 | let ast; 76 | try { 77 | ast = parser.parse(); 78 | } catch (e) { 79 | // Try to generate a nicer error message. 80 | if (e instanceof SyntaxError && e.pos !== undefined) { 81 | let message = e.message; 82 | const match = message.match(/^(.*) \(\d+:\d+\)$/); 83 | if (match) message = match[1]; 84 | e = makeNiceSyntaxError(message, code, filename, e.pos, parser); 85 | } 86 | throw e; 87 | } 88 | 89 | if (args !== null) { 90 | const pBody = assertType(ast, 'Program').body; 91 | if (pBody.length !== 1) throw new SyntaxError('Single function literal required'); 92 | const expr = pBody[0]; 93 | if (expr.type !== 'ExpressionStatement') throw new SyntaxError('Single function literal required'); 94 | const func = expr.expression; 95 | if (func.type !== 'FunctionExpression') throw new SyntaxError('Single function literal required'); 96 | if (func.body.start !== argsOffset + 3) throw new SyntaxError('Unexpected end of arg string'); 97 | } 98 | 99 | const insertions = []; 100 | let hasAsync = false; 101 | 102 | const TO_LEFT = -100; 103 | const TO_RIGHT = 100; 104 | 105 | let internStateValiable = undefined; 106 | let tmpname = 'VM2_INTERNAL_TMPNAME'; 107 | 108 | acornWalkFull(ast, (node, state, type) => { 109 | if (type === 'Function') { 110 | if (node.async) hasAsync = true; 111 | } 112 | const nodeType = node.type; 113 | if (nodeType === 'CatchClause') { 114 | const param = node.param; 115 | if (param) { 116 | if (param.type === 'Identifier') { 117 | const name = assertType(param, 'Identifier').name; 118 | const cBody = assertType(node.body, 'BlockStatement'); 119 | if (cBody.body.length > 0) { 120 | insertions.push({ 121 | __proto__: null, 122 | pos: cBody.body[0].start, 123 | order: TO_LEFT, 124 | coder: () => `${name}=${INTERNAL_STATE_NAME}.handleException(${name});` 125 | }); 126 | } 127 | } else { 128 | insertions.push({ 129 | __proto__: null, 130 | pos: node.start, 131 | order: TO_RIGHT, 132 | coder: () => `catch(${tmpname}){${tmpname}=${INTERNAL_STATE_NAME}.handleException(${tmpname});try{throw ${tmpname};}` 133 | }); 134 | insertions.push({ 135 | __proto__: null, 136 | pos: node.body.end, 137 | order: TO_LEFT, 138 | coder: () => `}` 139 | }); 140 | } 141 | } 142 | } else if (nodeType === 'WithStatement') { 143 | insertions.push({ 144 | __proto__: null, 145 | pos: node.object.start, 146 | order: TO_LEFT, 147 | coder: () => INTERNAL_STATE_NAME + '.wrapWith(' 148 | }); 149 | insertions.push({ 150 | __proto__: null, 151 | pos: node.object.end, 152 | order: TO_RIGHT, 153 | coder: () => ')' 154 | }); 155 | } else if (nodeType === 'Identifier') { 156 | if (node.name === INTERNAL_STATE_NAME) { 157 | if (internStateValiable === undefined || internStateValiable.start > node.start) { 158 | internStateValiable = node; 159 | } 160 | } else if (node.name.startsWith(tmpname)) { 161 | tmpname = node.name + '_UNIQUE'; 162 | } 163 | } else if (nodeType === 'ImportExpression') { 164 | insertions.push({ 165 | __proto__: null, 166 | pos: node.start, 167 | order: TO_RIGHT, 168 | coder: () => INTERNAL_STATE_NAME + '.' 169 | }); 170 | } 171 | }); 172 | 173 | if (internStateValiable) { 174 | throw makeNiceSyntaxError('Use of internal vm2 state variable', code, filename, internStateValiable.start, { 175 | __proto__: null, 176 | start: internStateValiable.start, 177 | end: internStateValiable.end 178 | }); 179 | } 180 | 181 | if (insertions.length === 0) return {__proto__: null, code, hasAsync}; 182 | 183 | insertions.sort((a, b) => (a.pos == b.pos ? a.order - b.order : a.pos - b.pos)); 184 | 185 | let ncode = ''; 186 | let curr = 0; 187 | for (let i = 0; i < insertions.length; i++) { 188 | const change = insertions[i]; 189 | ncode += code.substring(curr, change.pos) + change.coder(); 190 | curr = change.pos; 191 | } 192 | ncode += code.substring(curr); 193 | 194 | return {__proto__: null, code: ncode, hasAsync}; 195 | } 196 | 197 | exports.INTERNAL_STATE_NAME = INTERNAL_STATE_NAME; 198 | exports.transformer = transformer; 199 | -------------------------------------------------------------------------------- /lib/vm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This callback will be called to transform a script to JavaScript. 5 | * 6 | * @callback compileCallback 7 | * @param {string} code - Script code to transform to JavaScript. 8 | * @param {string} filename - Filename of this script. 9 | * @return {string} JavaScript code that represents the script code. 10 | */ 11 | 12 | /** 13 | * This callback will be called to resolve a module if it couldn't be found. 14 | * 15 | * @callback resolveCallback 16 | * @param {string} moduleName - Name of the modulusedRequiree to resolve. 17 | * @param {string} dirname - Name of the current directory. 18 | * @return {(string|undefined)} The file or directory to use to load the requested module. 19 | */ 20 | 21 | const fs = require('fs'); 22 | const pa = require('path'); 23 | const { 24 | Script, 25 | createContext 26 | } = require('vm'); 27 | const { 28 | EventEmitter 29 | } = require('events'); 30 | const { 31 | INSPECT_MAX_BYTES 32 | } = require('buffer'); 33 | const { 34 | createBridge, 35 | VMError 36 | } = require('./bridge'); 37 | const { 38 | transformer, 39 | INTERNAL_STATE_NAME 40 | } = require('./transformer'); 41 | const { 42 | lookupCompiler 43 | } = require('./compiler'); 44 | const { 45 | VMScript 46 | } = require('./script'); 47 | const { 48 | inspect 49 | } = require('util'); 50 | 51 | const objectDefineProperties = Object.defineProperties; 52 | 53 | /** 54 | * Host objects 55 | * 56 | * @private 57 | */ 58 | const HOST = Object.freeze({ 59 | Buffer, 60 | Function, 61 | Object, 62 | transformAndCheck, 63 | INSPECT_MAX_BYTES, 64 | INTERNAL_STATE_NAME 65 | }); 66 | 67 | /** 68 | * Compile a script. 69 | * 70 | * @private 71 | * @param {string} filename - Filename of the script. 72 | * @param {string} script - Script. 73 | * @return {vm.Script} The compiled script. 74 | */ 75 | function compileScript(filename, script) { 76 | return new Script(script, { 77 | __proto__: null, 78 | filename, 79 | displayErrors: false 80 | }); 81 | } 82 | 83 | /** 84 | * Default run options for vm.Script.runInContext 85 | * 86 | * @private 87 | */ 88 | const DEFAULT_RUN_OPTIONS = Object.freeze({__proto__: null, displayErrors: false}); 89 | 90 | function checkAsync(allow) { 91 | if (!allow) throw new VMError('Async not available'); 92 | } 93 | 94 | function transformAndCheck(args, code, isAsync, isGenerator, allowAsync) { 95 | const ret = transformer(args, code, isAsync, isGenerator, undefined); 96 | checkAsync(allowAsync || !ret.hasAsync); 97 | return ret.code; 98 | } 99 | 100 | /** 101 | * 102 | * This callback will be called and has a specific time to finish.
103 | * No parameters will be supplied.
104 | * If parameters are required, use a closure. 105 | * 106 | * @private 107 | * @callback runWithTimeout 108 | * @return {*} 109 | * 110 | */ 111 | 112 | let cacheTimeoutContext = null; 113 | let cacheTimeoutScript = null; 114 | 115 | /** 116 | * Run a function with a specific timeout. 117 | * 118 | * @private 119 | * @param {runWithTimeout} fn - Function to run with the specific timeout. 120 | * @param {number} timeout - The amount of time to give the function to finish. 121 | * @return {*} The value returned by the function. 122 | * @throws {Error} If the function took to long. 123 | */ 124 | function doWithTimeout(fn, timeout) { 125 | if (!cacheTimeoutContext) { 126 | cacheTimeoutContext = createContext(); 127 | cacheTimeoutScript = new Script('fn()', { 128 | __proto__: null, 129 | filename: 'timeout_bridge.js', 130 | displayErrors: false 131 | }); 132 | } 133 | cacheTimeoutContext.fn = fn; 134 | try { 135 | return cacheTimeoutScript.runInContext(cacheTimeoutContext, { 136 | __proto__: null, 137 | displayErrors: false, 138 | timeout 139 | }); 140 | } finally { 141 | cacheTimeoutContext.fn = null; 142 | } 143 | } 144 | 145 | const bridgeScript = compileScript(`${__dirname}/bridge.js`, 146 | `(function(global) {"use strict"; const exports = {};${fs.readFileSync(`${__dirname}/bridge.js`, 'utf8')}\nreturn exports;})`); 147 | const setupSandboxScript = compileScript(`${__dirname}/setup-sandbox.js`, 148 | `(function(global, host, bridge, data, context) { ${fs.readFileSync(`${__dirname}/setup-sandbox.js`, 'utf8')}\n})`); 149 | const getGlobalScript = compileScript('get_global.js', 'this'); 150 | 151 | let getGeneratorFunctionScript = null; 152 | let getAsyncFunctionScript = null; 153 | let getAsyncGeneratorFunctionScript = null; 154 | try { 155 | getGeneratorFunctionScript = compileScript('get_generator_function.js', '(function*(){}).constructor'); 156 | } catch (ex) {} 157 | try { 158 | getAsyncFunctionScript = compileScript('get_async_function.js', '(async function(){}).constructor'); 159 | } catch (ex) {} 160 | try { 161 | getAsyncGeneratorFunctionScript = compileScript('get_async_generator_function.js', '(async function*(){}).constructor'); 162 | } catch (ex) {} 163 | 164 | /** 165 | * Class VM. 166 | * 167 | * @public 168 | */ 169 | class VM extends EventEmitter { 170 | 171 | /** 172 | * The timeout for {@link VM#run} calls. 173 | * 174 | * @public 175 | * @since v3.9.0 176 | * @member {number} timeout 177 | * @memberOf VM# 178 | */ 179 | 180 | /** 181 | * Get the global sandbox object. 182 | * 183 | * @public 184 | * @readonly 185 | * @since v3.9.0 186 | * @member {Object} sandbox 187 | * @memberOf VM# 188 | */ 189 | 190 | /** 191 | * The compiler to use to get the JavaScript code. 192 | * 193 | * @public 194 | * @readonly 195 | * @since v3.9.0 196 | * @member {(string|compileCallback)} compiler 197 | * @memberOf VM# 198 | */ 199 | 200 | /** 201 | * The resolved compiler to use to get the JavaScript code. 202 | * 203 | * @private 204 | * @readonly 205 | * @member {compileCallback} _compiler 206 | * @memberOf VM# 207 | */ 208 | 209 | /** 210 | * Create a new VM instance. 211 | * 212 | * @public 213 | * @param {Object} [options] - VM options. 214 | * @param {number} [options.timeout] - The amount of time until a call to {@link VM#run} will timeout. 215 | * @param {Object} [options.sandbox] - Objects that will be copied into the global object of the sandbox. 216 | * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. 217 | * @param {boolean} [options.eval=true] - Allow the dynamic evaluation of code via eval(code) or Function(code)().
218 | * Only available for node v10+. 219 | * @param {boolean} [options.wasm=true] - Allow to run wasm code.
220 | * Only available for node v10+. 221 | * @param {boolean} [options.allowAsync=true] - Allows for async functions. 222 | * @throws {VMError} If the compiler is unknown. 223 | */ 224 | constructor(options = {}) { 225 | super(); 226 | 227 | // Read all options 228 | const { 229 | timeout, 230 | sandbox, 231 | compiler = 'javascript', 232 | allowAsync: optAllowAsync = true 233 | } = options; 234 | const allowEval = options.eval !== false; 235 | const allowWasm = options.wasm !== false; 236 | const allowAsync = optAllowAsync && !options.fixAsync; 237 | 238 | // Early error if sandbox is not an object. 239 | if (sandbox && 'object' !== typeof sandbox) { 240 | throw new VMError('Sandbox must be object.'); 241 | } 242 | 243 | // Early error if compiler can't be found. 244 | const resolvedCompiler = lookupCompiler(compiler); 245 | 246 | // Create a new context for this vm. 247 | const _context = createContext(undefined, { 248 | __proto__: null, 249 | codeGeneration: { 250 | __proto__: null, 251 | strings: allowEval, 252 | wasm: allowWasm 253 | } 254 | }); 255 | 256 | const sandboxGlobal = getGlobalScript.runInContext(_context, DEFAULT_RUN_OPTIONS); 257 | 258 | // Initialize the sandbox bridge 259 | const { 260 | createBridge: sandboxCreateBridge 261 | } = bridgeScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal); 262 | 263 | // Initialize the bridge 264 | const bridge = createBridge(sandboxCreateBridge, () => {}); 265 | 266 | const data = { 267 | __proto__: null, 268 | allowAsync 269 | }; 270 | 271 | if (getGeneratorFunctionScript) { 272 | data.GeneratorFunction = getGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS); 273 | } 274 | if (getAsyncFunctionScript) { 275 | data.AsyncFunction = getAsyncFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS); 276 | } 277 | if (getAsyncGeneratorFunctionScript) { 278 | data.AsyncGeneratorFunction = getAsyncGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS); 279 | } 280 | 281 | // Create the bridge between the host and the sandbox. 282 | const internal = setupSandboxScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal, HOST, bridge.other, data, _context); 283 | 284 | const runScript = (script) => { 285 | // This closure is intentional to hide _context and bridge since the allow to access the sandbox directly which is unsafe. 286 | let ret; 287 | try { 288 | ret = script.runInContext(_context, DEFAULT_RUN_OPTIONS); 289 | } catch (e) { 290 | throw bridge.from(e); 291 | } 292 | return bridge.from(ret); 293 | }; 294 | 295 | const makeReadonly = (value, mock) => { 296 | try { 297 | internal.readonly(value, mock); 298 | } catch (e) { 299 | throw bridge.from(e); 300 | } 301 | return value; 302 | }; 303 | 304 | const makeProtected = (value) => { 305 | const sandboxBridge = bridge.other; 306 | try { 307 | sandboxBridge.fromWithFactory(sandboxBridge.protectedFactory, value); 308 | } catch (e) { 309 | throw bridge.from(e); 310 | } 311 | return value; 312 | }; 313 | 314 | const addProtoMapping = (hostProto, sandboxProto) => { 315 | const sandboxBridge = bridge.other; 316 | let otherProto; 317 | try { 318 | otherProto = sandboxBridge.from(sandboxProto); 319 | sandboxBridge.addProtoMapping(otherProto, hostProto); 320 | } catch (e) { 321 | throw bridge.from(e); 322 | } 323 | bridge.addProtoMapping(hostProto, otherProto); 324 | }; 325 | 326 | const addProtoMappingFactory = (hostProto, sandboxProtoFactory) => { 327 | const sandboxBridge = bridge.other; 328 | const factory = () => { 329 | const proto = sandboxProtoFactory(this); 330 | bridge.addProtoMapping(hostProto, proto); 331 | return proto; 332 | }; 333 | try { 334 | const otherProtoFactory = sandboxBridge.from(factory); 335 | sandboxBridge.addProtoMappingFactory(otherProtoFactory, hostProto); 336 | } catch (e) { 337 | throw bridge.from(e); 338 | } 339 | }; 340 | 341 | // Define the properties of this object. 342 | // Use Object.defineProperties here to be able to 343 | // hide and set properties read-only. 344 | objectDefineProperties(this, { 345 | __proto__: null, 346 | timeout: { 347 | __proto__: null, 348 | value: timeout, 349 | writable: true, 350 | enumerable: true 351 | }, 352 | compiler: { 353 | __proto__: null, 354 | value: compiler, 355 | enumerable: true 356 | }, 357 | sandbox: { 358 | __proto__: null, 359 | value: bridge.from(sandboxGlobal), 360 | enumerable: true 361 | }, 362 | _runScript: {__proto__: null, value: runScript}, 363 | _makeReadonly: {__proto__: null, value: makeReadonly}, 364 | _makeProtected: {__proto__: null, value: makeProtected}, 365 | _addProtoMapping: {__proto__: null, value: addProtoMapping}, 366 | _addProtoMappingFactory: {__proto__: null, value: addProtoMappingFactory}, 367 | _compiler: {__proto__: null, value: resolvedCompiler}, 368 | _allowAsync: {__proto__: null, value: allowAsync} 369 | }); 370 | 371 | this.readonly(inspect); 372 | 373 | // prepare global sandbox 374 | if (sandbox) { 375 | this.setGlobals(sandbox); 376 | } 377 | } 378 | 379 | /** 380 | * Adds all the values to the globals. 381 | * 382 | * @public 383 | * @since v3.9.0 384 | * @param {Object} values - All values that will be added to the globals. 385 | * @return {this} This for chaining. 386 | * @throws {*} If the setter of a global throws an exception it is propagated. And the remaining globals will not be written. 387 | */ 388 | setGlobals(values) { 389 | for (const name in values) { 390 | if (Object.prototype.hasOwnProperty.call(values, name)) { 391 | this.sandbox[name] = values[name]; 392 | } 393 | } 394 | return this; 395 | } 396 | 397 | /** 398 | * Set a global value. 399 | * 400 | * @public 401 | * @since v3.9.0 402 | * @param {string} name - The name of the global. 403 | * @param {*} value - The value of the global. 404 | * @return {this} This for chaining. 405 | * @throws {*} If the setter of the global throws an exception it is propagated. 406 | */ 407 | setGlobal(name, value) { 408 | this.sandbox[name] = value; 409 | return this; 410 | } 411 | 412 | /** 413 | * Get a global value. 414 | * 415 | * @public 416 | * @since v3.9.0 417 | * @param {string} name - The name of the global. 418 | * @return {*} The value of the global. 419 | * @throws {*} If the getter of the global throws an exception it is propagated. 420 | */ 421 | getGlobal(name) { 422 | return this.sandbox[name]; 423 | } 424 | 425 | /** 426 | * Freezes the object inside VM making it read-only. Not available for primitive values. 427 | * 428 | * @public 429 | * @param {*} value - Object to freeze. 430 | * @param {string} [globalName] - Whether to add the object to global. 431 | * @return {*} Object to freeze. 432 | * @throws {*} If the setter of the global throws an exception it is propagated. 433 | */ 434 | freeze(value, globalName) { 435 | this.readonly(value); 436 | if (globalName) this.sandbox[globalName] = value; 437 | return value; 438 | } 439 | 440 | /** 441 | * Freezes the object inside VM making it read-only. Not available for primitive values. 442 | * 443 | * @public 444 | * @param {*} value - Object to freeze. 445 | * @param {*} [mock] - When the object does not have a property the mock is used before prototype lookup. 446 | * @return {*} Object to freeze. 447 | */ 448 | readonly(value, mock) { 449 | return this._makeReadonly(value, mock); 450 | } 451 | 452 | /** 453 | * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. 454 | * 455 | * @public 456 | * @param {*} value - Object to protect. 457 | * @param {string} [globalName] - Whether to add the object to global. 458 | * @return {*} Object to protect. 459 | * @throws {*} If the setter of the global throws an exception it is propagated. 460 | */ 461 | protect(value, globalName) { 462 | this._makeProtected(value); 463 | if (globalName) this.sandbox[globalName] = value; 464 | return value; 465 | } 466 | 467 | /** 468 | * Run the code in VM. 469 | * 470 | * @public 471 | * @param {(string|VMScript)} code - Code to run. 472 | * @param {(string|Object)} [options] - Options map or filename. 473 | * @param {string} [options.filename="vm.js"] - Filename that shows up in any stack traces produced from this script.
474 | * This is only used if code is a String. 475 | * @return {*} Result of executed code. 476 | * @throws {SyntaxError} If there is a syntax error in the script. 477 | * @throws {Error} An error is thrown when the script took to long and there is a timeout. 478 | * @throws {*} If the script execution terminated with an exception it is propagated. 479 | */ 480 | run(code, options) { 481 | let script; 482 | let filename; 483 | 484 | if (typeof options === 'object') { 485 | filename = options.filename; 486 | } else { 487 | filename = options; 488 | } 489 | 490 | if (code instanceof VMScript) { 491 | script = code._compileVM(); 492 | checkAsync(this._allowAsync || !code._hasAsync); 493 | } else { 494 | const useFileName = filename || 'vm.js'; 495 | let scriptCode = this._compiler(code, useFileName); 496 | const ret = transformer(null, scriptCode, false, false, useFileName); 497 | scriptCode = ret.code; 498 | checkAsync(this._allowAsync || !ret.hasAsync); 499 | // Compile the script here so that we don't need to create a instance of VMScript. 500 | script = new Script(scriptCode, { 501 | __proto__: null, 502 | filename: useFileName, 503 | displayErrors: false 504 | }); 505 | } 506 | 507 | if (!this.timeout) { 508 | return this._runScript(script); 509 | } 510 | 511 | return doWithTimeout(() => { 512 | return this._runScript(script); 513 | }, this.timeout); 514 | } 515 | 516 | /** 517 | * Run the code in VM. 518 | * 519 | * @public 520 | * @since v3.9.0 521 | * @param {string} filename - Filename of file to load and execute in a NodeVM. 522 | * @return {*} Result of executed code. 523 | * @throws {Error} If filename is not a valid filename. 524 | * @throws {SyntaxError} If there is a syntax error in the script. 525 | * @throws {Error} An error is thrown when the script took to long and there is a timeout. 526 | * @throws {*} If the script execution terminated with an exception it is propagated. 527 | */ 528 | runFile(filename) { 529 | const resolvedFilename = pa.resolve(filename); 530 | 531 | if (!fs.existsSync(resolvedFilename)) { 532 | throw new VMError(`Script '${filename}' not found.`); 533 | } 534 | 535 | if (fs.statSync(resolvedFilename).isDirectory()) { 536 | throw new VMError('Script must be file, got directory.'); 537 | } 538 | 539 | return this.run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename); 540 | } 541 | 542 | } 543 | 544 | exports.VM = VM; 545 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Patrik Simek", 4 | "url": "https://patriksimek.cz" 5 | }, 6 | "name": "vm2", 7 | "description": "vm2 is a sandbox that can run untrusted code with whitelisted Node's built-in modules. Securely!", 8 | "keywords": [ 9 | "sandbox", 10 | "prison", 11 | "jail", 12 | "vm", 13 | "alcatraz", 14 | "contextify" 15 | ], 16 | "version": "3.9.19", 17 | "main": "index.js", 18 | "sideEffects": false, 19 | "repository": "github:patriksimek/vm2", 20 | "license": "MIT", 21 | "dependencies": { 22 | "acorn": "^8.7.0", 23 | "acorn-walk": "^8.2.0" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^5.16.0", 27 | "eslint-config-integromat": "^1.5.0", 28 | "mocha": "^6.2.2" 29 | }, 30 | "engines": { 31 | "node": ">=18.0" 32 | }, 33 | "scripts": { 34 | "test": "mocha test", 35 | "pretest": "eslint ." 36 | }, 37 | "bin": { 38 | "vm2": "./bin/vm2" 39 | }, 40 | "types": "index.d.ts" 41 | } 42 | -------------------------------------------------------------------------------- /test/additional-modules/my-es-module/index.cjs: -------------------------------------------------------------------------------- 1 | module.exports = {additional_cjs_module: true}; -------------------------------------------------------------------------------- /test/additional-modules/my-es-module/index.js: -------------------------------------------------------------------------------- 1 | export default {additional_es_module: true}; -------------------------------------------------------------------------------- /test/additional-modules/my-es-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "type": "module", 4 | "exports": { 5 | ".": { 6 | "default": { 7 | "require": "./index.cjs", 8 | "default": "./index.js" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/additional-modules/my-module/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {additional_module: true}; 2 | -------------------------------------------------------------------------------- /test/data/custom_extension.ts: -------------------------------------------------------------------------------- 1 | 1 + 1; 2 | -------------------------------------------------------------------------------- /test/data/json.json: -------------------------------------------------------------------------------- 1 | {"working": true} 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 2000 2 | --reporter spec 3 | -------------------------------------------------------------------------------- /test/node_modules/foobar/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/node_modules/module-main-without-extension/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bar: () => 1 3 | }; 4 | -------------------------------------------------------------------------------- /test/node_modules/module-main-without-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "something", 3 | "version": "0.0.1", 4 | "main": "main" 5 | } 6 | -------------------------------------------------------------------------------- /test/node_modules/module-with-wrong-main/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bar: () => 1 3 | }; 4 | -------------------------------------------------------------------------------- /test/node_modules/module-with-wrong-main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "something", 3 | "version": "0.0.1", 4 | "main": "foo.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/node_modules/module1/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('module2'); 2 | -------------------------------------------------------------------------------- /test/node_modules/module2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/node_modules/require/index.js: -------------------------------------------------------------------------------- 1 | exports.require = require; 2 | -------------------------------------------------------------------------------- /test/node_modules/with-exports/main.js: -------------------------------------------------------------------------------- 1 | exports.ok = true; 2 | -------------------------------------------------------------------------------- /test/node_modules/with-exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-exports", 3 | "exports": { 4 | ".": { 5 | "require": "./main.js" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/nodevm.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable no-new-wrappers, max-len */ 3 | 4 | 'use strict'; 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const assert = require('assert'); 9 | const {EventEmitter} = require('events'); 10 | const {NodeVM, VMScript, makeResolverFromLegacyOptions} = require('..'); 11 | // const NODE_VERSION = parseInt(process.versions.node.split('.')[0]); 12 | 13 | global.isHost = true; 14 | 15 | function isVMProxy(obj) { 16 | const key = {}; 17 | const proto = Object.getPrototypeOf(obj); 18 | if (!proto) return undefined; 19 | proto.isVMProxy = key; 20 | const proxy = obj.isVMProxy !== key; 21 | delete proto.isVMProxy; 22 | return proxy; 23 | } 24 | 25 | describe('NodeVM', () => { 26 | let vm; 27 | 28 | const customArgv = []; 29 | const customEnv = {}; 30 | 31 | before(() => { 32 | vm = new NodeVM({ 33 | argv: customArgv, 34 | env: customEnv 35 | }); 36 | }); 37 | 38 | it('globals', () => { 39 | const ex = vm.run('module.exports = global'); 40 | assert.equal(ex.isHost, undefined); 41 | }); 42 | 43 | it('options', ()=>{ 44 | const vmProcess = vm.run('module.exports = process'); 45 | assert.equal(vmProcess.argv, customArgv); 46 | assert.equal(vmProcess.env, customEnv); 47 | }); 48 | 49 | it('errors', () => { 50 | assert.throws(() => vm.run('notdefined'), /notdefined is not defined/); 51 | }); 52 | 53 | it('prevent global access', () => { 54 | assert.throws(() => vm.run('process.exit()'), /(undefined is not a function|process\.exit is not a function)/); 55 | }); 56 | 57 | it('arguments attack', () => { 58 | assert.strictEqual(vm.run('module.exports = (function() { return arguments.callee.caller.constructor === Function; })()'), true); 59 | assert.throws(() => vm.run('module.exports = (function() { return arguments.callee.caller.caller.toString(); })()'), /Cannot read propert.*toString/); 60 | }); 61 | 62 | it('global attack', () => { 63 | assert.equal(vm.run("module.exports = console.log.constructor('return (function(){return this})().isHost')()"), undefined); 64 | }); 65 | 66 | it('shebang', () => { 67 | assert.doesNotThrow(() => vm.run('#!shebang')); 68 | }); 69 | 70 | it('strict', () => { 71 | assert.doesNotThrow(() => vm.run('newGlobal = 2;')); 72 | assert.throws(() => new NodeVM({strict: true}).run('newGlobal = 2;'), /ReferenceError: newGlobal is not defined/); 73 | }); 74 | 75 | it.skip('timeout (not supported by Node\'s VM)', () => { 76 | assert.throws(() => new NodeVM({ 77 | timeout: 10 78 | }).run('while (true) {}'), /Script execution timed out\./); 79 | }); 80 | 81 | after(() => { 82 | vm = null; 83 | }); 84 | }); 85 | 86 | describe('modules', () => { 87 | it('require json', () => { 88 | const vm = new NodeVM({ 89 | require: { 90 | external: true, 91 | context: 'sandbox' 92 | } 93 | }); 94 | 95 | assert.equal(vm.run(`module.exports = require('./data/json.json')`, `${__dirname}/vm.js`).working, true); 96 | }); 97 | 98 | it.skip('run coffee-script', () => { 99 | const vm = new NodeVM({ 100 | require: { 101 | external: true 102 | }, 103 | compiler: 'coffeescript' 104 | }); 105 | 106 | assert.equal(vm.run('module.exports = working: true').working, true); 107 | }); 108 | 109 | it('optionally can run a custom compiler function', () => { 110 | let ranCustomCompiler = false; 111 | const scriptCode = 'var a = 1;'; 112 | const vm = new NodeVM({ 113 | compiler: (code) => { 114 | ranCustomCompiler = true; 115 | assert.equal(code, scriptCode); 116 | } 117 | }); 118 | vm.run(scriptCode); 119 | assert.equal(ranCustomCompiler, true); 120 | }); 121 | 122 | it('optionally passes a filename to a custom compiler function', () => { 123 | let ranCustomCompiler = false; 124 | const vm = new NodeVM({ 125 | compiler: (code, filename) => { 126 | ranCustomCompiler = true; 127 | assert.equal(filename, '/a/b/c.js'); 128 | } 129 | }); 130 | vm.run('module.exports = working: true', '/a/b/c.js'); 131 | assert.equal(ranCustomCompiler, true); 132 | }); 133 | 134 | it('disabled require', () => { 135 | const vm = new NodeVM; 136 | 137 | assert.throws(() => vm.run("require('fs')"), /Cannot find module 'fs'/); 138 | }); 139 | 140 | it('disable setters on builtin modules', () => { 141 | const vm = new NodeVM({ 142 | require: { 143 | builtin: ['fs'] 144 | } 145 | }); 146 | 147 | vm.run("require('fs').readFileSync = undefined"); 148 | assert.strictEqual(fs.readFileSync instanceof Function, true); 149 | 150 | vm.run("require('fs').readFileSync.thisPropertyShouldntBeThere = true"); 151 | assert.strictEqual(fs.readFileSync.thisPropertyShouldntBeThere, undefined); 152 | 153 | assert.throws(() => vm.run("Object.defineProperty(require('fs'), 'test', {})"), err => { 154 | assert.ok(err instanceof TypeError); 155 | assert.equal(err.name, 'TypeError'); 156 | assert.equal(err.message, '\'defineProperty\' on proxy: trap returned falsish for property \'test\''); 157 | return true; 158 | }); 159 | 160 | assert.throws(() => vm.run("'use strict'; delete require('fs').readFileSync"), err => { 161 | assert.ok(err instanceof TypeError); 162 | assert.equal(err.name, 'TypeError'); 163 | assert.equal(err.message, '\'deleteProperty\' on proxy: trap returned falsish for property \'readFileSync\''); 164 | return true; 165 | }); 166 | }); 167 | 168 | it('enabled require for certain modules', () => { 169 | const vm = new NodeVM({ 170 | require: { 171 | builtin: ['fs'] 172 | } 173 | }); 174 | 175 | assert.doesNotThrow(() => vm.run("require('fs')")); 176 | }); 177 | 178 | it('require relative', () => { 179 | const vm = new NodeVM({ 180 | require: { 181 | external: true 182 | }, 183 | }); 184 | 185 | vm.run("require('foobar')", __filename); 186 | }); 187 | 188 | it('can require a module inside the vm', () => { 189 | const vm = new NodeVM({ 190 | require: { 191 | external: true 192 | } 193 | }); 194 | 195 | vm.run("require('mocha')", __filename); 196 | }); 197 | 198 | it('can deny requiring modules inside the vm', () => { 199 | const vm = new NodeVM({ 200 | require: { 201 | external: false 202 | }, 203 | }); 204 | 205 | assert.throws(() => vm.run("require('mocha')", __filename), err => { 206 | assert.equal(err.name, 'VMError'); 207 | assert.equal(err.message, 'Cannot find module \'mocha\''); 208 | return true; 209 | }); 210 | }); 211 | 212 | it('can whitelist modules inside the vm', () => { 213 | const vm = new NodeVM({ 214 | require: { 215 | external: ['mocha'] 216 | } 217 | }); 218 | 219 | assert.ok(vm.run("require('mocha')", __filename)); 220 | assert.throws(() => vm.run("require('unknown')", __filename), err => { 221 | assert.equal(err.name, 'VMError'); 222 | assert.equal(err.message, "Cannot find module 'unknown'"); 223 | return true; 224 | }); 225 | }); 226 | 227 | it('allows specific transitive external dependencies in sandbox context', () => { 228 | const vm = new NodeVM({ 229 | require: { 230 | external: { 231 | modules: ['module1'], 232 | transitive: true 233 | }, 234 | context: 'sandbox' 235 | } 236 | }); 237 | 238 | assert.ok(vm.run("require('module1')", __filename)); 239 | }); 240 | 241 | it('allows choosing a context by path legacy', () => { 242 | const vm = new NodeVM({ 243 | require: { 244 | external: { 245 | modules: ['mocha', 'module1'], 246 | transitive: true, 247 | }, 248 | context(module) { 249 | if (module.includes('mocha')) return 'host'; 250 | return 'sandbox'; 251 | } 252 | } 253 | }); 254 | assert.equal(isVMProxy(vm.run("module.exports = require('mocha')", __filename)), false, 'Mocha is a proxy'); 255 | assert.equal(isVMProxy(vm.run("module.exports = require('module1')", __filename)), true, 'Module1 is not a proxy'); 256 | }); 257 | 258 | it('allows choosing a context by path', () => { 259 | const vm = new NodeVM({ 260 | require: { 261 | external: true, 262 | context(module) { 263 | if (module.includes('mocha')) return 'host'; 264 | return 'sandbox'; 265 | } 266 | } 267 | }); 268 | assert.equal(isVMProxy(vm.run("module.exports = require('mocha')", __filename)), false, 'Mocha is a proxy'); 269 | assert.equal(isVMProxy(vm.run("module.exports = require('module1')", __filename)), true, 'Module1 is not a proxy'); 270 | }); 271 | 272 | it('can resolve paths based on a custom resolver', () => { 273 | const vm = new NodeVM({ 274 | require: { 275 | external: ['my-module'], 276 | resolve: moduleName => path.resolve(__dirname, 'additional-modules', moduleName) 277 | } 278 | }); 279 | 280 | assert.ok(vm.run("require('my-module')", __filename)); 281 | }); 282 | 283 | it('can resolve conditional exports with a custom resolver', () => { 284 | const vm = new NodeVM({ 285 | require: { 286 | external: ['my-es-module'], 287 | resolve: () => ({ path: path.resolve(__dirname, 'additional-modules') }) 288 | } 289 | }); 290 | 291 | assert.ok(vm.run("require('my-es-module')", __filename)); 292 | }); 293 | 294 | it('allows for multiple root folders', () => { 295 | const vm = new NodeVM({ 296 | require: { 297 | external: ['mocha'], 298 | root: [ 299 | path.resolve(__dirname), 300 | path.resolve(__dirname, '..', 'node_modules') 301 | ] 302 | } 303 | }); 304 | 305 | assert.ok(vm.run("require('mocha')", __filename)); 306 | }); 307 | 308 | it('falls back to index.js if the file specified in the package.json "main" attribute is missing', () => { 309 | const vm = new NodeVM({ 310 | require: { 311 | external: true 312 | } 313 | }); 314 | 315 | assert.equal(vm.run("module.exports = require('module-with-wrong-main').bar()", __filename), 1); 316 | }); 317 | 318 | it('attempts to add extension if the file specified in the package.json "main" attribute is missing', () => { 319 | const vm = new NodeVM({ 320 | require: { 321 | external: true 322 | } 323 | }); 324 | 325 | assert.equal(vm.run("module.exports = require('module-main-without-extension').bar()", __filename), 1); 326 | }); 327 | 328 | it('module with exports', () => { 329 | const vm = new NodeVM({ 330 | require: { 331 | external: [ 332 | 'with-exports' 333 | ] 334 | } 335 | }); 336 | 337 | assert.strictEqual(vm.run("module.exports = require('with-exports')", __filename).ok, true); 338 | 339 | }); 340 | 341 | it('whitelist check before custom resolver', () => { 342 | const vm = new NodeVM({ 343 | require: { 344 | external: [], 345 | resolve: () => { 346 | throw new Error('Unexpected'); 347 | }, 348 | }, 349 | }); 350 | 351 | assert.throws(() => vm.run("require('mocha')", __filename), /Cannot find module 'mocha'/); 352 | }); 353 | 354 | it('root path checking', () => { 355 | const vm = new NodeVM({ 356 | require: { 357 | external: true, 358 | root: `${__dirname}/node_modules/module` 359 | }, 360 | }); 361 | 362 | assert.throws(() => vm.run("require('module2')", __filename), /Cannot find module 'module2'/); 363 | }); 364 | 365 | it('relative require not allowed to enter node modules', () => { 366 | const vm = new NodeVM({ 367 | require: { 368 | external: ['mocha'], 369 | root: `${__dirname}` 370 | }, 371 | }); 372 | 373 | assert.throws(() => vm.run("require('./node_modules/module2')", __filename), /Cannot find module '\.\/node_modules\/module2'/); 374 | }); 375 | 376 | it('outer require', () => { 377 | const vm = new NodeVM({ 378 | require: { 379 | external: [], 380 | context: 'sandbox', 381 | root: `${__dirname}` 382 | }, 383 | }); 384 | assert.strictEqual(vm.require(`${__dirname}/data/json.json`).working, true); 385 | assert.strictEqual(vm.require(`${__dirname}/additional-modules/my-module`).additional_module, true); 386 | }); 387 | 388 | it('arguments attack', () => { 389 | let vm = new NodeVM; 390 | 391 | assert.throws(() => vm.run('module.exports = function fce(msg) { return arguments.callee.caller.toString(); }')(), /Cannot read propert.*toString/); 392 | 393 | vm = new NodeVM; 394 | 395 | assert.throws(() => vm.run('module.exports = function fce(msg) { return fce.caller.toString(); }')(), /Cannot read propert.*toString/); 396 | }); 397 | 398 | it('builtin module arguments attack', done => { 399 | const vm = new NodeVM({ 400 | require: { 401 | builtin: ['fs'] 402 | }, 403 | sandbox: { 404 | parentfilename: __filename, 405 | done 406 | } 407 | }); 408 | 409 | vm.run("var fs = require('fs'); fs.exists(parentfilename, function() {try {arguments.callee.caller.toString()} catch (err) {return done();}; done(new Error('Missing expected exception'))})"); 410 | }); 411 | 412 | it('path attack', () => { 413 | const vm = new NodeVM({ 414 | require: { 415 | external: true, 416 | root: __dirname 417 | } 418 | }); 419 | 420 | assert.throws(() => vm.run("var test = require('../package.json')", __filename), /Cannot find module '\.\.\/package.json'/); 421 | }); 422 | 423 | it('process events', () => { 424 | const vm = new NodeVM({ 425 | sandbox: { 426 | VM2_COUNTER: 0 427 | } 428 | }); 429 | 430 | const sandbox = vm.run("global.VM2_HANDLER = function() { VM2_COUNTER++ }; process.on('exit', VM2_HANDLER); module.exports = global;"); 431 | process.emit('exit'); 432 | assert.strictEqual(sandbox.VM2_COUNTER, 1); 433 | assert.strictEqual(vm.run("module.exports = process.listeners('exit')[0] === VM2_HANDLER;"), true); 434 | vm.run("process.removeListener('exit', VM2_HANDLER);"); 435 | process.emit('exit'); 436 | assert.strictEqual(sandbox.VM2_COUNTER, 1); 437 | 438 | process.on('exit', () => {}); // Attach event in host 439 | assert.strictEqual(process.listeners('exit').length, 1); // Sandbox must only see it's own handlers 440 | 441 | const vmm = new NodeVM({}); 442 | assert.strictEqual(vmm.run("module.exports = process.listeners('exit')").length, 0); // Listeners must not be visible cross-sandbox 443 | }); 444 | 445 | it('timers #1', done => { 446 | const vm = new NodeVM({ 447 | sandbox: { 448 | done 449 | } 450 | }); 451 | 452 | vm.run('let i = setImmediate(function() { global.TICK = true; });clearImmediate(i);'); 453 | 454 | setImmediate(() => { 455 | assert.strictEqual(vm.run('module.exports = global.TICK'), void 0); 456 | vm.run('setImmediate(done);'); 457 | }); 458 | }); 459 | 460 | it('timers #2', done => { 461 | const start = Date.now(); 462 | const vm = new NodeVM({ 463 | sandbox: { 464 | done: (arg) => { 465 | assert.strictEqual(arg, 1337); 466 | assert.ok(Date.now() - start >= 200); 467 | done(); 468 | } 469 | } 470 | }); 471 | 472 | vm.run('setTimeout((arg) => done(arg), 200, 1337);'); 473 | }); 474 | 475 | it('mock', () => { 476 | const vm = new NodeVM({ 477 | require: { 478 | mock: { 479 | fs: { 480 | readFileSync() { 481 | return 'Nice try!'; 482 | } 483 | } 484 | } 485 | } 486 | }); 487 | 488 | assert.strictEqual(vm.run("module.exports = require('fs').constructor.constructor === Function"), true); 489 | assert.strictEqual(vm.run("module.exports = require('fs').readFileSync.constructor.constructor === Function"), true); 490 | assert.strictEqual(vm.run("module.exports = require('fs').readFileSync()"), 'Nice try!'); 491 | }); 492 | 493 | it('missing contextify attack', () => { 494 | const vm = new NodeVM(); 495 | 496 | // https://github.com/patriksimek/vm2/issues/276 497 | assert.strictEqual(vm.run('const timeout = setTimeout(()=>{});module.exports = !timeout.ref || timeout.ref().constructor.constructor === Function'), true); 498 | 499 | // https://github.com/patriksimek/vm2/issues/285 500 | assert.strictEqual(vm.run(`try { 501 | process.listeners({toString(){return {};}}); 502 | module.exports = true; 503 | } catch(e) { 504 | module.exports = e.constructor.constructor === Function; 505 | }`), true); 506 | 507 | }); 508 | 509 | it('native event emitter', () => { 510 | const vm = new NodeVM({ 511 | require: { 512 | builtin: ['events'] 513 | } 514 | }); 515 | 516 | assert.ok(vm.run(`const {EventEmitter} = require('events'); const ee = new EventEmitter(); let tr; ee.on('test', ()=>{tr = true;}); ee.emit('test'); return tr`, {wrapper: 'none'})); 517 | assert.ok(vm.run('const {EventEmitter} = require("events"); return new EventEmitter()', {wrapper: 'none'}) instanceof EventEmitter); 518 | assert.ok(vm.run('return nei => nei instanceof require("events").EventEmitter', {wrapper: 'none'})(new EventEmitter())); 519 | assert.ok(vm.run(` 520 | const {EventEmitter} = require('events'); 521 | class EEE extends EventEmitter { 522 | test() {return true;} 523 | } 524 | return new EEE().test(); 525 | `, {wrapper: 'none'})); 526 | 527 | }); 528 | 529 | it('cache modules', () => { 530 | const vm = new NodeVM({ 531 | require: { 532 | context: 'sandbox', 533 | external: ['module1', 'module2', 'require'], 534 | builtin: ['*'] 535 | } 536 | }); 537 | assert.ok(vm.run('return require("module1") === require("module2")', {filename: `${__dirname}/vm.js`, wrapper: 'none'})); 538 | assert.ok(vm.run('return require("require").require("fs") === require("fs")', {filename: `${__dirname}/vm.js`, wrapper: 'none'})); 539 | assert.ok(vm.run('return require("require").require("buffer") === require("buffer")', {filename: `${__dirname}/vm.js`, wrapper: 'none'})); 540 | assert.ok(vm.run('return require("require").require("util") === require("util")', {filename: `${__dirname}/vm.js`, wrapper: 'none'})); 541 | }); 542 | 543 | it('strict module name checks', () => { 544 | const vm = new NodeVM({ 545 | require: { 546 | external: ['module'] 547 | } 548 | }); 549 | assert.throws(()=>vm.run('require("module1")', `${__dirname}/vm.js`), /Cannot find module 'module1'/); 550 | }); 551 | 552 | it('module name globs', () => { 553 | const vm = new NodeVM({ 554 | require: { 555 | external: ['mo?ule1', 'm*e2'] 556 | } 557 | }); 558 | assert.doesNotThrow(()=>vm.run('require("module1");require("module2")', `${__dirname}/vm.js`)); 559 | }); 560 | 561 | it('module name glob escape', () => { 562 | const vm = new NodeVM({ 563 | require: { 564 | external: ['module1*'] 565 | } 566 | }); 567 | assert.throws(()=>vm.run('require("module1/../module2")', `${__dirname}/vm.js`), /Cannot find module 'module1\/..\/module2'/); 568 | }); 569 | 570 | }); 571 | 572 | describe('nesting', () => { 573 | it('NodeVM', () => { 574 | const vm = new NodeVM({ 575 | nesting: true 576 | }); 577 | 578 | const nestedObject = vm.run(` 579 | const {VM} = require('vm2'); 580 | const vm = new VM(); 581 | let o = vm.run('({})'); 582 | module.exports = o; 583 | `, 'vm.js'); 584 | 585 | assert.strictEqual(nestedObject.constructor.constructor === Function, true); 586 | }); 587 | }); 588 | 589 | describe('wrappers', () => { 590 | it('none', () => { 591 | const vm = new NodeVM({ 592 | wrapper: 'none' 593 | }); 594 | 595 | assert.strictEqual(vm.run('return 2 + 2'), 4); 596 | }); 597 | }); 598 | 599 | describe('precompiled scripts', () => { 600 | it('NodeVM', () => { 601 | const vm = new NodeVM(); 602 | const script = new VMScript('module.exports = Math.random()'); 603 | const val1 = vm.run(script); 604 | const val2 = vm.run(script); 605 | assert.ok('number' === typeof val1 && 'number' === typeof val2); 606 | assert.ok( val1 != val2); 607 | }); 608 | it('VMScript options', () => { 609 | const vm = new NodeVM(); 610 | // V8 Stack Trace API: https://v8.dev/docs/stack-trace-api 611 | const code = `module.exports = getStack(new Error()); 612 | function customPrepareStackTrace(error, structuredStackTrace) { 613 | return { 614 | fileName: structuredStackTrace[0].getFileName(), 615 | lineNumber: structuredStackTrace[0].getLineNumber(), 616 | columnNumber: structuredStackTrace[0].getColumnNumber() 617 | }; 618 | }; 619 | function getStack(error) { 620 | var original = Error.prepareStackTrace; 621 | Error.prepareStackTrace = customPrepareStackTrace; 622 | Error.captureStackTrace(error, getStack); 623 | var stack = error.stack; 624 | Error.prepareStackTrace = original; 625 | return stack; 626 | }`; 627 | const script = new VMScript(code, 'test.js', { 628 | lineOffset: 10, 629 | columnOffset: 20 630 | }); 631 | const stack = vm.run(script); 632 | assert.strictEqual(stack.fileName, 'test.js'); 633 | // line number start with 1 634 | assert.strictEqual(stack.lineNumber, 10 + 1); 635 | // column number start with 0 636 | // columnNumber was move just a tad to the right. 637 | // because, vmScript wrap the code for commonjs 638 | // Note: columnNumber option affect only the first line of the script 639 | // https://github.com/nodejs/node/issues/26780 640 | assert.ok(stack.columnNumber > (code.indexOf('new Error') + 20)); 641 | 642 | }); 643 | }); 644 | 645 | describe('resolver', () => { 646 | it('use resolver', () => { 647 | const resolver = makeResolverFromLegacyOptions({ 648 | external: true 649 | }); 650 | const vm = new NodeVM({ 651 | require: resolver 652 | }); 653 | 654 | vm.run("require('mocha')", __filename); 655 | }); 656 | }); 657 | 658 | describe('source extensions', () => { 659 | it('does not find a TS module with the default settings', () => { 660 | const vm = new NodeVM({ 661 | require: { 662 | external: true 663 | } 664 | }); 665 | assert.throws(() => { 666 | vm.run("require('./data/custom_extension')", __filename); 667 | }); 668 | }); 669 | 670 | it('finds a TS module with source extensions set', () => { 671 | const vm = new NodeVM({ 672 | require: { 673 | external: true 674 | }, 675 | sourceExtensions: ['ts', 'js'] 676 | }); 677 | 678 | vm.run("require('./data/custom_extension')", __filename); 679 | }); 680 | }); 681 | --------------------------------------------------------------------------------