A simple and powerful sandbox for running untrusted JavaScript.
For a project I'm working on, I needed the ability to run untrusted JavaScript code.
I had a couple specific requirements:
I could not find a library that met all these requirements, enter SandCastle.
npm install sandcastle
var SandCastle = require('sandcastle').SandCastle;
var sandcastle = new SandCastle();
var script = sandcastle.createScript("\
exports.main = function() {\
exit('Hey ' + name + ' Hello World!');\
}\
");
script.on('exit', function(err, output) {
console.log(output); // Hello World!
});
script.run({name: 'Ben'});// we can pass variables into run.
Outputs
Hey Ben Hello World!
The following options may be passed to the SandCastle constructor:
timeout — number of milliseconds to allow script to run (defaults to 5000 ms)memoryLimitMB — maximum amount of memory that a script may consume (defaults to 0)useStrictMode — boolean; when true script runs in strict mode (defaults to false)api — path to file that defines the API accessible to scriptcwd — path to the current working directory that the script will be run in (defaults to process.cwd())spawnExecPath — path to a external node binary to run the sandbox with. (defaults to process.execPath) This is a temporary workaround which allows you to run the sandbox within node-webkitrefreshTimeoutOnTask — boolean; refreshes the timeout whenever an answer to a task will be sent to the scriptA pool consists of several SandCastle child-processes, which will handle the script execution. Pool-object is a drop-in replacement of single Sandcastle instance. Only difference is, when creating the Pool-instance.
You can specify the amount of child-processes with parameter named numberOfInstances (default = 1).
var Pool = require('sandcastle').Pool;
var poolOfSandcastles = new Pool( { numberOfInstances: 3 }, { timeout: 6000 } );
var script = poolOfSandcastles.createScript("\
exports.main = function() {\
exit('Hello World!');\
}\
");
script.on('exit', function(err, output) {
console.log(output);
});
script.run();
If a script takes too long to execute, a timeout event will be fired:
var SandCastle = require('sandcastle').SandCastle;
var sandcastle = new SandCastle({ timeout: 6000 });
var script = sandcastle.createScript("\
exports.main = function() {\
while(true) {};\
}\
");
script.on('exit', function(err, output) {
console.log('this will never happen.');
});
script.on('timeout', function() {
console.log('I timed out, oh what a silly script I am!');
});
script.run();
Outputs
I timed out, oh what a silly script I am!
If an exception occurs while executing a script, it will be returned as the first parameter in an on(exit) event.
var SandCastle = require('sandcastle').SandCastle;
var sandcastle = new SandCastle();
var script = sandcastle.createScript("\
exports.main = function() {\n\
require('fs');\n\
}\
");
script.on('exit', function(err, output) {
console.log(err.message);
console.log(err.stack);
});
script.run();
Outputs
require is not defined
ReferenceError: require is not defined
at Object.main ([object Context]:2:5)
at [object Context]:4:9
at Sandbox.executeScript (/Users/bcoe/hacking/open-source/sandcastle/lib/sandbox.js:58:8)
at Socket.<anonymous> (/Users/bcoe/hacking/open-source/sandcastle/lib/sandbox.js:16:13)
at Socket.emit (events.js:64:17)
at Socket._onReadable (net.js:678:14)
at IOWatcher.onReadable [as callback] (net.js:177:10)
When creating an instance of SandCastle, you can provide an API. Functions within this API will be available inside of the untrustred scripts being executed.
An Example of an API:
var fs = require('fs');
exports.api = {
getFact: function(callback) {
fs.readFile('./examples/example.txt', function (err, data) {
if (err) throw err;
callback(data.toString());
});
},
setTimeout: function(callback, timeout) {
setTimeout(callback, timeout);
}
}
A Script Using the API:
var SandCastle = require('sandcastle').SandCastle;
var sandcastle = new SandCastle({
api: './examples/api.js'
});
var script = sandcastle.createScript("\
exports.main = function() {\
getFact(function(fact) {\
exit(fact);\
});\
}\
");
script.on('exit', function(err, result) {
equal(result, 'The rain in spain falls mostly on the plain.', prefix);
sandcastle.kill();
finished();
});
script.run();
Rather than main, you create a script file that exports multiple methods.
Notice that one extra parameter methodName is available within the callback functions.
var SandCastle = require('sandcastle').SandCastle;
var sandcastle = new SandCastle();
var script = sandcastle.createScript("\
exports = {\
foo: function() {\
exit('Hello Foo!');\
},\
bar: function() {\
exit('Hello Bar!');\
},\
hello: function() {\
exit('Hey ' + name + ' Hello World!');\
}\
}\
");
script.on('timeout', function(methodName) {
console.log(methodName);
});
script.on('exit', function(err, output, methodName) {
console.log(methodName); / foo, bar, hello
});
// take note that a single script should only be
// executing a single method at a time.
var cb = null;
async.eachLimit(['foo', 'bar', 'hello'], 1, function(item, _cb) {
cb = _cb;
script.run(item, {name: 'Ben'});
});
In contrast to the API which runs trusted code inside the sandbox, the script can request that a task (a snippet of code) is executed in another process.
To run a task call runTask(taskName, options = {}) and provide a onTask(taskName, data) method within the script file. Alternatively you can create a task specific function on{TaskName}Task, to receive data for an individual task.
var script = sandcastle.createScript("\
exports = {\
onGetContentTask: function (data) {\
// received content. do something here...
},\
main: function() {\
runTask('getContent', {url: 'http://foo.bar'});\
}\
}\
");
script.on('task', function (err, taskName, options, methodName, callback) {
if (whitelistedUrls.indexOf(options.url) !== -1) {
http.get(options.url, function(res) {
callback(res);
}).on('error', function(e) {
callback(null);
});
}
});
refreshTimeoutOnTask can be used to control the timeout behavior of the script executing the task. If set to true, the script will have its timeout reset when the task is completed.
Make debugging a little easier by ensuring the DEBUG environment variable includes sandcastle.
SandCastle will be an ongoing project, please be liberal with your feedback, criticism, and contributions.
Copyright (c) 2012 Benjamin Coe. See LICENSE.txt for further details.