diff --git a/core/server/apps/loader.js b/core/server/apps/loader.js index 81d34881f4..73a1d37edc 100644 --- a/core/server/apps/loader.js +++ b/core/server/apps/loader.js @@ -4,10 +4,9 @@ var path = require('path'), when = require('when'), appProxy = require('./proxy'), config = require('../config'), + AppSandbox = require('./sandbox'), loader; - - // Get a relative path to the given apps root, defaults // to be relative to __dirname function getAppRelativePath(name, relativeTo) { @@ -16,10 +15,16 @@ function getAppRelativePath(name, relativeTo) { return path.relative(relativeTo, path.join(config.paths().appPath, name)); } +// Load apps through a psuedo sandbox +function loadApp(appPath) { + var sandbox = new AppSandbox(); + + return sandbox.loadApp(appPath); +} function getAppByName(name) { // Grab the app class to instantiate - var AppClass = require(getAppRelativePath(name)), + var AppClass = loadApp(getAppRelativePath(name)), app; // Check for an actual class, otherwise just use whatever was returned diff --git a/core/server/apps/sandbox.js b/core/server/apps/sandbox.js new file mode 100644 index 0000000000..418444dfc8 --- /dev/null +++ b/core/server/apps/sandbox.js @@ -0,0 +1,91 @@ + +var fs = require('fs'), + path = require('path'), + Module = require('module'), + _ = require('underscore'); + +function AppSandbox(opts) { + this.opts = _.defaults(opts || {}, AppSandbox.defaults); +} + +AppSandbox.prototype.loadApp = function loadAppSandboxed(appPath) { + var appFile = require.resolve(appPath), + appBase = path.dirname(appFile); + + this.opts.appRoot = appBase; + + return this.loadModule(appPath); +}; + +AppSandbox.prototype.loadModule = function loadModuleSandboxed(modulePath) { + // Set loaded modules parent to this + var self = this, + moduleDir = path.dirname(modulePath), + parentModulePath = self.opts.parent || module.parent, + appRoot = self.opts.appRoot || moduleDir, + currentModule, + nodeRequire; + + // Resolve the modules path + modulePath = Module._resolveFilename(modulePath, parentModulePath); + + // Instantiate a Node Module class + currentModule = new Module(modulePath, parentModulePath); + + // Grab the original modules require function + nodeRequire = currentModule.require; + + // Set a new proxy require function + currentModule.require = function requireProxy(module) { + // check whitelist, plugin config, etc. + if (_.contains(self.opts.blacklist, module)) { + throw new Error("Unsafe App require: " + module); + } + + var firstTwo = module.slice(0, 2), + resolvedPath, + relPath, + innerBox, + newOpts; + + // Load relative modules with their own sandbox + if (firstTwo === './' || firstTwo === '..') { + // Get the path relative to the modules directory + resolvedPath = path.resolve(moduleDir, module); + + // Check relative path from the appRoot for outside requires + relPath = path.relative(appRoot, resolvedPath); + if (relPath.slice(0, 2) === '..') { + throw new Error('Unsafe App require: ' + relPath); + } + + // Assign as new module path + module = resolvedPath; + + // Pass down the same options + newOpts = _.extend({}, self.opts); + + // Make sure the appRoot and parent are appropriate + newOpts.appRoot = appRoot; + newOpts.parent = currentModule.parent; + + // Create the inner sandbox for loading this module. + innerBox = new AppSandbox(newOpts); + + return innerBox.loadModule(module); + } + + // Call the original require method for white listed named modules + return nodeRequire.call(currentModule, module); + }; + + currentModule.load(currentModule.id); + + return currentModule.exports; +}; + +AppSandbox.defaults = { + blacklist: ['knex', 'fs', 'http', 'sqlite3', 'pg', 'mysql', 'ghost'] +}; + +module.exports = AppSandbox; \ No newline at end of file diff --git a/core/test/unit/app_proxy_spec.js b/core/test/unit/app_proxy_spec.js deleted file mode 100644 index e389c40e42..0000000000 --- a/core/test/unit/app_proxy_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -/*globals describe, beforeEach, afterEach, before, it*/ -var should = require('should'), - sinon = require('sinon'), - _ = require("underscore"), - helpers = require('../../server/helpers'), - filters = require('../../server/filters'), - - // Stuff we are testing - appProxy = require('../../server/apps/proxy'); - -describe('App Proxy', function () { - - var sandbox, - fakeApi; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - - fakeApi = { - posts: { - browse: sandbox.stub(), - read: sandbox.stub(), - edit: sandbox.stub(), - add: sandbox.stub(), - destroy: sandbox.stub() - }, - users: { - browse: sandbox.stub(), - read: sandbox.stub(), - edit: sandbox.stub() - }, - tags: { - all: sandbox.stub() - }, - notifications: { - destroy: sandbox.stub(), - add: sandbox.stub() - }, - settings: { - browse: sandbox.stub(), - read: sandbox.stub(), - add: sandbox.stub() - } - }; - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('creates a ghost proxy', function () { - should.exist(appProxy.filters); - appProxy.filters.register.should.equal(filters.registerFilter); - appProxy.filters.unregister.should.equal(filters.unregisterFilter); - - should.exist(appProxy.helpers); - appProxy.helpers.register.should.equal(helpers.registerThemeHelper); - appProxy.helpers.registerAsync.should.equal(helpers.registerAsyncThemeHelper); - - should.exist(appProxy.api); - - should.exist(appProxy.api.posts); - should.not.exist(appProxy.api.posts.edit); - should.not.exist(appProxy.api.posts.add); - should.not.exist(appProxy.api.posts.destroy); - - should.not.exist(appProxy.api.users); - - should.exist(appProxy.api.tags); - - should.exist(appProxy.api.notifications); - should.not.exist(appProxy.api.notifications.destroy); - - should.exist(appProxy.api.settings); - should.not.exist(appProxy.api.settings.browse); - should.not.exist(appProxy.api.settings.add); - - }); -}); \ No newline at end of file diff --git a/core/test/unit/apps_spec.js b/core/test/unit/apps_spec.js new file mode 100644 index 0000000000..5e845ef104 --- /dev/null +++ b/core/test/unit/apps_spec.js @@ -0,0 +1,157 @@ +/*globals describe, beforeEach, afterEach, before, it*/ +var fs = require('fs'), + path = require('path'), + should = require('should'), + sinon = require('sinon'), + _ = require("underscore"), + helpers = require('../../server/helpers'), + filters = require('../../server/filters'), + + // Stuff we are testing + appProxy = require('../../server/apps/proxy'), + AppSandbox = require('../../server/apps/sandbox'); + +describe('Apps', function () { + + var sandbox, + fakeApi; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + + fakeApi = { + posts: { + browse: sandbox.stub(), + read: sandbox.stub(), + edit: sandbox.stub(), + add: sandbox.stub(), + destroy: sandbox.stub() + }, + users: { + browse: sandbox.stub(), + read: sandbox.stub(), + edit: sandbox.stub() + }, + tags: { + all: sandbox.stub() + }, + notifications: { + destroy: sandbox.stub(), + add: sandbox.stub() + }, + settings: { + browse: sandbox.stub(), + read: sandbox.stub(), + add: sandbox.stub() + } + }; + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('Proxy', function () { + it('creates a ghost proxy', function () { + should.exist(appProxy.filters); + appProxy.filters.register.should.equal(filters.registerFilter); + appProxy.filters.unregister.should.equal(filters.unregisterFilter); + + should.exist(appProxy.helpers); + appProxy.helpers.register.should.equal(helpers.registerThemeHelper); + appProxy.helpers.registerAsync.should.equal(helpers.registerAsyncThemeHelper); + + should.exist(appProxy.api); + + should.exist(appProxy.api.posts); + should.not.exist(appProxy.api.posts.edit); + should.not.exist(appProxy.api.posts.add); + should.not.exist(appProxy.api.posts.destroy); + + should.not.exist(appProxy.api.users); + + should.exist(appProxy.api.tags); + + should.exist(appProxy.api.notifications); + should.not.exist(appProxy.api.notifications.destroy); + + should.exist(appProxy.api.settings); + should.not.exist(appProxy.api.settings.browse); + should.not.exist(appProxy.api.settings.add); + }); + }); + + describe('Sandbox', function () { + it('loads apps in a sandbox', function () { + var appBox = new AppSandbox(), + appPath = path.resolve(__dirname, '..', 'utils', 'fixtures', 'app', 'good.js'), + GoodApp, + app; + + GoodApp = appBox.loadApp(appPath); + + should.exist(GoodApp); + + app = new GoodApp(appProxy); + + app.install(appProxy); + + app.app.something.should.equal(42); + app.app.util.util().should.equal(42); + app.app.nested.other.should.equal(42); + app.app.path.should.equal(appPath); + }); + + it('does not allow apps to require blacklisted modules at top level', function () { + var appBox = new AppSandbox(), + badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badtop.js'), + BadApp, + app, + loadApp = function () { + appBox.loadApp(badAppPath); + }; + + loadApp.should.throw('Unsafe App require: knex'); + }); + + it('does not allow apps to require blacklisted modules at install', function () { + var appBox = new AppSandbox(), + badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badinstall.js'), + BadApp, + app, + installApp = function () { + app.install(appProxy); + }; + + BadApp = appBox.loadApp(badAppPath); + + app = new BadApp(appProxy); + + installApp.should.throw('Unsafe App require: knex'); + }); + + it('does not allow apps to require blacklisted modules from other requires', function () { + var appBox = new AppSandbox(), + badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badrequire.js'), + BadApp, + app, + loadApp = function () { + BadApp = appBox.loadApp(badAppPath); + }; + + loadApp.should.throw('Unsafe App require: knex'); + }); + + it('does not allow apps to require modules relatively outside their directory', function () { + var appBox = new AppSandbox(), + badAppPath = path.join(__dirname, '..', 'utils', 'fixtures', 'app', 'badoutside.js'), + BadApp, + app, + loadApp = function () { + BadApp = appBox.loadApp(badAppPath); + }; + + loadApp.should.throw('Unsafe App require: ../example'); + }); + }); +}); \ No newline at end of file diff --git a/core/test/utils/fixtures/app/badinstall.js b/core/test/utils/fixtures/app/badinstall.js new file mode 100644 index 0000000000..1fe296a3df --- /dev/null +++ b/core/test/utils/fixtures/app/badinstall.js @@ -0,0 +1,16 @@ + +function BadApp(app) { + this.app = app; +} + +BadApp.prototype.install = function () { + var knex = require('knex'); + + return knex.dropTableIfExists('users'); +}; + +BadApp.prototype.activate = function () { + +}; + +module.exports = BadApp; \ No newline at end of file diff --git a/core/test/utils/fixtures/app/badlib.js b/core/test/utils/fixtures/app/badlib.js new file mode 100644 index 0000000000..4b95186a6e --- /dev/null +++ b/core/test/utils/fixtures/app/badlib.js @@ -0,0 +1,6 @@ + +var knex = require('knex'); + +module.exports = { + knex: knex +}; \ No newline at end of file diff --git a/core/test/utils/fixtures/app/badoutside.js b/core/test/utils/fixtures/app/badoutside.js new file mode 100644 index 0000000000..9052522ee7 --- /dev/null +++ b/core/test/utils/fixtures/app/badoutside.js @@ -0,0 +1,16 @@ + +var lib = require('../example'); + +function BadApp(app) { + this.app = app; +} + +BadApp.prototype.install = function () { + return lib.answer; +}; + +BadApp.prototype.activate = function () { + +}; + +module.exports = BadApp; \ No newline at end of file diff --git a/core/test/utils/fixtures/app/badrequire.js b/core/test/utils/fixtures/app/badrequire.js new file mode 100644 index 0000000000..563416cf1a --- /dev/null +++ b/core/test/utils/fixtures/app/badrequire.js @@ -0,0 +1,16 @@ + +var lib = require('./badlib'); + +function BadApp(app) { + this.app = app; +} + +BadApp.prototype.install = function () { + return lib.knex.dropTableIfExists('users'); +}; + +BadApp.prototype.activate = function () { + +}; + +module.exports = BadApp; \ No newline at end of file diff --git a/core/test/utils/fixtures/app/badtop.js b/core/test/utils/fixtures/app/badtop.js new file mode 100644 index 0000000000..a285ed5643 --- /dev/null +++ b/core/test/utils/fixtures/app/badtop.js @@ -0,0 +1,16 @@ + +var knex = require('knex'); + +function BadApp(app) { + this.app = app; +} + +BadApp.prototype.install = function () { + return knex.dropTableIfExists('users'); +}; + +BadApp.prototype.activate = function () { + +}; + +module.exports = BadApp; \ No newline at end of file diff --git a/core/test/utils/fixtures/app/good.js b/core/test/utils/fixtures/app/good.js new file mode 100644 index 0000000000..c2713a92d6 --- /dev/null +++ b/core/test/utils/fixtures/app/good.js @@ -0,0 +1,24 @@ + +var path = require('path'), + util = require('./goodlib.js'), + nested = require('./nested/goodnested'); + +function GoodApp(app) { + this.app = app; +} + +GoodApp.prototype.install = function () { + // Goes through app to do data + this.app.something = 42; + this.app.util = util; + this.app.nested = nested; + this.app.path = path.join(__dirname, 'good.js'); + + return true; +}; + +GoodApp.prototype.activate = function () { + +}; + +module.exports = GoodApp; \ No newline at end of file diff --git a/core/test/utils/fixtures/app/goodlib.js b/core/test/utils/fixtures/app/goodlib.js new file mode 100644 index 0000000000..ce51d4d15e --- /dev/null +++ b/core/test/utils/fixtures/app/goodlib.js @@ -0,0 +1,6 @@ + +module.exports = { + util: function () { + return 42; + } +}; \ No newline at end of file diff --git a/core/test/utils/fixtures/app/nested/goodnested.js b/core/test/utils/fixtures/app/nested/goodnested.js new file mode 100644 index 0000000000..62647785bb --- /dev/null +++ b/core/test/utils/fixtures/app/nested/goodnested.js @@ -0,0 +1,6 @@ + +var lib = require('../goodlib.js'); + +module.exports = { + other: 42 +}; \ No newline at end of file diff --git a/core/test/utils/fixtures/example.js b/core/test/utils/fixtures/example.js new file mode 100644 index 0000000000..bb7c00d40e --- /dev/null +++ b/core/test/utils/fixtures/example.js @@ -0,0 +1,4 @@ + +module.exports = { + answer: 42 +}; \ No newline at end of file