diff --git a/ghost/core/core/server/web/api/endpoints/admin/routes.js b/ghost/core/core/server/web/api/endpoints/admin/routes.js index 2ba6834df7..ab76c28f7a 100644 --- a/ghost/core/core/server/web/api/endpoints/admin/routes.js +++ b/ghost/core/core/server/web/api/endpoints/admin/routes.js @@ -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)); diff --git a/ghost/core/core/server/web/shared/middleware/api/spam-prevention.js b/ghost/core/core/server/web/shared/middleware/api/spam-prevention.js index 90248b730a..1f045d590a 100644 --- a/ghost/core/core/server/web/shared/middleware/api/spam-prevention.js +++ b/ghost/core/core/server/web/shared/middleware/api/spam-prevention.js @@ -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 || {}; } diff --git a/ghost/core/core/server/web/shared/middleware/brute.js b/ghost/core/core/server/web/shared/middleware/brute.js index f64ad6f8ed..146b4b5a7c 100644 --- a/ghost/core/core/server/web/shared/middleware/brute.js +++ b/ghost/core/core/server/web/shared/middleware/brute.js @@ -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 */ diff --git a/ghost/core/core/shared/config/defaults.json b/ghost/core/core/shared/config/defaults.json index 31e5972751..b992c3f1c6 100644 --- a/ghost/core/core/shared/config/defaults.json +++ b/ghost/core/core/shared/config/defaults.json @@ -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, diff --git a/ghost/core/core/shared/config/env/config.testing-browser.json b/ghost/core/core/shared/config/env/config.testing-browser.json index 2a99eca8a4..30daa9c3f1 100644 --- a/ghost/core/core/shared/config/env/config.testing-browser.json +++ b/ghost/core/core/shared/config/env/config.testing-browser.json @@ -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, diff --git a/ghost/core/core/shared/config/env/config.testing-mysql.json b/ghost/core/core/shared/config/env/config.testing-mysql.json index 31eb21e101..84102e76ab 100644 --- a/ghost/core/core/shared/config/env/config.testing-mysql.json +++ b/ghost/core/core/shared/config/env/config.testing-mysql.json @@ -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, diff --git a/ghost/core/core/shared/config/env/config.testing.json b/ghost/core/core/shared/config/env/config.testing.json index 25ba79f6bc..c8e9d30322 100644 --- a/ghost/core/core/shared/config/env/config.testing.json +++ b/ghost/core/core/shared/config/env/config.testing.json @@ -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, diff --git a/ghost/core/test/utils/fixtures/config/defaults.json b/ghost/core/test/utils/fixtures/config/defaults.json index dba63e89d5..092269dc2e 100644 --- a/ghost/core/test/utils/fixtures/config/defaults.json +++ b/ghost/core/test/utils/fixtures/config/defaults.json @@ -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,