mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Added brute protection to 2FA endpoints
ref ENG-1629 Use separate protection for the 2 endpoints as one can resend an email, and the other is used to login -- each presents its own security challenges.
This commit is contained in:
parent
d90a70e43c
commit
eef6c64131
8 changed files with 146 additions and 2 deletions
|
@ -243,8 +243,8 @@ module.exports = function apiRoutes() {
|
|||
http(api.session.add)
|
||||
);
|
||||
router.del('/session', mw.authAdminApi, http(api.session.delete));
|
||||
router.post('/session/verify', http(api.session.sendVerification));
|
||||
router.put('/session/verify', http(api.session.verify));
|
||||
router.post('/session/verify', shared.middleware.brute.sendVerificationCode, http(api.session.sendVerification));
|
||||
router.put('/session/verify', shared.middleware.brute.userVerification, http(api.session.verify));
|
||||
|
||||
// ## Identity
|
||||
router.get('/identities', mw.authAdminApi, http(api.identities.read));
|
||||
|
|
|
@ -29,6 +29,8 @@ let spamGlobalBlock = spam.global_block || {};
|
|||
let spamGlobalReset = spam.global_reset || {};
|
||||
let spamUserReset = spam.user_reset || {};
|
||||
let spamUserLogin = spam.user_login || {};
|
||||
let spamSendVerificationCode = spam.send_verification_code || {};
|
||||
let spamUserVerification = spam.user_verification || {};
|
||||
let spamMemberLogin = spam.member_login || {};
|
||||
let spamContentApiKey = spam.content_api_key || {};
|
||||
let spamWebmentionsBlock = spam.webmentions_block || {};
|
||||
|
@ -44,6 +46,8 @@ let userLoginInstance;
|
|||
let membersAuthInstance;
|
||||
let membersAuthEnumerationInstance;
|
||||
let userResetInstance;
|
||||
let sendVerificationCodeInstance;
|
||||
let userVerificationInstance;
|
||||
let contentApiKeyInstance;
|
||||
let emailPreviewBlockInstance;
|
||||
|
||||
|
@ -308,6 +312,58 @@ const userReset = function userReset() {
|
|||
return userResetInstance;
|
||||
};
|
||||
|
||||
const userVerification = function userVerification() {
|
||||
const ExpressBrute = require('express-brute');
|
||||
const BruteKnex = require('brute-knex');
|
||||
const db = require('../../../../data/db');
|
||||
|
||||
store = store || new BruteKnex({
|
||||
tablename: 'brute',
|
||||
createTable: false,
|
||||
knex: db.knex
|
||||
});
|
||||
|
||||
userVerificationInstance = userVerificationInstance || new ExpressBrute(store,
|
||||
extend({
|
||||
attachResetToRequest: true,
|
||||
failCallback(req, res, next) {
|
||||
return next(new errors.TooManyRequestsError({
|
||||
message: tpl(messages.tooManyAttempts)
|
||||
}));
|
||||
},
|
||||
handleStoreError: handleStoreError
|
||||
}, pick(spamUserVerification, spamConfigKeys))
|
||||
);
|
||||
|
||||
return userVerificationInstance;
|
||||
};
|
||||
|
||||
const sendVerificationCode = function sendVerificationCode() {
|
||||
const ExpressBrute = require('express-brute');
|
||||
const BruteKnex = require('brute-knex');
|
||||
const db = require('../../../../data/db');
|
||||
|
||||
store = store || new BruteKnex({
|
||||
tablename: 'brute',
|
||||
createTable: false,
|
||||
knex: db.knex
|
||||
});
|
||||
|
||||
sendVerificationCodeInstance = sendVerificationCodeInstance || new ExpressBrute(store,
|
||||
extend({
|
||||
attachResetToRequest: true,
|
||||
failCallback(req, res, next) {
|
||||
return next(new errors.TooManyRequestsError({
|
||||
message: tpl(messages.tooManyAttempts)
|
||||
}));
|
||||
},
|
||||
handleStoreError: handleStoreError
|
||||
}, pick(spamSendVerificationCode, spamConfigKeys))
|
||||
);
|
||||
|
||||
return sendVerificationCodeInstance;
|
||||
};
|
||||
|
||||
// This protects a private blog from spam attacks. The defaults here allow 10 attempts per IP per hour
|
||||
// The endpoint is then locked for an hour
|
||||
const privateBlog = () => {
|
||||
|
@ -372,6 +428,8 @@ module.exports = {
|
|||
globalBlock: globalBlock,
|
||||
globalReset: globalReset,
|
||||
userLogin: userLogin,
|
||||
sendVerificationCode: sendVerificationCode,
|
||||
userVerification: userVerification,
|
||||
membersAuth: membersAuth,
|
||||
membersAuthEnumeration: membersAuthEnumeration,
|
||||
userReset: userReset,
|
||||
|
@ -389,6 +447,8 @@ module.exports = {
|
|||
membersAuthInstance = undefined;
|
||||
membersAuthEnumerationInstance = undefined;
|
||||
userResetInstance = undefined;
|
||||
sendVerificationCodeInstance = undefined;
|
||||
userVerificationInstance = undefined;
|
||||
contentApiKeyInstance = undefined;
|
||||
|
||||
spam = config.get('spam') || {};
|
||||
|
@ -397,6 +457,8 @@ module.exports = {
|
|||
spamGlobalReset = spam.global_reset || {};
|
||||
spamUserReset = spam.user_reset || {};
|
||||
spamUserLogin = spam.user_login || {};
|
||||
spamSendVerificationCode = spam.send_verification_code || {};
|
||||
spamUserVerification = spam.user_verification || {};
|
||||
spamMemberLogin = spam.member_login || {};
|
||||
spamContentApiKey = spam.content_api_key || {};
|
||||
}
|
||||
|
|
|
@ -50,6 +50,28 @@ module.exports = {
|
|||
}
|
||||
})(req, res, next);
|
||||
},
|
||||
/**
|
||||
* block per IP
|
||||
*/
|
||||
sendVerificationCode(req, res, next) {
|
||||
return spamPrevention.sendVerificationCode().getMiddleware({
|
||||
ignoreIP: false,
|
||||
key(_req, _res, _next) {
|
||||
return _next('send_verification_code');
|
||||
}
|
||||
})(req, res, next);
|
||||
},
|
||||
/**
|
||||
* block per IP
|
||||
*/
|
||||
userVerification(req, res, next) {
|
||||
return spamPrevention.userVerification().getMiddleware({
|
||||
ignoreIP: false,
|
||||
key(_req, _res, _next) {
|
||||
return _next('user_verification');
|
||||
}
|
||||
})(req, res, next);
|
||||
},
|
||||
/**
|
||||
* block per ip
|
||||
*/
|
||||
|
|
|
@ -73,6 +73,18 @@
|
|||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"user_verification": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"send_verification_code": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"global_reset": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
|
|
|
@ -29,6 +29,18 @@
|
|||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"user_verification": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"send_verification_code": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"global_reset": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
|
|
|
@ -30,6 +30,18 @@
|
|||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"user_verification": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"send_verification_code": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"global_reset": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
|
|
|
@ -29,6 +29,18 @@
|
|||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"user_verification": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"send_verification_code": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"global_reset": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
|
|
|
@ -18,6 +18,18 @@
|
|||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"user_verification": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"send_verification_code": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries": 4
|
||||
},
|
||||
"global_reset": {
|
||||
"minWait": 3600000,
|
||||
"maxWait": 3600000,
|
||||
|
|
Loading…
Add table
Reference in a new issue