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.
- package/README.md +164 -31
- package/package.json +1 -1
- package/src/compile.js +63 -4
package/README.md
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
<h3 align="center">
|
|
2
|
-
<img
|
|
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-
|
|
5
|
-
<a target="_blank" rel="noopener noreferrer nofollow"><img
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
- Auto install
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
##
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
32
|
-
const
|
|
55
|
+
/* interact with the isolated-function */
|
|
56
|
+
const [value, profiling] = await sum(3, 2)
|
|
57
|
+
console.log({ value, profiling })
|
|
33
58
|
|
|
34
|
-
|
|
59
|
+
/* close resources associated with the isolated-function initialization */
|
|
60
|
+
await teardown()
|
|
35
61
|
```
|
|
36
62
|
|
|
37
|
-
|
|
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
|
|
41
|
-
const
|
|
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
|
-
|
|
154
|
+
or by execution duration:
|
|
50
155
|
|
|
51
|
-
|
|
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(
|
|
169
|
+
### isolatedFunction(code, [options])
|
|
56
170
|
|
|
57
|
-
####
|
|
171
|
+
#### code
|
|
58
172
|
|
|
59
|
-
|
|
60
|
-
Type: `
|
|
173
|
+
_Required_<br>
|
|
174
|
+
Type: `function`
|
|
61
175
|
|
|
62
|
-
The
|
|
176
|
+
The hosted function to run.
|
|
63
177
|
|
|
64
178
|
#### options
|
|
65
179
|
|
|
66
|
-
#####
|
|
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: `
|
|
69
|
-
Default: `false`
|
|
202
|
+
Type: `function`
|
|
70
203
|
|
|
71
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
const dependencies = detectDependencies(
|
|
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
|