forked from TryGhost/Ghost
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Based on suggestions from hswolff loading with a Module class approach - Loads relative modules in child sandboxes
- Loading branch information
Showing
13 changed files
with
366 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
|
||
var knex = require('knex'); | ||
|
||
module.exports = { | ||
knex: knex | ||
}; |
Oops, something went wrong.