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", {
|
$dropzone.find('.js-fileupload').fileupload().fileupload("option", {
|
||||||
url: '/ghost/upload/',
|
url: '/ghost/upload/',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||||
|
},
|
||||||
add: function (e, data) {
|
add: function (e, data) {
|
||||||
$dropzone.find('.js-fileupload').removeClass('right');
|
$dropzone.find('.js-fileupload').removeClass('right');
|
||||||
$dropzone.find('.js-url, button.centre').remove();
|
$dropzone.find('.js-url, button.centre').remove();
|
||||||
|
@ -23,6 +23,16 @@
|
|||||||
|
|
||||||
_.extend(Ghost, Backbone.Events);
|
_.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.init = function () {
|
||||||
Ghost.router = new Ghost.Router();
|
Ghost.router = new Ghost.Router();
|
||||||
|
|
||||||
|
@ -202,6 +202,9 @@
|
|||||||
if (self.className.indexOf('notification-persistent') !== -1) {
|
if (self.className.indexOf('notification-persistent') !== -1) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||||
|
},
|
||||||
url: '/api/v0.1/notifications/' + $(self).find('.close').data('id')
|
url: '/api/v0.1/notifications/' + $(self).find('.close').data('id')
|
||||||
}).done(function (result) {
|
}).done(function (result) {
|
||||||
bbSelf.$el.slideUp(250, function () {
|
bbSelf.$el.slideUp(250, function () {
|
||||||
@ -231,6 +234,9 @@
|
|||||||
bbSelf = this;
|
bbSelf = this;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||||
|
},
|
||||||
url: '/api/v0.1/notifications/' + $(self).data('id')
|
url: '/api/v0.1/notifications/' + $(self).data('id')
|
||||||
}).done(function (result) {
|
}).done(function (result) {
|
||||||
var height = bbSelf.$('.js-notification').outerHeight(true),
|
var height = bbSelf.$('.js-notification').outerHeight(true),
|
||||||
|
@ -33,6 +33,9 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/ghost/signin/',
|
url: '/ghost/signin/',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
@ -87,6 +90,9 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/ghost/signup/',
|
url: '/ghost/signup/',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
name: name,
|
name: name,
|
||||||
email: email,
|
email: email,
|
||||||
@ -136,6 +142,9 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/ghost/forgotten/',
|
url: '/ghost/forgotten/',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
email: email
|
email: email
|
||||||
},
|
},
|
||||||
|
@ -351,6 +351,9 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/ghost/changepw/',
|
url: '/ghost/changepw/',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
password: oldPassword,
|
password: oldPassword,
|
||||||
newpassword: newPassword,
|
newpassword: newPassword,
|
||||||
|
@ -104,6 +104,7 @@ function ghostLocals(req, res, next) {
|
|||||||
res.locals = res.locals || {};
|
res.locals = res.locals || {};
|
||||||
res.locals.version = packageInfo.version;
|
res.locals.version = packageInfo.version;
|
||||||
res.locals.path = req.path;
|
res.locals.path = req.path;
|
||||||
|
res.locals.csrfToken = req.session._csrf;
|
||||||
|
|
||||||
if (res.isAdmin) {
|
if (res.isAdmin) {
|
||||||
_.extend(res.locals, {
|
_.extend(res.locals, {
|
||||||
@ -270,9 +271,21 @@ when(ghost.init()).then(function () {
|
|||||||
server.use('/ghost/upload/', express.multipart());
|
server.use('/ghost/upload/', express.multipart());
|
||||||
server.use('/ghost/upload/', express.multipart({uploadDir: __dirname + '/content/images'}));
|
server.use('/ghost/upload/', express.multipart({uploadDir: __dirname + '/content/images'}));
|
||||||
server.use('/ghost/debug/db/import/', express.multipart());
|
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
|
// local data
|
||||||
server.use(ghostLocals);
|
server.use(ghostLocals);
|
||||||
// So on every request we actually clean out reduntant passive notifications from the server side
|
// 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/', auth, admin.debug.index);
|
||||||
server.get('/ghost/debug/db/export/', auth, admin.debug['export']);
|
server.get('/ghost/debug/db/export/', auth, admin.debug['export']);
|
||||||
server.post('/ghost/debug/db/import/', auth, admin.debug['import']);
|
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
|
// 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.
|
// 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) {
|
server.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)/, function (req, res) {
|
||||||
res.redirect('/ghost/');
|
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) {
|
getUniqueFileName(dir, basename, ext, null, function (filename) {
|
||||||
renameFile(filename);
|
renameFile(filename);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.send(404, 'Invalid filetype');
|
res.send(403, 'Invalid file type');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'login': function (req, res) {
|
'login': function (req, res) {
|
||||||
@ -134,16 +136,27 @@ adminControllers = {
|
|||||||
if (!denied) {
|
if (!denied) {
|
||||||
loginSecurity.push({ip: req.connection.remoteAddress, time: process.hrtime()[0]});
|
loginSecurity.push({ip: req.connection.remoteAddress, time: process.hrtime()[0]});
|
||||||
api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) {
|
api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) {
|
||||||
req.session.user = user.id;
|
if (process.env.NODE_ENV === 'development'
|
||||||
res.json(200, {redirect: req.body.redirect ? '/ghost/'
|
&& ghost.config().hasOwnProperty('useCookieSession')
|
||||||
+ decodeURIComponent(req.body.redirect) : '/ghost/'});
|
&& 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) {
|
}, function (error) {
|
||||||
res.json(401, {error: error.message});
|
res.json(401, {error: error.message});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.json(401, {error: 'Slow down, there are way too many login attempts!'});
|
res.json(401, {error: 'Slow down, there are way too many login attempts!'});
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
changepw: function (req, res) {
|
changepw: function (req, res) {
|
||||||
api.users.changePassword({
|
api.users.changePassword({
|
||||||
@ -177,10 +190,23 @@ adminControllers = {
|
|||||||
password: password
|
password: password
|
||||||
}).then(function (user) {
|
}).then(function (user) {
|
||||||
api.settings.edit('email', email).then(function () {
|
api.settings.edit('email', email).then(function () {
|
||||||
if (req.session.user === undefined) {
|
if (process.env.NODE_ENV === 'development'
|
||||||
req.session.user = user.id;
|
&& 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) {
|
}).otherwise(function (error) {
|
||||||
res.json(401, {error: error.message});
|
res.json(401, {error: error.message});
|
||||||
@ -228,7 +254,13 @@ adminControllers = {
|
|||||||
}).otherwise(errors.logAndThrowError);
|
}).otherwise(errors.logAndThrowError);
|
||||||
},
|
},
|
||||||
'logout': function (req, res) {
|
'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 = {
|
var notification = {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'You were successfully signed out',
|
message: 'You were successfully signed out',
|
||||||
@ -368,7 +400,13 @@ adminControllers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return api.notifications.add(notification).then(function () {
|
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({
|
res.set({
|
||||||
"X-Cache-Invalidate": "/*"
|
"X-Cache-Invalidate": "/*"
|
||||||
});
|
});
|
||||||
@ -384,37 +422,6 @@ adminControllers = {
|
|||||||
id: 'per-' + (ghost.notifications.length + 1)
|
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 () {
|
return api.notifications.add(notification).then(function () {
|
||||||
res.redirect('/ghost/debug/');
|
res.redirect('/ghost/debug/');
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="csrf-param" content="{{csrfToken}}">
|
||||||
|
|
||||||
<title>{{siteTitle}}</title>
|
<title>{{siteTitle}}</title>
|
||||||
<meta name="description" content="{{siteDescription}}">
|
<meta name="description" content="{{siteDescription}}">
|
||||||
|
@ -29,26 +29,26 @@ describe('Admin Controller', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('can not upload invalid file', 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();
|
res.send = sinon.stub();
|
||||||
req.files.uploadimage.name = 'INVALID.FILE';
|
req.files.uploadimage.name = 'INVALID.FILE';
|
||||||
req.files.uploadimage.type = 'application/octet-stream'
|
req.files.uploadimage.type = 'application/octet-stream'
|
||||||
admin.uploader(req, res);
|
admin.uploader(req, res);
|
||||||
res.send.calledOnce.should.be.true;
|
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 filetype');
|
res.send.args[0][1].should.equal('Invalid file type');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('can not upload file with valid extension but invalid type', function() {
|
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();
|
res.send = sinon.stub();
|
||||||
req.files.uploadimage.name = 'INVALID.jpg';
|
req.files.uploadimage.name = 'INVALID.jpg';
|
||||||
req.files.uploadimage.type = 'application/octet-stream'
|
req.files.uploadimage.type = 'application/octet-stream'
|
||||||
admin.uploader(req, res);
|
admin.uploader(req, res);
|
||||||
res.send.calledOnce.should.be.true;
|
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 filetype');
|
res.send.args[0][1].should.equal('Invalid file type');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user