Skip to content

Commit

Permalink
Add recursive sandboxing and JSON support.
Browse files Browse the repository at this point in the history
Closes #11.
  • Loading branch information
alonbardavid authored and domenic committed Jun 11, 2014
1 parent ff9c9cd commit 350321b
Show file tree
Hide file tree
Showing 20 changed files with 186 additions and 14 deletions.
3 changes: 3 additions & 0 deletions Readme.md
Expand Up @@ -46,6 +46,9 @@ following:
* `locals:` An object of local variables to inject into the sandboxed module.
* `sourceTransformers:` An object of named functions to transform the source code of
the sandboxed module's file (e.g. transpiler language, code coverage).
* `singleOnly:` If false - modules that are required by the sandboxed module will not
be sandboxed. By default all modules required by the sandboxedModule will be sandboxed
using the same options that were used for the original sandboxed module

### SandboxedModule.require(moduleId, [options])

Expand Down
101 changes: 87 additions & 14 deletions lib/sandboxed_module.js
Expand Up @@ -8,6 +8,11 @@ var parent = module.parent;
var globalOptions = {};
var registeredBuiltInSourceTransformers = ['coffee']

var builtinlibs = ['assert', 'buffer', 'child_process', 'cluster',
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https','net',
'os', 'path', 'punycode', 'querystring', 'readline', 'stream',
'string_decoder','timers', 'tls', 'tty', 'url', 'util', 'vm', 'zlib', 'smalloc'];

