runspace
v1.1.0
Published
Sandbox for running untrusted code with full-fledged module loading mechanism.
Downloads
3
Readme
Runspace
Sandbox for running untrusted code with full-fledged module loading mechanism.
Installation
npm install runspace
Usage
Runspace(path, [options])
Creates a sandbox rooted at the given path.
Files and modules outside the given path are normally denied for access. Additional controls to native and user modules can also be defined by creating proxies.
See Proxy and Sandbox section for more details.
var Runspace = require('runspace');
var runspace = new Runspace('./sandbox');
// all legitimate Node.js codes can be run smoothly
// inside the created sandbox without acknowledging it
runspace.run(' \
var fs = require("fs"); \
var util = require("util"); \
fs.readFile("./my.txt", function (err, data) { \
process.stdout.write(util.format(data, +new Date())); \
}); \
');
Options
Below is an exhaustive list of options, with the default value shown.
{
// list of whitelist paths where modules inside
// can be loaded by untrusted codes
loadPaths: []
}
runspace.run(code, [filename], [globals])
Runs the code in contextified sandbox.
If filename
is given, it determines the working path for resolving module locations.
var runspace = new Runspace('/parent/sandbox');
// look for:
// /parent/sandbox/subdir/dependency
// /parent/sandbox/subdir/dependency.{js,json,node}
// /parent/sandbox/subdir/dependency/index.{js,json,node}
// /parent/sandbox/subdir/node_modules/dependency
// /parent/sandbox/node_modules/dependency
// but NOT:
// /parent/node_modules/dependency
// /node_modules/dependency
// /other_global_paths/dependency
runspace.run('require("dependency")', '/parent/sandbox/subdir/hello-world.js');
// throws exception for invalid path
runspace.run('', '/outside-sandbox/example.js');
This method is identical to calling runspace.compile()
then run()
,
except that this method compiles code each time called.
// the following two lines gives identical result
runspace.run(code, filename, globals);
runspace.compile(code, filename).run(globals);
Passing additional globals
Other than built-in JavaScript and Node.js objects (see Global), additional global variables can be passed to the compiled script.
runspace.run('console.log(number)', { number: 1 }); // prints '1'
Note: They are actually not real globals but rather local to the function composed by the supplied code.
runspace.compile(code, [filename])
Compiles the code in contextified sandbox.
If filename
is given, it determines the working path for resolving module locations.
var script = runspace.compile('console.log(number)');
script.run({ number: 1 }); // prints '1'
runspace.terminate()
A runspace can be terminated by calling terminate()
.
All proxies, event listeners and timeouts are cleared. This allows GC to free resources taken up by the sandbox. Subsequent async callbacks and attempts to access proxies will throw exception.
Event: message
Triggered when process.send()
is called inside sandbox.
Event: error
Triggered when an exception is thrown and uncaught inside sandbox.
Event: terminate
Triggered when runspace.terminate()
is called.
Proxy
Proxies are wrappers on objects that allow protection and interception when those objects are accessed by untrusted code.
Important: Due to limitation in ES5, the proxies generated by this library is not intended to be a polyfill solution, with the following limitation:
- Properties are converted to get/setters on proxies to provide interception;
- Properties and methods on an object are only available on its proxy when they exist during proxy creation. Afterwards new properties and methods cannot be accessed through the proxy.
Functions and callbacks
Functions and callbacks are handled such that arguments and return values are translated from objects to their proxy counterparts and vice versa.
/* host */
function ClassA() {}
function ClassB() {}
function ClassX() {}
var objAdded = {};
var instA = new ClassA();
runspace.add(new ClassA());
runspace.add(ClassB);
runspace.add(objAdded);
var returnedInstA = script.run({
ClassA: ClassA,
ClassB: ClassB,
ClassX: ClassX,
instA: instA,
instB: new ClassB(),
instX: new ClassX(),
objNotAdded: {},
func: function (argInstA) {
// arguments from sandbox are un-proxied
argInstA === instA;
// return value will be re-proxied
return instA;
}
});
// returned value from sandbox is un-proxied
returnedInstA === instA;
/* sandbox */
// the following objects from host are proxied
ClassA, ClassB, instA, instB, objAdded;
ClassA.prototype, Object.getPrototypeOf(instB);
// the following objects from host are NOT proxied
ClassX, instX, objNotAdded;
// proxied instA is un-proxied when passed to func()
// and returned instA is re-proxied
var returnedInstA = func(instA);
returnedInstA === instA;
// proxied instA will be un-proxied when returned to host
return instA;
runspace.getProxy(target)
Gets the proxy if the target has been proxied. Otherwise undefined
is returned.
runspace.add/proxy/weakProxy(target, [options])
Objects are proxied in two flavors:
Weakly-referenced proxies are for temporal objects that lived within the life of sandbox. The references being weak allows GC to collect even though the sandbox is active.
Strongly-referenced proxies are for global and shared objects. The references being strong allows
Runspace
to clear resources when terminating.
The target's prototypes are implicitly proxied recursively, i.e. all prototype objects and constructors up the prototype chain have also their proxy counterparts.
**Differences on add/proxy/weakProxy: **
Important: Calling the proxy generating methods for the same target repeatedly returns the same proxy with its flavor (strong-/weak-referenced) unchanged.
Options
Below is an exhaustive list of options. All options are optional.
{
// when target is [Function]
// name to assign for anonymous function
name: '',
// when target is [Function]
// accepted values: 'in', 'out', 'ctor'
// specify whether the function:
// in: accepts arguments from and returns value to sandbox
// out: accepts arguments from and returns value to host
// ctor: is a constructor (prototype chain is also proxied)
// default -
// if function name starts with an Uppercased letter: 'ctor'
// otherwise: 'in'
functionType: '',
// whitelist of properties and methods allowed to access
// see notes below
allow: [],
// blacklist of properties and methods allowed to access
// see notes below
deny: [],
// list of properties which their values should be freezed; or
// true if values of all properties should be freezed
freeze: [],
// called when getting property on a proxy
// see 'Interceptors'
get: function (name, value, target, undef) { ... },
// called when setting property on a proxy
// see 'Interceptors'
set: function (name, value, target, undef) { ... },
// called when calling method on a proxy
// see 'Interceptors'
call: function (name, fn, args, target, undef) { ... },
// called when creating new instance of a proxied class
// see 'Interceptors'
new: function (name, fn, args, undef) { ... }
}
Note: To blacklist/whitelist constructor "static" and "instance" members,
follow patterns of MyConstructor.staticMember
and MyConstructor#instMember
.
If blacklist and whitelist are supplied at the same time, blacklist takes precendence.
Interceptors
Interceptors enables modifications on supplied arguments and return value.
Arguments to interceptors
Referencing argument names of interceptor options shown in above section:
name
: name of the property or method intercepted
fn
: intercepted function
args
: arguments supplied to the intercepted function
value
: value supplied to the intercepted property/setter
target
: target object proxied
undef
: when returned from interceptors, tell the proxy to return undefined
as the return value instead of
proceeding. Arbitrary return value can be wrapped by undef.wrap()
.
undef.wrap(3); // 3
undef.wrap(null); // null
undef.wrap(undefined) === undef; // true
Example: Modifying arguments
/* host */
var target = {
add: function (a, b) {
return a + b;
}
};
runspace.proxy(target, {
call: function (name, fn, args) {
if (name === 'add') {
args[0] = String(args[0]);
}
}
});
/* sandbox */
target.add(1, 2); // '12'
target.add(null, 2); // 'null2'
Example: Modifying return value
/* host */
var target = {
one: 1,
two: 2,
three: 3,
four: undefined,
five: 5
};
runspace.proxy(target, {
get: function (name, value, target, undef) {
switch (name) {
case 'one':
return value + '';
case 'two':
return undef;
case 'three':
case 'four':
return undef.wrap(function () {
return name === 'three' ? 3 : undefined;
}());
}
// if reached here, tell the proxy to proceed
/* return undefined */;
}
});
/* sandbox */
target.one; // '1'
target.two; // undefined (undefined as return value)
target.three; // 3 (undef.wrap returned as-is)
target.four; // undefined (undef.wrap wrapped undefined)
target.five; // 5 (proceed to original property/getter)
Other properties and methods
runspace.context
The contextified sandbox which untrusted code runs in. Additional globals can be declared on this object.
runspace.stdin, runspace.stdout, runspace.stderr
Readable and writable streams piped from/to process.stdin
, process.stdout
and process.stderr
that are available inside sandbox.
runspace.send(message)
Sandboxed code receives the message by process.on('message')
.
The message can be primitive values or JSON objects.
Sandbox
The following section describes behaviors of global objects and built-in modules inside sandbox.
Global
The global scope and the global
object is a contextified sandbox.
Other than standard built-in global objects, objects that are native from Node.js are also available inside sandbox. Native objects, typed arrays and buffers are NOT proxied.
EventEmitter
Even if the EventEmitter
object is shared across sandboxes, listeners are scoped
within each sandbox. That is, only listeners attached from the same sandbox
can be listed.
var ee = new EventEmitter();
var rs1 = new Runspace('./');
var rs2 = new Runspace('./');
var script1 = rs1.compile('ee.on("event", function () {}); console.log(ee.listenerCount("event"))');
var script2 = rs2.compile('ee.on("event", function () {}); console.log(ee.listenerCount("event"))');
script1.run({ ee: ee }); // prints 1
script2.run({ ee: ee }); // prints 1
script1.run({ ee: ee }); // prints 2
EventEmitter.listeners(eventType)
Returns listeners attached by the calling sandbox.
EventEmitter.listenerCount(eventType)
Returns the number of listeners attached by the calling sandbox.
EventEmitter.removeAllListeners([eventType])
Removes listeners attached by the calling sandbox.
process
The following properties and methods are blocked from access:
abort
, binding
, chdir
, dlopen
, exit
, setgid
, setegid
, setuid
, seteuid
,
setgroups
, initgroups
, kill
, disconnect
, mainModule
.
process.stdin, process.stdout, process.stderr
The three standard IO streams are piped from/to the hosting runspace.stdin
,
runspace.stdout
and runspace.stderr
writables and readables.
If there are no data
event listeners attached in the readable end of those pipes,
any data written to those streams are discarded.
process.cwd()
Returns the sandbox root path rather than actual working directory.
process.send(message)
The message is routed to runspace.on('message')
instead of that
the listening process on IPC channel.
process.on('message')
Receives message sent from runspace.send()
instead of from
the listening process on IPC channel.
process.on('exit')
The exit
event is also triggered when the parent Runspace
object is terminated.
timers
Handle returned by setTimeout
and setInterval
is unref
'd and cannot be ref
'd again.
Calling ref()
throws exception.
fs
All functions that mention a path other than file descriptor throws exception when supplied with paths outside the sandbox's scope.
fs.watch(path, [option], [callback])
File watchers created by fs.watch()
are closed when the parent Runspace
is terminated.
Persistent file watchers are disallowed.
fs.watchFile(path)
Listeners attached to fs.watchFile()
are unwatched when the parent Runspace
is terminated.
fs.unwatchFile(path, [listener])
Only listeners attached by the calling sandbox are removed if no listeners is supplied.
path
path.resolve()
resolves paths from the sandbox root rather than actual working directory.
dgram, net, tls, http, https
Sockets and servers created by these modules are unref
'd and cannot be ref
'd,
and are closed when the parent Runspace
is terminated.
child_process, cluster, repl
These built-in modules are disallowed. An EACCES
error is thrown when requiring these modules.
require
Modules are resolved and required as-is, except:
- Built-in modules are proxied
- Built-in modules and their exposed APIs can be denied
- Modules outside sandbox's root path are invisible unless explicitly allowed
- Modules are NOT shared across sandboxes, i.e. same module required by different sandboxes are not of the same instance
License
The MIT License (MIT)
Copyright (c) 2015 misonou
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.