isolated-function 0.0.6 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +164 -31
  2. package/package.json +1 -1
  3. package/src/compile.js +63 -4
package/README.md CHANGED
@@ -1,18 +1,37 @@
1
1
  <h3 align="center">
2
- <img src="https://github.com/Kikobeats/isolated-function/blob/master/logo.png?raw=true" width="200">
2
+ <img
3
+ src="https://github.com/Kikobeats/isolated-function/blob/master/logo.png?raw=true"
4
+ width="200">
3
5
  <br>
4
- <p>isolated-functions</p>
5
- <a target="_blank" rel="noopener noreferrer nofollow"><img src="https://img.shields.io/github/tag/Kikobeats/isolated-function.svg?style=flat-square" style="max-width: 100%;"></a>
6
- <a href="https://coveralls.io/github/Kikobeats/isolated-function" rel="nofollow"><img src="https://img.shields.io/coveralls/Kikobeats/isolated-function.svg?style=flat-square" alt="Coverage Status" style="max-width: 100%;"></a>
7
- <a href="https://www.npmjs.org/package/isolated-function" rel="nofollow"><img src="https://img.shields.io/npm/dm/isolated-function.svg?style=flat-square" alt="NPM Status" style="max-width: 100%;"></a>
6
+ <p>isolated-function</p>
7
+ <a target="_blank" rel="noopener noreferrer nofollow"><img
8
+ src="https://img.shields.io/github/tag/Kikobeats/isolated-function.svg?style=flat-square"
9
+ style="max-width: 100%;"></a>
10
+ <a href="https://coveralls.io/github/Kikobeats/isolated-function"
11
+ rel="nofollow"><img
12
+ src="https://img.shields.io/coveralls/Kikobeats/isolated-function.svg?style=flat-square"
13
+ alt="Coverage Status" style="max-width: 100%;"></a>
14
+ <a href="https://www.npmjs.org/package/isolated-function" rel="nofollow"><img
15
+ src="https://img.shields.io/npm/dm/isolated-function.svg?style=flat-square"
16
+ alt="NPM Status" style="max-width: 100%;"></a>
8
17
  </h3>
9
18
 
