UNPKG

ghost

Version:

The professional publishing platform

466 lines (407 loc) 16.8 kB
const moment = require('moment'); const extend = require('lodash/extend'); const pick = require('lodash/pick'); const errors = require('@tryghost/errors'); const config = require('../../../../../shared/config'); const tpl = require('@tryghost/tpl'); const logging = require('@tryghost/logging'); let spam = config.get('spam') || {}; const messages = { forgottenPasswordEmail: { error: 'Only {rfa} forgotten password attempts per email every {rfp} seconds.', context: 'Forgotten password reset attempt failed' }, forgottenPasswordIp: { error: 'Only {rfa} tries per IP address every {rfp} seconds.', context: 'Forgotten password reset attempt failed' }, tooManySigninAttempts: { error: 'Only {rateSigninAttempts} tries per IP address every {rateSigninPeriod} seconds.', context: 'Too many login attempts.' }, tooManyAttempts: 'Too many attempts.', webmentionsBlock: 'Too many mention attempts', emailPreviewBlock: 'Only 10 test emails can be sent per hour' }; let spamPrivateBlock = spam.private_block || {}; 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 || {}; let spamEmailPreviewBlock = spam.email_preview_block || {}; let store; let memoryStore; let privateBlogInstance; let globalResetInstance; let globalBlockInstance; let webmentionsBlockInstance; let userLoginInstance; let membersAuthInstance; let membersAuthEnumerationInstance; let userResetInstance; let sendVerificationCodeInstance; let userVerificationInstance; let contentApiKeyInstance; let emailPreviewBlockInstance; const spamConfigKeys = ['freeRetries', 'minWait', 'maxWait', 'lifetime']; const handleStoreError = (err) => { const customError = new errors.InternalServerError({ message: 'Unknown error', err: err.parent ? err.parent : err }); // see https://github.com/AdamPflug/express-brute/issues/45 // express-brute does not always forward a callback // we are using reset as synchronous call, so we have to log the error if it occurs // there is no way to try/catch, because the reset operation happens asynchronous if (!err.next) { logging.error(err); return; } err.next(customError); }; // This locks a single endpoint based on excessive requests from an IP. // Currently only used for auth type methods. // We allow for a generous number of requests here to prevent communites on the same IP bing barred on account of a single user // Defaults to 50 attempts per hour and locks the endpoint for an hour const globalBlock = () => { 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 }); globalBlockInstance = globalBlockInstance || new ExpressBrute(store, extend({ attachResetToRequest: false, failCallback(req, res, next, nextValidRequestDate) { return next(new errors.TooManyRequestsError({ message: `Too many attempts try again in ${moment(nextValidRequestDate).fromNow(true)}`, context: tpl(messages.forgottenPasswordIp.error, {rfa: spamGlobalBlock.freeRetries + 1 || 5, rfp: spamGlobalBlock.lifetime || 60 * 60}), help: tpl(messages.tooManyAttempts) })); }, handleStoreError: handleStoreError }, pick(spamGlobalBlock, spamConfigKeys)) ); return globalBlockInstance; }; const globalReset = () => { 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 }); globalResetInstance = globalResetInstance || new ExpressBrute(store, extend({ attachResetToRequest: false, failCallback(req, res, next, nextValidRequestDate) { return next(new errors.TooManyRequestsError({ message: `Too many attempts try again in ${moment(nextValidRequestDate).fromNow(true)}`, context: tpl(messages.forgottenPasswordIp.error, {rfa: spamGlobalReset.freeRetries + 1 || 5, rfp: spamGlobalReset.lifetime || 60 * 60}), help: tpl(messages.forgottenPasswordIp.context) })); }, handleStoreError: handleStoreError }, pick(spamGlobalReset, spamConfigKeys)) ); return globalResetInstance; }; const webmentionsBlock = () => { 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 }); webmentionsBlockInstance = webmentionsBlockInstance || new ExpressBrute(store, extend({ attachResetToRequest: false, failCallback(req, res, next) { return next(new errors.TooManyRequestsError({ message: messages.webmentionsBlock })); }, handleStoreError: handleStoreError }, pick(spamWebmentionsBlock, spamConfigKeys)) ); return webmentionsBlockInstance; }; const emailPreviewBlock = () => { 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 }); emailPreviewBlockInstance = emailPreviewBlockInstance || new ExpressBrute(store, extend({ attachResetToRequest: false, failCallback(req, res, next) { return next(new errors.TooManyRequestsError({ message: messages.emailPreviewBlock })); }, handleStoreError: handleStoreError }, pick(spamEmailPreviewBlock, spamConfigKeys)) ); return emailPreviewBlockInstance; }; const membersAuth = () => { 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 }); if (!membersAuthInstance) { membersAuthInstance = new ExpressBrute(store, extend({ attachResetToRequest: true, failCallback(req, res, next, nextValidRequestDate) { return next(new errors.TooManyRequestsError({ message: `Too many sign-in attempts try again in ${moment(nextValidRequestDate).fromNow(true)}`, context: tpl(messages.tooManySigninAttempts.context), help: tpl(messages.tooManySigninAttempts.context) })); }, handleStoreError: handleStoreError }, pick(spamUserLogin, spamConfigKeys)) ); } return membersAuthInstance; }; /** * This one should have higher limits because it checks across all email addresses */ const membersAuthEnumeration = () => { 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 }); if (!membersAuthEnumerationInstance) { membersAuthEnumerationInstance = new ExpressBrute(store, extend({ attachResetToRequest: true, failCallback(req, res, next, nextValidRequestDate) { return next(new errors.TooManyRequestsError({ message: `Too many different sign-in attempts, try again in ${moment(nextValidRequestDate).fromNow(true)}`, context: tpl(messages.tooManySigninAttempts.context), help: tpl(messages.tooManySigninAttempts.context) })); }, handleStoreError: handleStoreError }, pick(spamMemberLogin, spamConfigKeys)) ); } return membersAuthEnumerationInstance; }; // Stops login attempts for a user+IP pair with an increasing time period starting from 10 minutes // and rising to a week in a fibonnaci sequence // The user+IP count is reset when on successful login // Default value of 5 attempts per user+IP pair const userLogin = () => { 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 }); userLoginInstance = userLoginInstance || new ExpressBrute(store, extend({ attachResetToRequest: true, failCallback(req, res, next, nextValidRequestDate) { return next(new errors.TooManyRequestsError({ message: `Too many login attempts. Please wait ${moment(nextValidRequestDate).fromNow(true)} before trying again, or reset your password.`, context: tpl(messages.tooManySigninAttempts.context), help: tpl(messages.tooManySigninAttempts.context) })); }, handleStoreError: handleStoreError }, pick(spamUserLogin, spamConfigKeys)) ); return userLoginInstance; }; // Stop password reset requests when there are (freeRetries + 1) requests per lifetime per email // Defaults here are 5 attempts per hour for a user+IP pair // The endpoint is then locked for an hour const userReset = function userReset() { 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 }); userResetInstance = userResetInstance || new ExpressBrute(store, extend({ attachResetToRequest: true, failCallback(req, res, next, nextValidRequestDate) { return next(new errors.TooManyRequestsError({ message: `Too many password reset attempts try again in ${moment(nextValidRequestDate).fromNow(true)}`, context: tpl(messages.forgottenPasswordEmail.error, {rfa: spamUserReset.freeRetries + 1 || 5, rfp: spamUserReset.lifetime || 60 * 60}), help: tpl(messages.forgottenPasswordEmail.context) })); }, handleStoreError: handleStoreError }, pick(spamUserReset, spamConfigKeys)) ); 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 = () => { 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 }); privateBlogInstance = privateBlogInstance || new ExpressBrute(store, extend({ attachResetToRequest: false, failCallback(req, res, next, nextValidRequestDate) { logging.error(new errors.TooManyRequestsError({ message: tpl(messages.tooManySigninAttempts.error, { rateSigninAttempts: spamPrivateBlock.freeRetries + 1 || 5, rateSigninPeriod: spamPrivateBlock.lifetime || 60 * 60 }), context: tpl(messages.tooManySigninAttempts.context) })); return next(new errors.TooManyRequestsError({ message: `Too many private sign-in attempts try again in ${moment(nextValidRequestDate).fromNow(true)}` })); }, handleStoreError: handleStoreError }, pick(spamPrivateBlock, spamConfigKeys)) ); return privateBlogInstance; }; const contentApiKey = () => { const ExpressBrute = require('express-brute'); memoryStore = memoryStore || new ExpressBrute.MemoryStore(); contentApiKeyInstance = contentApiKeyInstance || new ExpressBrute(memoryStore, extend({ attachResetToRequest: true, failCallback(req, res, next) { const err = new errors.TooManyRequestsError({ message: tpl(messages.tooManyAttempts) }); logging.error(err); return next(err); }, handleStoreError: handleStoreError }, pick(spamContentApiKey, spamConfigKeys)) ); return contentApiKeyInstance; }; module.exports = { globalBlock: globalBlock, globalReset: globalReset, userLogin: userLogin, sendVerificationCode: sendVerificationCode, userVerification: userVerification, membersAuth: membersAuth, membersAuthEnumeration: membersAuthEnumeration, userReset: userReset, privateBlog: privateBlog, contentApiKey: contentApiKey, webmentionsBlock: webmentionsBlock, emailPreviewBlock: emailPreviewBlock, reset: () => { store = undefined; memoryStore = undefined; privateBlogInstance = undefined; globalResetInstance = undefined; globalBlockInstance = undefined; userLoginInstance = undefined; membersAuthInstance = undefined; membersAuthEnumerationInstance = undefined; userResetInstance = undefined; sendVerificationCodeInstance = undefined; userVerificationInstance = undefined; contentApiKeyInstance = undefined; spam = config.get('spam') || {}; spamPrivateBlock = spam.private_block || {}; spamGlobalBlock = spam.global_block || {}; 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 || {}; } };