1.1.0 • Published 8 years ago

runspace v1.1.0

Weekly downloads
3
License
MIT
Repository
github
Last release
8 years ago

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.

1.1.0

8 years ago

1.0.0

8 years ago