From 350321bfa4a27959f32301a892d8c455c74005a9 Mon Sep 17 00:00:00 2001 From: Alon Date: Wed, 30 Oct 2013 21:35:40 +0200 Subject: [PATCH] Add recursive sandboxing and JSON support. Closes #11. --- Readme.md | 3 + lib/sandboxed_module.js | 101 +++++++++++++++--- test/fixture/coreModule.js | 1 + test/fixture/criss.js | 4 + test/fixture/cross.js | 4 + test/fixture/folder/zap.js | 2 + test/fixture/globalVars.js | 3 + test/fixture/includeJson.js | 3 + test/fixture/json.json | 3 + test/fixture/moduleModule.js | 5 + test/fixture/moreGlobal.js | 1 + test/fixture/resolve.js | 1 + test/integration/test-module-module.js | 10 ++ .../test-recursive-core-modules.js | 7 ++ test/integration/test-recursive-crossed.js | 11 ++ test/integration/test-recursive-disabled.js | 11 ++ test/integration/test-recursive-global.js | 7 ++ .../test-recursive-json-resolve.js | 6 ++ test/integration/test-recursive-json.js | 7 ++ test/integration/test-recursive.js | 10 ++ 20 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 test/fixture/coreModule.js create mode 100644 test/fixture/criss.js create mode 100644 test/fixture/cross.js create mode 100644 test/fixture/folder/zap.js create mode 100644 test/fixture/globalVars.js create mode 100644 test/fixture/includeJson.js create mode 100644 test/fixture/json.json create mode 100644 test/fixture/moduleModule.js create mode 100644 test/fixture/moreGlobal.js create mode 100644 test/fixture/resolve.js create mode 100644 test/integration/test-module-module.js create mode 100644 test/integration/test-recursive-core-modules.js create mode 100644 test/integration/test-recursive-crossed.js create mode 100644 test/integration/test-recursive-disabled.js create mode 100644 test/integration/test-recursive-global.js create mode 100644 test/integration/test-recursive-json-resolve.js create mode 100644 test/integration/test-recursive-json.js create mode 100644 test/integration/test-recursive.js diff --git a/Readme.md b/Readme.md index b4acc04..898aa83 100644 --- a/Readme.md +++ b/Readme.md @@ -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]) diff --git a/lib/sandboxed_module.js b/lib/sandboxed_module.js index c842d81..fdccb04 100644 --- a/lib/sandboxed_module.js +++ b/lib/sandboxed_module.js @@ -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; @@ -26,7 +31,7 @@ SandboxedModule.load = function(moduleId, options, trace) { var sandboxedModule = new SandboxedModule(); sandboxedModule._init(moduleId, trace, options); - + sandboxedModule._compile() return sandboxedModule; }; @@ -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) { @@ -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]; @@ -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; @@ -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() { @@ -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}; diff --git a/test/fixture/coreModule.js b/test/fixture/coreModule.js new file mode 100644 index 0000000..086fa67 --- /dev/null +++ b/test/fixture/coreModule.js @@ -0,0 +1 @@ +module.exports.path = require('path'); diff --git a/test/fixture/criss.js b/test/fixture/criss.js new file mode 100644 index 0000000..42324e4 --- /dev/null +++ b/test/fixture/criss.js @@ -0,0 +1,4 @@ +cross = require('./cross'); + +module.exports.cross = cross +module.exports.value = 'criss value'; diff --git a/test/fixture/cross.js b/test/fixture/cross.js new file mode 100644 index 0000000..49aef3c --- /dev/null +++ b/test/fixture/cross.js @@ -0,0 +1,4 @@ +criss = require('./criss'); + +module.exports.criss = criss +module.exports.value = 'cross value'; diff --git a/test/fixture/folder/zap.js b/test/fixture/folder/zap.js new file mode 100644 index 0000000..f268955 --- /dev/null +++ b/test/fixture/folder/zap.js @@ -0,0 +1,2 @@ +module.exports.zap = 'zap'; +module.exports.bar = require('../bar'); diff --git a/test/fixture/globalVars.js b/test/fixture/globalVars.js new file mode 100644 index 0000000..ad096dc --- /dev/null +++ b/test/fixture/globalVars.js @@ -0,0 +1,3 @@ +require('./moreGlobal'); + +module.exports.worse = global.worse; diff --git a/test/fixture/includeJson.js b/test/fixture/includeJson.js new file mode 100644 index 0000000..ddb29ab --- /dev/null +++ b/test/fixture/includeJson.js @@ -0,0 +1,3 @@ +var json =require('./json'); + +module.exports.json = json; diff --git a/test/fixture/json.json b/test/fixture/json.json new file mode 100644 index 0000000..9dd028d --- /dev/null +++ b/test/fixture/json.json @@ -0,0 +1,3 @@ +{ + "value":"value from json" +} diff --git a/test/fixture/moduleModule.js b/test/fixture/moduleModule.js new file mode 100644 index 0000000..2b6e236 --- /dev/null +++ b/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); diff --git a/test/fixture/moreGlobal.js b/test/fixture/moreGlobal.js new file mode 100644 index 0000000..f373bf4 --- /dev/null +++ b/test/fixture/moreGlobal.js @@ -0,0 +1 @@ +global.worse = 'worse'; diff --git a/test/fixture/resolve.js b/test/fixture/resolve.js new file mode 100644 index 0000000..b8523f9 --- /dev/null +++ b/test/fixture/resolve.js @@ -0,0 +1 @@ +module.exports.bar = require(require.resolve('./bar')); diff --git a/test/integration/test-module-module.js b/test/integration/test-module-module.js new file mode 100644 index 0000000..8db0285 --- /dev/null +++ b/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'); + diff --git a/test/integration/test-recursive-core-modules.js b/test/integration/test-recursive-core-modules.js new file mode 100644 index 0000000..a0805b4 --- /dev/null +++ b/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')); diff --git a/test/integration/test-recursive-crossed.js b/test/integration/test-recursive-crossed.js new file mode 100644 index 0000000..f066891 --- /dev/null +++ b/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); diff --git a/test/integration/test-recursive-disabled.js b/test/integration/test-recursive-disabled.js new file mode 100644 index 0000000..7cc5707 --- /dev/null +++ b/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'); diff --git a/test/integration/test-recursive-global.js b/test/integration/test-recursive-global.js new file mode 100644 index 0000000..51a9f52 --- /dev/null +++ b/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); diff --git a/test/integration/test-recursive-json-resolve.js b/test/integration/test-recursive-json-resolve.js new file mode 100644 index 0000000..457125e --- /dev/null +++ b/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'); diff --git a/test/integration/test-recursive-json.js b/test/integration/test-recursive-json.js new file mode 100644 index 0000000..e035527 --- /dev/null +++ b/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'); diff --git a/test/integration/test-recursive.js b/test/integration/test-recursive.js new file mode 100644 index 0000000..80cc280 --- /dev/null +++ b/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);