Security improvements

no issue
- added CSRF protection
- changed session handling to express.session
- changed session handling to change session id
- added config property useCookieSession
- added file extension check for /ghost/upload
- removed /ghost/debug/db/reset
This commit is contained in:
Sebastian Gierlinger 2013-10-17 15:28:28 +02:00
parent b544ee7ed6
commit 90176e1f40
9 changed files with 103 additions and 52 deletions

View File

@ -64,6 +64,9 @@
$dropzone.find('.js-fileupload').fileupload().fileupload("option", {
url: '/ghost/upload/',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
add: function (e, data) {
$dropzone.find('.js-fileupload').removeClass('right');
$dropzone.find('.js-url, button.centre').remove();

View File

@ -23,6 +23,16 @@
_.extend(Ghost, Backbone.Events);
Backbone.oldsync = Backbone.sync;
// override original sync method to make header request contain csrf token
Backbone.sync = function (method, model, options, error) {
options.beforeSend = function (xhr) {
xhr.setRequestHeader('X-CSRF-Token', $("meta[name='csrf-param']").attr('content'));
};
/* call the old sync method */
return Backbone.oldsync(method, model, options, error);
};
Ghost.init = function () {
Ghost.router = new Ghost.Router();

View File

@ -202,6 +202,9 @@
if (self.className.indexOf('notification-persistent') !== -1) {
$.ajax({
type: "DELETE",
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
url: '/api/v0.1/notifications/' + $(self).find('.close').data('id')
}).done(function (result) {
bbSelf.$el.slideUp(250, function () {
@ -231,6 +234,9 @@
bbSelf = this;
$.ajax({
type: "DELETE",
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
url: '/api/v0.1/notifications/' + $(self).data('id')
}).done(function (result) {
var height = bbSelf.$('.js-notification').outerHeight(true),

View File

@ -33,6 +33,9 @@
$.ajax({
url: '/ghost/signin/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
email: email,
password: password,
@ -87,6 +90,9 @@
$.ajax({
url: '/ghost/signup/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
name: name,
email: email,
@ -136,6 +142,9 @@
$.ajax({
url: '/ghost/forgotten/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
email: email
},

View File

@ -351,6 +351,9 @@
$.ajax({
url: '/ghost/changepw/',
type: 'POST',
headers: {
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
data: {
password: oldPassword,
newpassword: newPassword,

View File

@ -104,6 +104,7 @@ function ghostLocals(req, res, next) {
res.locals = res.locals || {};
res.locals.version = packageInfo.version;
res.locals.path = req.path;
res.locals.csrfToken = req.session._csrf;
if (res.isAdmin) {
_.extend(res.locals, {
@ -270,9 +271,21 @@ when(ghost.init()).then(function () {
server.use('/ghost/upload/', express.multipart());
server.use('/ghost/upload/', express.multipart({uploadDir: __dirname + '/content/images'}));
server.use('/ghost/debug/db/import/', express.multipart());
server.use(express.cookieParser(ghost.dbHash));
server.use(express.cookieSession({ cookie: { maxAge: 60000000 }}));
// Session handling
// Pro tip: while in development mode cookieSession can be used
// to keep you logged in while restarting the server
server.use(express.cookieParser());
if (process.env.NODE_ENV === 'development'
&& ghost.config().hasOwnProperty('useCookieSession')
&& ghost.config().useCookieSession) {
server.use(express.cookieSession({ secret: ghost.dbHash, cookie: { maxAge: 60000000 }}));
} else {
server.use(express.session({ secret: ghost.dbHash, cookie: { maxAge: 60000000 }}));
}
//enable express csrf protection
server.use(express.csrf());
// local data
server.use(ghostLocals);
// So on every request we actually clean out reduntant passive notifications from the server side
@ -342,9 +355,8 @@ when(ghost.init()).then(function () {
server.get('/ghost/debug/', auth, admin.debug.index);
server.get('/ghost/debug/db/export/', auth, admin.debug['export']);
server.post('/ghost/debug/db/import/', auth, admin.debug['import']);
server.get('/ghost/debug/db/reset/', auth, admin.debug.reset);
// We don't want to register bodyParser globally b/c of security concerns, so use multipart only here
server.post('/ghost/upload/', admin.uploader);
server.post('/ghost/upload/', auth, admin.uploader);
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
server.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)/, function (req, res) {
res.redirect('/ghost/');

View File

@ -106,12 +106,14 @@ adminControllers = {
});
}
if (type === 'image/jpeg' || type === 'image/png' || type === 'image/gif') {
//limit uploads to type && extension
if ((type === 'image/jpeg' || type === 'image/png' || type === 'image/gif')
&& (ext === '.jpg' || ext === '.jpeg' || ext === '.png' || ext === '.gif')) {
getUniqueFileName(dir, basename, ext, null, function (filename) {
renameFile(filename);
});
} else {
res.send(404, 'Invalid filetype');
res.send(403, 'Invalid file type');
}
},
'login': function (req, res) {
@ -134,16 +136,27 @@ adminControllers = {
if (!denied) {
loginSecurity.push({ip: req.connection.remoteAddress, time: process.hrtime()[0]});
api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) {
req.session.user = user.id;
res.json(200, {redirect: req.body.redirect ? '/ghost/'
+ decodeURIComponent(req.body.redirect) : '/ghost/'});
if (process.env.NODE_ENV === 'development'
&& ghost.config().hasOwnProperty('useCookieSession')
&& ghost.config().useCookieSession) {
req.session.user = user.id;
res.json(200, {redirect: req.body.redirect ? '/ghost/'
+ decodeURIComponent(req.body.redirect) : '/ghost/'});
} else {
req.session.regenerate(function (err) {
if (!err) {
req.session.user = user.id;
res.json(200, {redirect: req.body.redirect ? '/ghost/'
+ decodeURIComponent(req.body.redirect) : '/ghost/'});
}
});
}
}, function (error) {
res.json(401, {error: error.message});
});
} else {
res.json(401, {error: 'Slow down, there are way too many login attempts!'});
}
},
changepw: function (req, res) {
api.users.changePassword({
@ -177,10 +190,23 @@ adminControllers = {
password: password
}).then(function (user) {
api.settings.edit('email', email).then(function () {
if (req.session.user === undefined) {
req.session.user = user.id;
if (process.env.NODE_ENV === 'development'
&& ghost.config().hasOwnProperty('useCookieSession')
&& ghost.config().useCookieSession) {
if (req.session.user === undefined) {
req.session.user = user.id;
}
res.json(200, {redirect: '/ghost/'});
} else {
req.session.regenerate(function (err) {
if (!err) {
if (req.session.user === undefined) {
req.session.user = user.id;
}
res.json(200, {redirect: '/ghost/'});
}
});
}
res.json(200, {redirect: '/ghost/'});
});
}).otherwise(function (error) {
res.json(401, {error: error.message});
@ -228,7 +254,13 @@ adminControllers = {
}).otherwise(errors.logAndThrowError);
},
'logout': function (req, res) {
delete req.session.user;
if (process.env.NODE_ENV === 'development'
&& ghost.config().hasOwnProperty('useCookieSession')
&& ghost.config().useCookieSession) {
delete req.session.user;
} else {
req.session.destroy();
}
var notification = {
type: 'success',
message: 'You were successfully signed out',
@ -368,7 +400,13 @@ adminControllers = {
};
return api.notifications.add(notification).then(function () {
delete req.session.user;
if (process.env.NODE_ENV === 'development'
&& ghost.config().hasOwnProperty('useCookieSession')
&& ghost.config().useCookieSession) {
delete req.session.user;
} else {
req.session.destroy();
}
res.set({
"X-Cache-Invalidate": "/*"
});
@ -384,37 +422,6 @@ adminControllers = {
id: 'per-' + (ghost.notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
res.redirect('/ghost/debug/');
});
});
},
'reset': function (req, res) {
// Grab the current version so we can get the migration
dataProvider.reset()
.then(function resetSuccess() {
var notification = {
type: 'success',
message: "Database reset. Create a new user",
status: 'persistent',
id: 'per-' + (ghost.notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
delete req.session.user;
res.set({
"X-Cache-Invalidate": "/*"
});
res.redirect('/ghost/signup/');
});
}, function resetFailure(error) {
var notification = {
type: 'error',
message: error.message || error,
status: 'persistent',
id: 'per-' + (ghost.notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
res.redirect('/ghost/debug/');
});

View File

@ -4,6 +4,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="csrf-param" content="{{csrfToken}}">
<title>{{siteTitle}}</title>
<meta name="description" content="{{siteDescription}}">

View File

@ -29,26 +29,26 @@ describe('Admin Controller', function() {
});
describe('can not upload invalid file', function() {
it('should return 404 for invalid file type', function() {
it('should return 403 for invalid file type', function() {
res.send = sinon.stub();
req.files.uploadimage.name = 'INVALID.FILE';
req.files.uploadimage.type = 'application/octet-stream'
admin.uploader(req, res);
res.send.calledOnce.should.be.true;
res.send.args[0][0].should.equal(404);
res.send.args[0][1].should.equal('Invalid filetype');
res.send.args[0][0].should.equal(403);
res.send.args[0][1].should.equal('Invalid file type');
});
});
describe('can not upload file with valid extension but invalid type', function() {
it('should return 404 for invalid file type', function() {
it('should return 403 for invalid file type', function() {
res.send = sinon.stub();
req.files.uploadimage.name = 'INVALID.jpg';
req.files.uploadimage.type = 'application/octet-stream'
admin.uploader(req, res);
res.send.calledOnce.should.be.true;
res.send.args[0][0].should.equal(404);
res.send.args[0][1].should.equal('Invalid filetype');
res.send.args[0][0].should.equal(403);
res.send.args[0][1].should.equal('Invalid file type');
});
});