module.exports = SandboxedModule;
function SandboxedModule() {
this.id = null;
Expand All @@ -26,7 +31,7 @@ SandboxedModule.load = function(moduleId, options, trace) {

var sandboxedModule = new SandboxedModule();
sandboxedModule._init(moduleId, trace, options);

sandboxedModule._compile()
return sandboxedModule;
};

Expand Down Expand Up @@ -67,11 +72,10 @@ SandboxedModule.prototype._init = function(moduleId, trace, options) {

this.module = module;

this.locals = this._getLocals();
this.globals = this._getGlobals();
//globals must be set before locals to share global state
this.locals = this._getLocals();
this.sourceTransformers = this._getSourceTransformers();

this._compile();
};

SandboxedModule.prototype._resolveFilename = function(trace) {
Expand All @@ -81,12 +85,13 @@ SandboxedModule.prototype._resolveFilename = function(trace) {

SandboxedModule.prototype._getLocals = function() {
var locals = {
require: this._requireInterceptor(),
__filename: this.filename,
__dirname: path.dirname(this.filename),
module: this.module,
exports: this.exports
};
//must be initialized after exports, or cyclic dependencies won't work
locals.require = this._requireInterceptor()

for (var globalKey in globalOptions.locals) {
locals[globalKey] = globalOptions.locals[globalKey];
Expand Down Expand Up @@ -141,8 +146,72 @@ SandboxedModule.prototype._getRequires = function() {
return requires;
};

function bindRequire(proxy, sandboxedModule) {
var req = proxy.bind(sandboxedModule);
req.main = require.main;
req.resolve = requireLike(sandboxedModule.filename).resolve;
req.extensions = require.extensions;
req.registerExtensions = require.registerExtensions;
return req;
}
SandboxedModule.prototype._createRecursiveRequireProxy = function() {
var cache = Object.create(null);
var required = this._getRequires();
for (var key in required) {
var injectedFilename = requireLike(this.filename).resolve(key);
cache[injectedFilename] = required[key];
}
cache[this.filename] = this.exports;
var globals = this.globals;

function createInnerSandboxedModule(requestedFilename){
// load nested dependency in sandboxed module
var sandboxedModule = new SandboxedModule();
sandboxedModule._getGlobals = function(){return globals};
var trace = stackTrace.get(createInnerSandboxedModule);
sandboxedModule._init(requestedFilename,trace);
var proxyRequire = bindRequire(RecursiveRequireProxy,sandboxedModule);
sandboxedModule.locals.require = proxyRequire;
sandboxedModule.module.require = proxyRequire
cache[requestedFilename] = sandboxedModule.exports;
sandboxedModule._compile();
cache[requestedFilename] = sandboxedModule.exports;
return sandboxedModule;
}
function createFakeModuleModule(){
var realModule = require("module");
var fakeModule = function(){
realModule.apply(this,Array.prototype.slice.call(arguments,0));
}
Object.keys(realModule).forEach(function(key){
fakeModule[key] = realModule[key];
})
fakeModule._load = function(file,parentModule){
var sandboxedModule = createInnerSandboxedModule(parentModule.filename);
return RecursiveRequireProxy.bind(sandboxedModule)(file);
}
return fakeModule;
}
function RecursiveRequireProxy(request){
//core modules:
if (request == "module") {
//the module Module can also be used to require, so need special care
return createFakeModuleModule();
}
if (builtinlibs.indexOf(request) >=0) {
if (request in cache) return cache[request];
return require(request);
}
// cached modules
var requestedFilename = requireLike(this.filename).resolve(request);
if (requestedFilename in cache) return cache[requestedFilename];
var sandboxedModule = createInnerSandboxedModule(requestedFilename)
return sandboxedModule.exports;
}
return RecursiveRequireProxy.bind(this);
}
SandboxedModule.prototype._requireInterceptor = function() {
var requireProxy = requireLike(this.filename, true);
var requireProxy = this._options.singleOnly? requireLike(this.filename, true):this._createRecursiveRequireProxy();
var inject = this._getRequires();
var self = this;

Expand All @@ -157,19 +226,26 @@ SandboxedModule.prototype._requireInterceptor = function() {
for (var key in requireProxy) {
requireInterceptor[key] = requireProxy[key];
}

return requireInterceptor;
var wrappedRequire = bindRequire(requireInterceptor,this);
this.module.require = wrappedRequire;
return wrappedRequire;
};

SandboxedModule.prototype._compile = function() {
if (/\.json$/.test(this.filename)){
var json = require(this.filename);
for( var key in json){
this.exports[key] = json[key];
}
return;
}
var compile = this._getCompileInfo();
var compiledWrapper = vm.runInNewContext(
compile.source,
this.globals,
this.filename
);

this.globals = compiledWrapper.apply(this.exports, compile.parameters);
compiledWrapper.apply(this.exports, [this.globals].concat(compile.parameters));
};

SandboxedModule.prototype._getCompileInfo = function() {
Expand All @@ -186,11 +262,8 @@ SandboxedModule.prototype._getCompileInfo = function() {
}.bind(this), fs.readFileSync(this.filename, 'utf8'));

var source =
'global = GLOBAL = root = (function() { return this; })();' +
'(function(' + localVariables.join(', ') + ') { ' +
'(function(global,' + localVariables.join(', ') + ') { ' +
sourceToWrap +
'\n' +
'return global;\n' +
'});';

return {source: source, parameters: localValues};
Expand Down
1 change: 1 addition & 0 deletions test/fixture/coreModule.js
@@ -0,0 +1 @@
module.exports.path = require('path');
4 changes: 4 additions & 0 deletions test/fixture/criss.js
@@ -0,0 +1,4 @@
cross = require('./cross');

module.exports.cross = cross
module.exports.value = 'criss value';
4 changes: 4 additions & 0 deletions test/fixture/cross.js
@@ -0,0 +1,4 @@
criss = require('./criss');

module.exports.criss = criss
module.exports.value = 'cross value';
2 changes: 2 additions & 0 deletions test/fixture/folder/zap.js
@@ -0,0 +1,2 @@
module.exports.zap = 'zap';
module.exports.bar = require('../bar');
3 changes: 3 additions & 0 deletions test/fixture/globalVars.js
@@ -0,0 +1,3 @@
require('./moreGlobal');

module.exports.worse = global.worse;
3 changes: 3 additions & 0 deletions test/fixture/includeJson.js
@@ -0,0 +1,3 @@
var json =require('./json');

module.exports.json = json;
3 changes: 3 additions & 0 deletions test/fixture/json.json
@@ -0,0 +1,3 @@
{
"value":"value from json"
}
5 changes: 5 additions & 0 deletions test/fixture/moduleModule.js
@@ -0,0 +1,5 @@
var Module = require('module');

var parentModule = new Module(require.resolve('./foo'));
parentModule.filename = require.resolve('./foo');
module.exports.bar = Module._load('./bar',parentModule);
1 change: 1 addition & 0 deletions test/fixture/moreGlobal.js
@@ -0,0 +1 @@
global.worse = 'worse';
1 change: 1 addition & 0 deletions test/fixture/resolve.js
@@ -0,0 +1 @@
module.exports.bar = require(require.resolve('./bar'));
10 changes: 10 additions & 0 deletions test/integration/test-module-module.js
@@ -0,0 +1,10 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var requireModule = SandboxedModule.load('../fixture/moduleModule', {
requires: { './bar': 'fakeBar' }
});
var exports = requireModule.exports;

assert.strictEqual(exports.bar, 'fakeBar');

7 changes: 7 additions & 0 deletions test/integration/test-recursive-core-modules.js
@@ -0,0 +1,7 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var fakeBar = 'fakeBar';
var requireModule = SandboxedModule.load('../fixture/coreModule');
var recursiveExports = requireModule.exports;
assert.strictEqual(recursiveExports.path, require('path'));
11 changes: 11 additions & 0 deletions test/integration/test-recursive-crossed.js
@@ -0,0 +1,11 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var fakeBar = 'fakeBar';
var requireModule = SandboxedModule.load('../fixture/criss');
var criss = requireModule.exports;
var cross = criss.cross
assert.strictEqual(criss.value, 'criss value');
assert.strictEqual(cross.value, 'cross value');
assert.strictEqual(criss.cross, cross);
assert.strictEqual(cross.criss, criss);
11 changes: 11 additions & 0 deletions test/integration/test-recursive-disabled.js
@@ -0,0 +1,11 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var fakeBar = 'fakeBar';
var requireModule = SandboxedModule.load('../fixture/require', {
requires: { './bar': fakeBar },
singleOnly:true
});
var recursiveExports = requireModule.exports;
assert.strictEqual(recursiveExports.bar, fakeBar);
assert.strictEqual(recursiveExports.foo.bar, 'bar');
7 changes: 7 additions & 0 deletions test/integration/test-recursive-global.js
@@ -0,0 +1,7 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var requireModule = SandboxedModule.load('../fixture/globalVars');
var recursiveExports = requireModule.exports;
assert.strictEqual(recursiveExports.worse, 'worse');
assert.equal(global.worse, undefined);
6 changes: 6 additions & 0 deletions test/integration/test-recursive-json-resolve.js
@@ -0,0 +1,6 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var requireModule = SandboxedModule.load('../fixture/resolve');
var recursiveExports = requireModule.exports;
assert.strictEqual(recursiveExports.bar, 'bar');
7 changes: 7 additions & 0 deletions test/integration/test-recursive-json.js
@@ -0,0 +1,7 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var fakeBar = 'fakeBar';
var requireModule = SandboxedModule.load('../fixture/includeJson');
var recursiveExports = requireModule.exports;
assert.strictEqual(recursiveExports.json.value, 'value from json');
10 changes: 10 additions & 0 deletions test/integration/test-recursive.js
@@ -0,0 +1,10 @@
var assert = require('assert');
var SandboxedModule = require('../..');

var fakeBar = 'fakeBar';
var requireModule = SandboxedModule.load('../fixture/require', {
requires: { './bar': fakeBar }
});
var recursiveExports = requireModule.exports;
assert.strictEqual(recursiveExports.bar, fakeBar);
assert.strictEqual(recursiveExports.foo.bar, fakeBar);

0 comments on commit 350321b

Please sign in to comment.