0
Fork 0
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:
Sam Lord 2024-10-14 12:59:08 +01:00 committed by Kevin Ansfield
parent d90a70e43c
commit eef6c64131
8 changed files with 146 additions and 2 deletions

View file

@ -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));

View file

@ -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 || {};
}

View file

@ -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
*/

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,