-
Notifications
You must be signed in to change notification settings - Fork 73
/
README.md
385 lines (275 loc) · 12.8 KB
/
README.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
Jailed — flexible JS sandbox
============================
Jailed is a small JavaScript library for running untrusted code in a
sandbox. The library is written in vanilla-js and has no dependencies.
With Jailed you can:
- Load an untrusted code into a secure sandbox;
- Export a set of external functions into the sandbox.
The untrusted code may then interract with the main application by
directly calling those functions, but the application owner decides
which functions to export, and therefore what will be allowed for the
untrusted code to perform.
The code is executed as a *plugin*, a special instance running as a
restricted subprocess (in Node.js), or in a web-worker inside a
sandboxed frame (in case of web-browser environment). The iframe is
created locally, so that you don't need to host it on a separate
(sub)domain.
You can use Jailed to:
- Setup a safe environment for executing untrusted code, without a
need to create a sandboxed worker / subprocess manually;
- Do that in an isomorphic way: the syntax is same both for Node.js
and web-browser, the code works unchanged;
- Execute a code from a string or from a file;
- Initiate and interrupt the execution anytime;
- *[Demo](http://asvd.github.io/jailed/demos/web/console/)* safely
execute user-submitted code;
- *[Demo](http://asvd.github.io/jailed/demos/web/banner/)* embed
3rd-party code and provide it the precise set of functions to
harmlessly operate on the part of your application;
- Export the particular set of application functions into the sandbox
(or in the opposite direction), and let those functions be invoked
from the other site (without a need for manual messaging) thus
building any custom API and set of permissions.
For instance:
```js
var path = 'http://path.to/the/plugin.js';
// exported methods, will be available to the plugin
var api = {
alert: alert
};
var plugin = new jailed.Plugin(path, api);
```
*plugin.js:*
```js
// runs in a sandboxed worker, cannot access the main application,
// with except for the explicitly exported alert() method
// exported methods are stored in the application.remote object
application.remote.alert('Hello from the plugin!');
```
*(exporting the `alert()` method is not that good idea actually)*
Under the hood, an application may only communicate to a plugin
(sandboxed worker / jailed subprocess) through a messaging mechanism,
which is reused by Jailed in order to simulate the exporting of
particular functions. Each exported function is duplicated on the
opposite site with a special wrapper method with the same name. Upon
the wrapper method is called, arguments are serialized, and the
corresponding message is sent, which leads to the actual function
invocation on the other site. If the executed function then issues a
callback, the responce message will be sent back and handled by the
opposite site, which will, in turn, execute the actual callback
previously stored upon the initial wrapper method invocation. A
callback is in fact a short-term exported function and behaves in the
same way, particularly it may invoke a newer callback in reply.
### Installation
For the web-browser environment — download and unpack the
[distribution](https://github.com/asvd/jailed/releases/download/v0.3.1/jailed-0.3.1.tar.gz), or install it using [Bower](http://bower.io/):
```sh
$ bower install jailed
```
Load the `jailed.js` in a preferrable way. That is an UMD module, thus
for instance it may simply be loaded as a plain JavaScript file using
the `<script>` tag:
```html
<script src="jailed/jailed.js"></script>
```
For Node.js — install Jailed with npm:
```sh
$ npm install jailed
```
and then in your code:
```js
var jailed = require('jailed');
```
Optionally you may load the script from the
[distribution](https://github.com/asvd/jailed/releases/download/v0.3.1/jailed-0.3.1.tar.gz):
```js
var jailed = require('path/to/jailed.js');
```
After the module is loaded, the two plugin constructors are available:
`jailed.Plugin` and `jailed.DynamicPlugin`.
### Usage
The messaging mechanism reused beyond the remote method invocation
introduces some natural limitations for the exported functions and
their usage (nevertheless the most common use-cases are still
straightforward):
- Exported function arguments may only be either simple objects (which
are then serialized and sent within a message), or callbacks (which
are preserved and replaced with special identifiers before
sending). Custom object instance may not be used as an argument.
- A callback can not be executed several times, it will be destroyed
upon the first invocation.
- If several callbacks are provided, only one of them may be called.
- Returned value of an exported function is ignored, result should be
provided to a callback instead.
*In Node.js the
[send()](http://nodejs.org/api/child_process.html#child_process_child_send_message_sendhandle)
method of a child process object is used for transfering messages,
which serializes an object into a JSON-string. In a web-browser
environment, the messages are transfered via
[postMessage()](https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage)
method, which implements [the structured clone
algorithm](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm)
for the serialization. That algorithm is more capable than JSON (for
instance, in a web-browser you may send a RegExp object, which is not
possible in Node.js). [More details about structured clone algorithm
and its comparsion to
JSON](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm).*
A plugin object may be created either from a string containing a
source code to be executed, or with a path to the script. To load a
plugin code from a file, create the plugin using `jailed.Plugin`
constructor and provide the path:
```js
var path = 'http://path.to/some/plugin.js';
// set of methods to be exported into the plugin
var api = {
alert: alert
}
var plugin = new jailed.Plugin(path, api);
```
*plugin.js:*
```js
application.remote.alert('Hello from the plugin!');
```
Creating a plugin from a string containing a code is very similar,
this is performed using `jailed.DynamicPlugin` constructor:
```js
var code = "application.remote.alert('Hello from the plugin!');";
var api = {
alert: alert
}
var plugin = new jailed.DynamicPlugin(code, api);
```
The second `api` argument provided to the `jailed.Plugin` and
`jailed.DynamicPlugin` constructors is an interface object with a set
of functions to be exported into the plugin. It is also possible to
export functions in the opposite direction — from a plugin to the main
application. It may be used for instance if a plugin provides a method
to perform a calculation. In this case the second argument of a plugin
constructor may be omitted. To export some plugin functions, use
`application.setInterface()` method in the plugin code:
```js
// create a plugin
var path = "http://path.to/some/plugin.js";
var plugin = new jailed.Plugin(path);
// called after the plugin is loaded
var start = function() {
// exported method is available at this point
plugin.remote.square(2, reportResult);
}
var reportResult = function(result) {
window.alert("Result is: " + result);
}
// execute start() upon the plugin is loaded
plugin.whenConnected(start);
```
*plugin.js:*
```js
// provides the method to square a number
var api = {
square: function(num, cb) {
// result reported to the callback
cb(num*num);
}
}
// exports the api to the application environment
application.setInterface(api);
```
In this example the `whenConnected()` plugin method is used at the
application site: that method subscribes the given function to the
plugin connection event, after which the functions exported by the
plugin become accessible at the `remote` property of a plugin.
The `whenConnected()` method may be used as many times as needed and
thus subscribe several handlers for a single connection event. For
additional convenience, it is also possible to set a connection
handler even after the plugin has already been connected — in this
case the handler is issued immediately (yet asynchronously).
When a plugin code is executed, a set of functions exported by the
application is already prepared. But if one of those functions is
invoked, it will actually be called on the application site. If in
this case the code of that function will try to use a function
exported by the plugin, it may not be prepared yet. To solve this, the
similar `application.whenConnected()` method is available on the
plugin site. The method works same as the one of the plugin object:
the subscribed handler function will be executed after the connection
is initialized, and a set of functions exported by each site is
available on the opposite site.
Therefore:
- If you need to load a plugin and supply it with a set of exported
functions, simply provide those functions into the plugin
constructor, and then access those at `application.remote` property
on the plugin site — the exported functions are already prepared
when the plugin code is exectued.
- If you need to load a plugin and use the functions it provides
through exporting, set up a handler using `plugin.whenConnected()`
method on the application site. After the event is fired, the
functions exported by the plugin are available at its `remote`
property of the plugin object;.
- If both application and a plugin use the exported functions of each
other, *and* the communication is initiated by the plugin, you will
most likely need to use the `application.whenConnected()` method on
the plugin site before initiating the communication, in order to
make sure that the functions exported by the plugin are already
available to the application.
To disconnect a plugin, use the `disconnect()` method: it kills a
worker / subprocess immediately without any chance for its code to
react.
A plugin may also disconnect itself by calling the
`application.disconnect()` method.
In addition to `whenConnected()` method, the plugin object also
provides similar `whenFailed()` and `whenDisconnected()` methods:
- `whenFailed()` subscribes a handler function to the connection
failure event, which happens if there have been some error during
the plugin initialization, like a network problem or a syntax error
in the plugin initialization code.
- `whenDisconnected()` subscribes a function to the disconnect event,
which happens if a plugin was disconnected by calling the
`disconnect()` method, or a plugin has disconnected itself by
calling `application.disconnect()`, or if a plugin failed to
initialize (along with the failure event mentioned above). After the
event is fired, the plugin is not usable anymore.
Just like as for `whenConnected()` method, those two methods may also
be used several times or even after the event has actually been fired.
### Compatibility
Jailed was tested and should work in Node.js, and in the following
browsers:
- Internet Explorer 10+, Edge
- Firefox 26+
- Opera 12+
- Safari 6+
- Chrome 10+
### Security
This is how the sandbox is built:
##### In a web-browser:
- a [sandboxed
iframe](http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/)
is created with its `sandbox` attribute only set to `"allow-scripts"`
(to prevent the content of the frame from accessing anything of the
main application origin);
- then a web-worker is started inside that frame;
- finally the code is loaded by the worker and executed.
*Note: when Jailed library is loaded from the local source (its path
starts with `file://`), the `"allow-same-origin"` permission is added
to the `sandbox` attribute of the iframe. Local installations are
mostly used for testing, and without that permission it would not be
possible to load the plugin code from a local file. This means that
the plugin code has an access to the local filesystem, and to some
origin-shared things like IndexedDB (though the main application page
is still not accessible from the worker). Therefore if you need to
safely execute untrusted code on a local system, reuse the Jailed
library in Node.js.*
##### In Node.js:
*Warning: according to recent reports
([#33](https://github.com/asvd/jailed/issues/33)) this way of
sandboxing is not secure any longer, the fix is being prepared...*
- A Node.js subprocess is created by the Jailed library;
- the subprocess (down)loads the file containing an untrusted code as
a string (or, in case of `DynamicPlugin`, simply uses the provided
string with code)
- then `"use strict";` is appended to the head of that code (in order
to prevent breaking the sandbox using `arguments.callee.caller`);
- finally the code is executed using `vm.runInNewContext()` method,
where the provided sandbox only exposes some basic methods like
`setTimeout()`, and the `application` object for messaging with the
application site.
--
follow me on twitter: https://twitter.com/asvd0