10
- ## Highlights
11
-
12
- - Based in [Node.js Permission Model](https://nodejs.org/api/permissions.html#permission-model)
13
- - Auto install npm dependencies.
14
- - Memory limit support.
15
- - Timeout support.
19
+ - [Install](#install)
20
+ - [Quickstart](#quickstart)
21
+ - [Minimal privilege execution](#minimal-privilege-execution)
22
+ - [Auto install dependencies](#auto-install-dependencies)
23
+ - [Execution profiling](#execution-profiling)
24
+ - [Resource limits](#resource-limits)
25
+ - [API](#api)
26
+ - [isolatedFunction(code, \[options\])](#isolatedfunctioncode-options)
27
+ - [code](#code)
28
+ - [options](#options)
29
+ - [timeout](#timeout)
30
+ - [timeout](#timeout-1)
31
+ - [=\> (fn(\[...args\]), teardown())](#-fnargs-teardown)
32
+ - [fn](#fn)
33
+ - [teardown](#teardown)
34
+ - [License](#license)
16
35
 
17
36
  ## Install
18
37
 
@@ -20,55 +39,169 @@
20
39
  npm install isolated-function --save
21
40
  ```
22
41
 
23
- ## Usage
42
+ ## Quickstart
43
+
44
+ **isolated-function** is a modern solution for running untrusted code in Node.js.
24
45
 
25
46
  ```js
26
47
  const isolatedFunction = require('isolated-function')
27
48
 
28
- /* This function will run in a sandbox, in a separate process */
29
- const sum = isolatedFunction((y, z) => y + z)
49
+ /* create an isolated-function, with resources limitation */
50
+ const [sum, teardown] = isolatedFunction((y, z) => y + z, {
51
+ memory: 128, // in MB
52
+ timeout: 10000 // in milliseconds
53
+ })
30
54
 
31
- /* Interact with it as usual from your main code */
32
- const result = await sum(3, 2)
55
+ /* interact with the isolated-function */
56
+ const [value, profiling] = await sum(3, 2)
57
+ console.log({ value, profiling })
33
58
 
34
- console.log(result)
59
+ /* close resources associated with the isolated-function initialization */
60
+ await teardown()
35
61
  ```
36
62
 
37
- You can also use `require' for external dependencies:
63
+ ### Minimal privilege execution
64
+
65
+ The hosted code runs in a separate process, with minimal privilege, using [Node.js permission model API](https://nodejs.org/api/permissions.html#permission-model).
38
66
 
39
67
  ```js
40
- const isEmoji = isolatedFunction(emoji => {
41
- const isEmoji = require('is-standard-emoji')
68
+ const [fn, teardown] = isolatedFunction(() => {
69
+ const fs = require('fs')
70
+ fs.writeFileSync('/etc/passwd', 'foo')
71
+ })
72
+
73
+ await fn()
74
+ // => PermissionError: Access to 'FileSystemWrite' has been restricted.
75
+ ```
76
+
77
+ If you exceed your limit, an error will occur. Any of the following interaction will throw an error:
78
+
79
+ - Native modules
80
+ - Child process
81
+ - Worker Threads
82
+ - Inspector protocol
83
+ - File system access
84
+ - WASI
85
+
86
+ ### Auto install dependencies
87
+
88
+ The hosted code is parsed for detecting `require`/`import` calls and install these dependencies:
89
+
90
+ ```js
91
+ const [isEmoji, teardown] = isolatedFunction(emoji => {
92
+ /* this dependency only exists inside the isolated function */
93
+ const isEmoji = require('is-standard-emoji@1.0.0') // default is latest
42
94
  return isEmoji(emoji)
43
95
  })
44
96
 
45
97
  await isEmoji('🙌') // => true
46
98
  await isEmoji('foo') // => false
99
+ await teardown()
100
+ ```
101
+
102
+ The dependencies, along with the hosted code, are bundled by [esbuild](https://esbuild.github.io/) into a single file that will be evaluated at runtime.
103
+
104
+ ### Execution profiling
105
+
106
+ Any hosted code execution will be run in their own separate process:
107
+
108
+ ```js
109
+ /** make a function to consume ~128MB */
110
+ const [fn, teardown] = isolatedFunction(() => {
111
+ const storage = []
112
+ const oneMegabyte = 1024 * 1024
113
+ while (storage.length < 78) {
114
+ const array = new Uint8Array(oneMegabyte)
115
+ for (let ii = 0; ii < oneMegabyte; ii += 4096) {
116
+ array[ii] = 1
117
+ }
118
+ storage.push(array)
119
+ }
120
+ })
121
+ t.teardown(cleanup)
122
+
123
+ const [value, profiling] = await fn()
124
+ console.log(profiling)
125
+ // {
126
+ // memory: 128204800,
127
+ // duration: 54.98325
128
+ // }
129
+ ```
130
+
131
+ Each execution has a profile, which helps understand what happened.
132
+
133
+ ### Resource limits
134
+
135
+ You can limit a **isolated-function** by memory:
136
+
137
+ ```js
138
+ const [fn, teardown] = isolatedFunction(() => {
139
+ const storage = []
140
+ const oneMegabyte = 1024 * 1024
141
+ while (storage.length < 78) {
142
+ const array = new Uint8Array(oneMegabyte)
143
+ for (let ii = 0; ii < oneMegabyte; ii += 4096) {
144
+ array[ii] = 1
145
+ }
146
+ storage.push(array)
147
+ }
148
+ }, { memory: 64 })
149
+
150
+ await fn()
151
+ // => MemoryError: Out of memory
47
152
  ```
48
153
 
49
- The dependencies are bundled with the source code into a single file that is executed in the sandbox.
154
+ or by execution duration:
50
155
 
51
- It's intentionally not possible to expose any Node.js objects or functions directly to the sandbox (such as `process`, or filesystem). This makes it slightly harder to integrate into a project, but has the benefit of guaranteed isolation.
156
+ ```js
157
+ const [fn, teardown] = isolatedFunction(() => {
158
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
159
+ await delay(duration)
160
+ return 'done'
161
+ }, { timeout: 50 })
162
+
163
+ await fn(100)
164
+ // => TimeoutError: Execution timed out
165
+ ```
52
166
 
53
167
  ## API
54
168
 
55
- ### isolatedFunction(snippet, [options])
169
+ ### isolatedFunction(code, [options])
56
170
 
57
- #### snippet
171
+ #### code
58
172
 
59
- *Required*<br>
60
- Type: `string`
173
+ _Required_<br>
174
+ Type: `function`
61
175
 
62
- The source code to run.
176
+ The hosted function to run.
63
177
 
64
178
  #### options
65
179
 
66
- ##### foo
180
+ ##### timeout
181
+
182
+ Type: `number`
183
+
184
+ Timeout after a specified amount of time, in milliseconds.
185
+
186
+ ##### timeout
187
+
188
+ Type: `number`
189
+
190
+ Set the functino memory limit, in megabytes.
191
+
192
+ ### => (fn([...args]), teardown())
193
+
194
+ #### fn
195
+
196
+ Type: `function`
197
+
198
+ The isolated function to execute. You can pass arguments over it.
199
+
200
+ #### teardown
67
201
 
68
- Type: `boolean`<br>
69
- Default: `false`
202
+ Type: `function`
70
203
 
71
- Lorem ipsum.
204
+ A function to be called to release resources associated with the **isolated-function**.
72
205
 
73
206
  ## License
74
207
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "isolated-function",
3
3
  "description": "Runs untrusted code in a Node.js v8 sandbox.",
4
4
  "homepage": "https://github.com/Kikobeats/isolated-function",
5
- "version": "0.0.6",
5
+ "version": "0.1.1",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js"
package/src/compile.js CHANGED
@@ -37,17 +37,74 @@ const detectDependencies = code => {
37
37
  node.arguments.length === 1 &&
38
38
  node.arguments[0].type === 'Literal'
39
39
  ) {
40
- dependencies.add(node.arguments[0].value)
40
+ const dependency = node.arguments[0].value
41
+ // Check if the dependency string contains '@' symbol for versioning
42
+ if (dependency.includes('@')) {
43
+ // Split by '@' to separate module name and version
44
+ const parts = dependency.split('@')
45
+ // Handle edge case where module name might also contain '@' (scoped packages)
46
+ const moduleName = parts.length > 2 ? `${parts[0]}@${parts[1]}` : parts[0]
47
+ const version = parts[parts.length - 1]
48
+ dependencies.add(`${moduleName}@${version}`)
49
+ } else {
50
+ dependencies.add(`${dependency}@latest`)
51
+ }
41
52
  }
42
53
  },
43
54
  ImportDeclaration (node) {
44
- dependencies.add(node.source.value)
55
+ const source = node.source.value
56
+ if (source.includes('@')) {
57
+ const parts = source.split('@')
58
+ const moduleName = parts.length > 2 ? `${parts[0]}@${parts[1]}` : parts[0]
59
+ const version = parts[parts.length - 1]
60
+ dependencies.add(`${moduleName}@${version}`)
61
+ } else {
62
+ dependencies.add(`${source}@latest`)
63
+ }
45
64
  }
46
65
  })
47
66
 
48
67
  return Array.from(dependencies)
49
68
  }
50
69
 
70
+ const transformDependencies = code => {
71
+ const ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module' })
72
+
73
+ let newCode = ''
74
+ let lastIndex = 0
75
+
76
+ // Helper function to process and transform nodes
77
+ const processNode = node => {
78
+ if (node.type === 'Literal' && node.value.includes('@')) {
79
+ // Extract module name without version
80
+ const [moduleName] = node.value.split('@')
81
+ // Append code before this node
82
+ newCode += code.substring(lastIndex, node.start)
83
+ // Append transformed dependency
84
+ newCode += `'${moduleName}'`
85
+ // Update lastIndex to end of current node
86
+ lastIndex = node.end
87
+ }
88
+ }
89
+
90
+ // Traverse the AST to find require and import declarations
91
+ walk.simple(ast, {
92
+ CallExpression (node) {
93
+ if (node.callee.name === 'require' && node.arguments.length === 1) {
94
+ processNode(node.arguments[0])
95
+ }
96
+ },
97
+ ImportDeclaration (node) {
98
+ processNode(node.source)
99
+ }
100
+ })
101
+
102
+ // Append remaining code after last modified dependency
103
+ newCode += code.substring(lastIndex)
104
+
105
+ return newCode
106
+ }
107
+
51
108
  const getTmp = async content => {
52
109
  const cwd = await fs.mkdtemp(path.join(tmpdir(), 'compile-'))
53
110
  await fs.mkdir(cwd, { recursive: true })
@@ -60,8 +117,10 @@ const getTmp = async content => {
60
117
  }
61
118
 
62
119
  module.exports = async snippet => {
63
- const tmp = await getTmp(generateTemplate(snippet))
64
- const dependencies = detectDependencies(tmp.content)
120
+ const compiledTemplate = generateTemplate(snippet)
121
+ const dependencies = detectDependencies(compiledTemplate)
122
+ const tmp = await getTmp(transformDependencies(compiledTemplate))
123
+
65
124
  await $(packageManager.init, { cwd: tmp.cwd })
66
125
  await $(`${packageManager.install} ${dependencies.join(' ')}`, {
67
126
  cwd: tmp.cwd