node-sandbox is a way of running untrusted code outside of your application's node process. You can interface with code running in the sandbox via RPC (or any library that works over the node
Note that at the moment, the only protection is that the code is run in a separate process, and that the process is prevented from loading certain C bindings used by core modules. In the future, there will be container plugins available to block off functionality to the process itself (eg FS access, ability to open sockets, etc.) Containment will be done on the OS-level to reduce the attack surface of the sandbox.
Note that the method of containment used by the sandbox is far from bulletproof, and there is a very real possibility that malicious code could break out of the sandbox. In the future we will be working on improving it, but it will never be perfect.
This library is Licensed under the Academic Free License version 2.1
Using node-sandbox is pretty straightforward. The source code is fairly well documented, and there a good number of test cases, so if you have any questions, feel free to dive in!
Note: by default, the code being run won't have access to
require() or anything. See "Specifying Permissions" for more info.
//create a new sandbox instance w/ default optionsvar sb = "./path/to/code.js";//expose a method for the sandbox to callsbrpc;//run the sandboxsb;//Wait for the sandbox to initialize.//We can't call methods until the sandbox is ready,//otherwise we'll get an error!sb;
There are some basic options you should know about for fringe cases.
If your node command isn't in your
PATH, you need to specify it manually. By default, it just uses
If for some reason
Sandbox isn't detecting the path to
shovel.js, you can specify the path manually (it's in
var sb = "path/to/code.js"//the node command used to spawn the child processnode_command: "node"//the path to the shovel (what bootstraps the child process)shovel: path;
Other options are discussed in the relevant sections!
Specifying permissions is somewhat complicated. In node, there's a function called
process.binding() that's used by node's built-in modules to get the relevant C bindings for things like I/O, crypto, and others.
process.binding() works like
require() does; you pass a module as an argument, and it fetches the relevant C binding.
node-sandbox's permissions system will block off loading of the C bindings. To allow loading of them, we need to pass a
permissions option to it, specifying which C bindings are allowed to be loaded.
The following are the permissions specified by default, and are needed for the sandbox to run.
var sb = "./path/to/code.js"permissions: "tty_wrap" "pipe_wrap";
If you want to allow use of specific modules (eg
crypto), look at their file in node's
lib/ directory, and see what bindings they load by searching for
process.binding. Then pass any modules needed through
Note that if you want access to
require(), you'll need to pass the following to permissions:
var sb = "./path/to/code.js"permissions: "tty_wrap" "pipe_wrap" //TODO: figure these out;
Exposing RPC Methods
node-sandbox comes with a stock RPC library that uses JSON-RPC in a bi-directional way, so that both ends can expose methods for the other to call. We can access it through
Exposing methods in the main process is easy:
var sb = /* options etc */;sb;//we can expose individual methods, eg:sbrpc;//we can create namespaces by including a '.'sbrpc;//we can unexpose methods like this!sbrpc;//alternatively, we can opt to expose an entire object.//This deletes anything that was already exposed previously.sbrpc;//unexposing still works the same way when//using exposeObjectsbrpc;
If you need to work with asynchronous libraries in the methods you expose, you can return a
Promise object instead. You can use any
Promise library you like, but I use Kris Zyp's node-promise.
Exposing methods within the sandbox works the same way, except we use the global
rpc variable instead of
Sandbox.rpc. Inside the sandbox,
Promise is accessible globally for convenience.
Calling RPC Methods
To call methods, you can use
Sandbox.rpc.notify. Both methods will call the remote method, but
call will give you a return value, while
notify won't (see the JSON-RPC docs if this is confusing).
It's important to note that you can't call methods until after
Sandbox has emitted a
ready event! See the "Basic Usage" code snippet for an example.
call will return a
Promise object to give you the result asynchronously. See the docs for Kris Zyp's node-promise for all available methods.
//this will call the method like so: myMethod(1, 2, 3);sbrpc;//if we pass call() an object as arguments, it'll call the method like this: myMethod(myObj);var myObj = foo: "bar";sbrpc;//here's an example using notify(). It's arguments are identical to call()sbrpc; //no returned value
Again, the API is identical from within the sandbox. Just use the
rpc global variable instead of
RPC Call Timeouts
Sometimes, we might want to specify a timeout for method calls, just in case. We can do this one of two ways.
The first is to use the
var p = rpc;p; //timeout after 10 secondsp;
The second is to add an additional
call_timeout option, eg:
var sb = "path/to/code.js"call_timeout: 10000 //10 seconds;
-1, which disables timeouts to method calls. The value specified in
call_timeout will also be applied to the
rpc class inside the sandbox.
Detecting when the sandbox exits
We can detect when the sandbox exits using the
Lockup detection & killing the sandbox
node-sandbox has built in lockup detection, so if a stray
while() loop locks up the sandbox, we can react to it.
To kill the sandbox, we can use
var sb = "path/to/code.js"permissions: /*...*///Here are some relevant options for lockup detection.//All time is specified in milliseconds.//Set any of these values to -1 to disable them.//how long we should wait for a reply//before emitting a 'lockup' event. (default: 10 seconds)lockup_timeout: 10000//how long we should wait after killing the process//to kill -9 it. (default: 10 seconds)kill_with_fire_timeout: 10000//how frequently we should check the sandbox (default: 10 seconds)ping_interval: 10000//how long we should wait before assuming//the sandbox failed to start (locked up immediately)//(default: 10 seconds)startup_timeout: 10000;sb;sb;
Detecting output on STDERR
If something ever goes wrong within the sandbox, by default it doesn't get printed to the main process'
STDOUT. Instead, you need to listen on the
stderr event and do it yourself, eg:
You can also pass this on to any logging library you use.
Pinging the Sandbox
If you want to ping the sandbox to figure out latency, you can use
Sandbox.ping(), which returns a
node-sandbox has a full featured plugin system, and a lot of it's features are provided by built-in plugins. Built-in plugins can be found in
lib/plugins/, and include the following:
_base: not meant to be loaded, but provides base functionality to other plugins
rpc: provides JSON-RPC functionality (exposed through
Sandbox.rpc) over the
Streambetween the parent and child processes exposed by
lockup_detection: provides lockup detection functionality, including the
on("lockup")event. Relies on
process.bindingso that any unauthorized modules aren't allowed to be loaded.
Eventually I'd like to include a plugin that can set up a secure container for the child process using OS features (eg SELinux). If you know a lot about this sort of thing and would like to contribute, please let me know!
Plugins can be specified using the
var sb = "path/to/file.js"plugins://Note: these are the default plugins that are loaded. If you want to load an extra plugin, you should include these built-in ones too!"rpc" "lockup_detection" "wrapper";
Note that plugin hooks are executed in the order that they're provided in the array, so make sure "wrapper" goes last, otherwise a plugin might not have access to the resources it needs to initiate itself!
Some plugins in the future may take additional arguments, but all the built-in ones at the time of writing read from the main arguments passed to the sandbox (for the sake of ease-of-use). Here's an example on how to pass custom arguments to a plugin:
var sb = "path/to/file.js"plugins:name: "my_plugin"options: foo: "bar""some_other_plugin";
To load an external plugin, simply pass a path to the directory containing all the plugin's files (
manifest.js, etc.) instead of a plugin name. It can either be an absolute path, or relative to the
var sb = "path/to/file.js"plugins:name: "/path/to/my_plugin"options: foo: "bar"//OR"/path/to/my_plugin";
Writing Custom Plugins
The plugin system lets you hook into the sandbox and add any functionality you want. Things you can do include:
- Extend the sandbox by adding custom options and methods, and pretty much override/wrap any function/variable you want
- Run code during certain events in the parent process (eg when the child process exits, when the process writes to stderr, etc.)
- Run code inside the child process (eg when the process spawns, after the code is loaded, after the code is executed, etc.)
- Pass extra arguments to the child process via
Using this, you can write your own RPC plugin, container plugin, or anything else you need. I strongly suggest looking in the
lib/plugins directory for examples, especially at the
_base plugin, which documents when each hook is called.
Plugins consist of three files:
plugin_name/manifest.js: a manifest file that provides information about the plugin.
plugin_name/ParentHooks.js: a class that provides hooks for the parent process and allows you to hook into each
plugin_name/ShovelHooks.js: a class that provides hooks for
shovel.js, which is what's run to create our child process.
ShovelHooks should both extend the respective classes in the
manifest.js can simply be copied to your plugin's directory.
PluginManager class loads the plugins, and does some basic dependency/conflict checks. Below is the
manifest.js file from the
moduleexports =name: "_base" //the name of the parent directory our plugin is inprovides: //features that it provides. This is flexible, so it can be something like "container" or "rpc".conflicts: //features or specific plugins this plugin conflicts with.depends: //features or specific plugins that this plugin requires to run.
To put this into practice, lets say we want to write a replacement for the RPC plugin. If we put
"rpc" in the
provides array, the plugin manager will throw an error if any other plugins that provide the
"rpc" functionality are loaded. This way two plugins won't fight over access to
If we don't provide RPC functionality, but for one reason or the other we conflict with the RPC module (maybe our plugin wants to use the
Stream between parent and child processes exclusively), we can put it in the
conflicts array instead.
If our plugin depended on the RPC class in order to pass data between the parent and child processes, we could put
"rpc" in the
depends array. Note that any plugin that provides the
"rpc" functionality would satisfy this requirement. For this reason, it's strongly suggested that any module that provides a specified functionality should have an identical base API (extra functions are allowed to be implemented).
To implement subclasses of
ShovelHooks, take a look inside of the respective class definitions in the
_base class. Everything is well documented in there, and will explain how to access things through member variables, and which methods get called when.