├── .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] [](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 |
--------------------------------------------------------------------------------