0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00
ghost/core/server/middleware/api/spam-prevention.js
Katharina Irrgang 4e7779b783 🎨 remove token logic from user model (#7622)
* 🔥  remove User model functions

- validateToken
- generateToken
- resetPassword
- all this logic will re-appear in a different way

Token logic:
- was already extracted as separate PR, see https://github.com/TryGhost/Ghost/pull/7554
- we will use this logic in the controller, you will see in the next commits

Reset Password:
Was just a wrapper for calling the token logic and change the password.
We can reconsider keeping the function to call: changePassword and activate the status of the user - but i think it's fine to trigger these two actions from the controlling unit.

* 🔥  remove password reset tests from User model

- we already have unit tests for change password and the token logic
- i will re-check at the end if any test case is missing - but for now i will just burn the tests

*   add token logic to controlling unit

generateResetToken endpoint
- the only change here is instead of calling the User model to generate a token, we generate the token via utils
- we fetch the user by email, and generate a hash and return

resetPassword endpoint
- here we have changed a little bit more
- first of all: we have added the validation check if the new passwords match
- a new helper method to extract the token informations
- the brute force security check, which can be handled later from the new bruteforce middleware (see TODO)
- the actual reset function is doing the steps: load me the user, compare the token, change the password and activate the user
- we can think of wrapping these steps into a User model function
- i was not sure about it, because it is actually part of the controlling unit

[ci skip]

* 🎨  tidy up

- jscs
- jshint
- naming functions
- fixes

*   add a test for resetting the password

- there was none
- added a test to reset the password

* 🎨  add more token tests

- ensure quality
- ensure logic we had

* 🔥  remove compare new password check from User Model

- this part of controlling unit

*   compare new passwords for user endpoint

- we deleted the logic in User Model
- we are adding the logic to controlling unit

* 🐛  spam prevention forgotten can crash

- no validation happend before this middleware
- it just assumes that the root key is present
- when we work on our API, we need to ensure that
  1. pre validation happens
  2. we call middlewares
  3. ...

* 🎨  token translation key
2016-11-07 11:18:50 +00:00

129 lines
5.8 KiB
JavaScript

// # SpamPrevention Middleware
// Usage: spamPrevention
// After:
// Before:
// App: Admin|Blog|API
//
// Helpers to handle spam detection on signin, forgot password, and protected pages.
var _ = require('lodash'),
errors = require('../../errors'),
config = require('../../config'),
i18n = require('../../i18n'),
loginSecurity = [],
forgottenSecurity = [],
spamPrevention;
spamPrevention = {
/*jslint unparam:true*/
// limit signin requests to ten failed requests per IP per hour
signin: function signin(req, res, next) {
var currentTime = process.hrtime()[0],
remoteAddress = req.connection.remoteAddress,
deniedRateLimit = '',
ipCount = '',
rateSigninPeriod = config.rateSigninPeriod || 3600,
rateSigninAttempts = config.rateSigninAttempts || 10;
if (req.body.username && req.body.grant_type === 'password') {
loginSecurity.push({ip: remoteAddress, time: currentTime, email: req.body.username});
} else if (req.body.grant_type === 'refresh_token' || req.body.grant_type === 'authorization_code') {
return next();
} else {
return next(new errors.BadRequestError({message: i18n.t('errors.middleware.spamprevention.noUsername')}));
}
// filter entries that are older than rateSigninPeriod
loginSecurity = _.filter(loginSecurity, function filter(logTime) {
return (logTime.time + rateSigninPeriod > currentTime);
});
// check number of tries per IP address
ipCount = _.chain(loginSecurity).countBy('ip').value();
deniedRateLimit = (ipCount[remoteAddress] > rateSigninAttempts);
if (deniedRateLimit) {
return next(new errors.TooManyRequestsError({
message: i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateSigninPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'),
context: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.error', {rateSigninAttempts: rateSigninAttempts, rateSigninPeriod: rateSigninPeriod}),
help: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.context')
}));
}
next();
},
// limit forgotten password requests to five requests per IP per hour for different email addresses
// limit forgotten password requests to five requests per email address
// @TODO: add validation check to validation middleware
forgotten: function forgotten(req, res, next) {
if (!req.body.passwordreset) {
return next(new errors.BadRequestError({
message: i18n.t('errors.api.utils.noRootKeyProvided', {docName: 'passwordreset'})
}));
}
var currentTime = process.hrtime()[0],
remoteAddress = req.connection.remoteAddress,
rateForgottenPeriod = config.rateForgottenPeriod || 3600,
rateForgottenAttempts = config.rateForgottenAttempts || 5,
email = req.body.passwordreset[0].email,
ipCount = '',
deniedRateLimit = '',
deniedEmailRateLimit = '',
index = _.findIndex(forgottenSecurity, function findIndex(logTime) {
return (logTime.ip === remoteAddress && logTime.email === email);
});
if (email) {
if (index !== -1) {
forgottenSecurity[index].count = forgottenSecurity[index].count + 1;
} else {
forgottenSecurity.push({ip: remoteAddress, time: currentTime, email: email, count: 0});
}
} else {
return next(new errors.BadRequestError({message: i18n.t('errors.middleware.spamprevention.noEmail')}));
}
// filter entries that are older than rateForgottenPeriod
forgottenSecurity = _.filter(forgottenSecurity, function filter(logTime) {
return (logTime.time + rateForgottenPeriod > currentTime);
});
// check number of tries with different email addresses per IP
ipCount = _.chain(forgottenSecurity).countBy('ip').value();
deniedRateLimit = (ipCount[remoteAddress] > rateForgottenAttempts);
if (index !== -1) {
deniedEmailRateLimit = (forgottenSecurity[index].count > rateForgottenAttempts);
}
if (deniedEmailRateLimit) {
return next(new errors.TooManyRequestsError({
message: i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateForgottenPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'),
context: i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.error', {
rfa: rateForgottenAttempts,
rfp: rateForgottenPeriod
}),
help: i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.context')
}));
}
if (deniedRateLimit) {
return next(new errors.TooManyRequestsError({
message: i18n.t('errors.middleware.spamprevention.tooManyAttempts') + rateForgottenPeriod === 3600 ? i18n.t('errors.middleware.spamprevention.waitOneHour') : i18n.t('errors.middleware.spamprevention.tryAgainLater'),
context: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.error', {rfa: rateForgottenAttempts, rfp: rateForgottenPeriod}),
help: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.context')
}));
}
next();
},
resetCounter: function resetCounter(email) {
loginSecurity = _.filter(loginSecurity, function filter(logTime) {
return (logTime.email !== email);
});
}
};
module.exports = spamPrevention;