mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 19:33:02 +03:00
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:
parent
b544ee7ed6
commit
90176e1f40
@ -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();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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/');
|
||||
|
@ -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) {
|
||||
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 (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/'});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}).otherwise(function (error) {
|
||||
res.json(401, {error: error.message});
|
||||
@ -228,7 +254,13 @@ adminControllers = {
|
||||
}).otherwise(errors.logAndThrowError);
|
||||
},
|
||||
'logout': function (req, res) {
|
||||
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 () {
|
||||
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/');
|
||||
});
|
||||
|
@ -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}}">
|
||||
|
@ -29,25 +29,25 @@ 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][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][0].should.equal(403);
|
||||
res.send.args[0][1].should.equal('Invalid file type');
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user