Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Replace hollabacks with callbacks #33

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 9 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A nifty javascript sandbox for node.js.
- Handles errors gracefully
- Restricted code (cannot access node.js methods)
- Supports `console.log` and `print` utility methods
- Supports interprocess messaging with the sandboxed code
- Supports interprocess (IPC) messaging with the sandboxed code


## Example
Expand All @@ -28,13 +28,11 @@ s.run('1 + 1 + " apples"', function(output) {

## Documentation

### `Sandbox`#`run`(`code`, `hollaback`)
### `Sandbox`#`run`(`code`, `callback`)

* `code` {`String`} — string of Javascript to be executed.
* `hollaback` {`Function`} — called after execution with a single argument, `output`.
- `output` is an object with two properties: `result` and `console`. The `result`
property is an inspected string of the return value of the code. The `console`
property is an array of all console output.
* `callback` {`Function`} — called after execution with two arguments, `error` and
`result`

For example, given the following code:

Expand All @@ -48,14 +46,8 @@ function add(a, b){
add(20, 22);
```

The resulting output object is:
The callback will be called with `(null, '42')`

```javascript
{
result: "42",
console: ["20", "22"]
}
```

### `Sandbox`#`postMessage`(`message`)

Expand Down Expand Up @@ -86,9 +78,10 @@ sandbox.on('message', function(message){
sandbox.postMessage('hello from outside');
```

The process will ONLY be considered finished if `onmessage` is NOT a function.
If `onmessage` is defined the sandbox will assume that it is waiting for an
incoming message.
The process will ONLY be considered finished if `onmessage` is NOT a function or
`process.exit()` is called. If `onmessage` is defined the sandbox will assume that
it is waiting for an incoming message. Note, however, that the timeout will still
cause asynchronous sandboxed code to result in a `TimeoutError` if it takes too long.


## Installation & Running
Expand Down
48 changes: 24 additions & 24 deletions example/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,63 @@ var Sandbox = require("../lib/sandbox")
, s = new Sandbox()

// Example 1 - Standard JS
s.run( "1 + 1", function( output ) {
console.log( "Example 1: " + output.result + "\n" )
s.run( "1 + 1", function(error, result) {
console.log( "Example 1: " + result + "\n" )
})

// Example 2 - Something slightly more complex
s.run( "(function(name) { return 'Hi there, ' + name + '!'; })('Fabio')", function( output ) {
console.log( "Example 2: " + output.result + "\n" )
s.run( "(function(name) { return 'Hi there, ' + name + '!'; })('Fabio')", function(error, result) {
console.log( "Example 2: " + result + "\n" )
})

// Example 3 - Syntax error
s.run( "lol)hai", function( output ) {
console.log( "Example 3: " + output.result + "\n" )
s.run( "lol)hai", function(error, result) {
console.log( "Example 3: " + result + "\n" )
});

// Example 4 - Restricted code
s.run( "process.platform", function( output ) {
console.log( "Example 4: " + output.result + "\n" )
s.run( "process.platform", function(error, result) {
console.log( "Example 4: " + result + "\n" )
})

// Example 5 - Infinite loop
s.run( "while (true) {}", function( output ) {
console.log( "Example 5: " + output.result + "\n" )
// A different sandbox is used for this example because the following ones
// end up being run before this one is finished otherwise
var sb = new Sandbox();
sb.run( "while (true) {}", function(error, result) {
console.log( "Example 5: " + error + "\n" )
})

// Example 6 - Caller Attack Failure
s.run( "(function foo() {return foo.caller.caller;})()", function( output ) {
console.log( "Example 6: " + output.result + "\n" )
s.run( "(function foo() {return foo.caller.caller;})()", function(error, result) {
console.log( "Example 6: " + result + "\n" )
})

// Example 7 - Argument Attack Failure
s.run( "(function foo() {return [].slice.call(foo.caller.arguments);})()", function( output ) {
console.log( "Example 7: " + output.result + "\n" )
s.run( "(function foo() {return [].slice.call(foo.caller.arguments);})()", function(error, result) {
console.log( "Example 7: " + result + "\n" )
})

// Example 8 - Type Coersion Attack Failure
s.run( "(function foo() {return {toJSON:function x(){return x.caller.caller.name}}})()", function( output ) {
console.log( "Example 8: " + output.result + "\n" )
s.run( "(function foo() {return {toJSON:function x(){return x.caller.caller.name}}})()", function(error, result) {
console.log( "Example 8: " + result + "\n" )
})

// Example 9 - Global Attack Failure
s.run( "x=1;(function() {return this})().console.log.constructor('return this')()", function( output ) {
console.log( "Example 9: " + output.result + "\n" )
s.run( "x=1;(function() {return this})().console.log.constructor('return this')()", function(error, result) {
console.log( "Example 9: " + result + "\n" )
})

// Example 10 - Console Log
s.run( "var x = 5; console.log(x * x); x", function( output ) {
console.log( "Example 10: " + output.console + "\n" )
})
s.run( "var x = 5; console.log('Example 10: ' + (x * x)); x", function(error, result) {})

// Example 11 - IPC Messaging
s.run( "onmessage = function(message){ if (message === 'hello from outside') { postMessage('hello from inside'); };", function(output){
s.run( "onmessage = function(message){ if (message === 'hello from outside') { postMessage('hello from inside'); process.exit(); };", function(error, result){

})
s.on('message', function(message){
console.log("Example 11: received message sent from inside the sandbox '" + message + "'\n")
});
var test_message = "hello from outside";
console.log("Example 11: sending message into the sandbox '" + test_message + "'");
s.postMessage(test_message);

109 changes: 70 additions & 39 deletions lib/sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ var path = require('path');
var spawn = require('child_process').spawn;
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var concat = require('concat-stream');

//-----------------------------------------------------------------------------
// Constructor
//-----------------------------------------------------------------------------

function Sandbox(options) {
var self = this;

// message_queue is used to store messages that are meant to be sent
// to the sandbox before the sandbox is ready to process them
self._ready = false;
self._message_queue = [];


// Instance keeps a reference to stdout so it can be
// overwritten for testing purposes
self._stdout = process.stdout;

self.options = {
timeout: 500,
node: 'node',
Expand All @@ -37,79 +42,105 @@ util.inherits(Sandbox, EventEmitter);
// Instance Methods
//-----------------------------------------------------------------------------

Sandbox.prototype.run = function(code, hollaback) {
Sandbox.prototype.run = function(code, callback) {
var self = this;
var timer;
var stdout = '';
var result;
var error = null;

// Spawn child process
self.child = spawn(this.options.node, [this.options.shovel], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] });
var output = function(data) {
if (!!data) {
stdout += data;
}
};

if (typeof hollaback == 'undefined') {
hollaback = console.log;
} else {
hollaback = hollaback.bind(this);
}
// Pass data written to stdout directly to this process' stdout
function stdoutHandler(data){
var lines = String(data).split('\n');
lines.forEach(function(line, index){
// String.split will result in an extra empty string at the end
if (index !== lines.length - 1 || line) {
self._stdout.write(line + '\n');
}
});
};
self.child.stdout.on('data', stdoutHandler);

// Listen
self.child.stdout.on('data', output);
// Listen for errors and call the callback immediately
function stderrHandler(data) {
if (data && data.length > 0) {
error = String(data);
}
};
self.child.stderr.pipe(concat(stderrHandler));

// Pass messages out from child process
// These messages can be handled by Sandbox.on('message', function(message){...});
self.child.on('message', function(message){
if (message === '__sandbox_inner_ready__') {

self.emit('ready');
if (typeof message !== 'object' || typeof message.type !== 'string') {
throw new Error('Bad IPC Message: ' + JSON.stringify(message));
}

if (message.type === 'ready') {

self._ready = true;

self.emit('ready');

// Process the _message_queue
while(self._message_queue.length > 0) {
self.postMessage(self._message_queue.shift());
}

} else if (message.type === 'result') {

// Should this be stringified?
result = String(message.data);

// Special case null and undefined so that the result does not
// end up as a stringified version (i.e. "null")
if (result === 'null') {
result = null;
} else if (result === 'undefined') {
result = undefined;
}


} else if (message.type === 'message') {

self.emit('message', message.data);

} else {
self.emit('message', message);
throw new Error('Bad IPC Message: ' + JSON.stringify(message));
}
});

self.child.on('exit', function(code) {

// This function should be the only one that calls the hollback
function onExit(code) {
clearTimeout(timer);
setImmediate(function(){
if (!stdout) {
hollaback({ result: 'Error', console: [] });
} else {
var ret;
try {
ret = JSON.parse(stdout);
} catch (e) {
ret = { result: 'JSON Error (data was "'+stdout+'")', console: [] }
}
hollaback(ret);
if (typeof callback === 'function') {
callback(error, result);
}
});
});
};
self.child.on('exit', onExit);


// Go
self.child.stdin.write(code);
self.child.stdin.end();

timer = setTimeout(function() {
self.child.stdout.removeListener('output', output);
stdout = JSON.stringify({ result: 'TimeoutError', console: [] });
self.child.stdout.removeListener('data', stdoutHandler);
error = 'TimeoutError';
self.child.kill('SIGKILL');
}, self.options.timeout);
};

// Send a message to the code running inside the sandbox
// This message will be passed to the sandboxed
// This message will be passed to the sandboxed
// code's `onmessage` function, if defined.
// Messages posted before the sandbox is ready will be queued
Sandbox.prototype.postMessage = function(message) {
var self = this;

if (self._ready) {
self.child.send(message);
} else {
Expand Down
Loading