Skip to content

Commit

Permalink
Load Apps Sandboxed
Browse files Browse the repository at this point in the history
- Based on suggestions from hswolff loading with a Module class approach
- Loads relative modules in child sandboxes
  • Loading branch information
jgable committed Feb 4, 2014
1 parent 64dd6b0 commit c7713c1
Show file tree
Hide file tree
Showing 13 changed files with 366 additions and 82 deletions.
11 changes: 8 additions & 3 deletions core/server/apps/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
91 changes: 91 additions & 0 deletions core/server/apps/sandbox.js
Original file line number Diff line number Diff line change
@@ -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;
79 changes: 0 additions & 79 deletions core/test/unit/app_proxy_spec.js

This file was deleted.

157 changes: 157 additions & 0 deletions core/test/unit/apps_spec.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
16 changes: 16 additions & 0 deletions core/test/utils/fixtures/app/badinstall.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions core/test/utils/fixtures/app/badlib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

var knex = require('knex');

module.exports = {
knex: knex
};
Loading

0 comments on commit c7713c1

Please sign in to comment.