Ghost/core/server/middleware/api/spam-prevention.js
Hannah Wolfe bcf5a1bc34
Switch to Eslint (#9197)
refs #9178

* Add eslint deps, remove old lint deps
* Add eslint config, remove old lint configs
* Config for server and tests are different
* Tweaked rules to suit us
* Fix linting in codebase - lots of indent changes.
* Fix a real broken test
2017-11-01 13:44:54 +00:00

215 lines
8.1 KiB
JavaScript

var moment = require('moment'),
errors = require('../../errors'),
config = require('../../config'),
spam = config.get('spam') || {},
_ = require('lodash'),
spamPrivateBlog = spam.private_blog || {},
spamGlobalBlock = spam.global_block || {},
spamGlobalReset = spam.global_reset || {},
spamUserReset = spam.user_reset || {},
spamUserLogin = spam.user_login || {},
i18n = require('../../i18n'),
store,
handleStoreError,
globalBlock,
globalReset,
privateBlogInstance,
globalResetInstance,
globalBlockInstance,
userLoginInstance,
userResetInstance,
privateBlog,
userLogin,
userReset,
logging = require('../../logging'),
spamConfigKeys = ['freeRetries', 'minWait', 'maxWait', 'lifetime'];
handleStoreError = function handleStoreError(err) {
var 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 is a global endpoint protection mechanism that will lock an endpoint if there are so many
// requests from a single IP
// We allow for a generous number of requests here to prevent communites on the same IP bing barred on account of a single suer
// Defaults to 50 attempts per hour and locks the endpoint for an hour
globalBlock = function globalBlock() {
var ExpressBrute = require('express-brute'),
BruteKnex = require('brute-knex'),
db = require('../../data/db');
store = store || new BruteKnex({
tablename: 'brute',
createTable: false,
knex: db.knex
});
globalBlockInstance = globalBlockInstance || new ExpressBrute(store,
_.extend({
attachResetToRequest: false,
failCallback: function (req, res, next, nextValidRequestDate) {
return next(new errors.TooManyRequestsError({
message: 'Too many attempts try again in ' + moment(nextValidRequestDate).fromNow(true),
context: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.error',
{rfa: spamGlobalBlock.freeRetries + 1 || 5, rfp: spamGlobalBlock.lifetime || 60 * 60}),
help: i18n.t('errors.middleware.spamprevention.tooManyAttempts')
}));
},
handleStoreError: handleStoreError
}, _.pick(spamGlobalBlock, spamConfigKeys))
);
return globalBlockInstance;
};
globalReset = function globalReset() {
var ExpressBrute = require('express-brute'),
BruteKnex = require('brute-knex'),
db = require('../../data/db');
store = store || new BruteKnex({
tablename: 'brute',
createTable: false,
knex: db.knex
});
globalResetInstance = globalResetInstance || new ExpressBrute(store,
_.extend({
attachResetToRequest: false,
failCallback: function (req, res, next, nextValidRequestDate) {
// TODO use i18n again
return next(new errors.TooManyRequestsError({
message: 'Too many attempts try again in ' + moment(nextValidRequestDate).fromNow(true),
context: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.error',
{rfa: spamGlobalReset.freeRetries + 1 || 5, rfp: spamGlobalReset.lifetime || 60 * 60}),
help: i18n.t('errors.middleware.spamprevention.forgottenPasswordIp.context')
}));
},
handleStoreError: handleStoreError
}, _.pick(spamGlobalReset, spamConfigKeys))
);
return globalResetInstance;
};
// 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
userLogin = function userLogin() {
var ExpressBrute = require('express-brute'),
BruteKnex = require('brute-knex'),
db = require('../../data/db');
store = store || new BruteKnex({
tablename: 'brute',
createTable: false,
knex: db.knex
});
userLoginInstance = userLoginInstance || new ExpressBrute(store,
_.extend({
attachResetToRequest: true,
failCallback: function (req, res, next, nextValidRequestDate) {
return next(new errors.TooManyRequestsError({
message: 'Too many sign-in attempts try again in ' + moment(nextValidRequestDate).fromNow(true),
// TODO add more options to i18n
context: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.context'),
help: i18n.t('errors.middleware.spamprevention.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
userReset = function userReset() {
var ExpressBrute = require('express-brute'),
BruteKnex = require('brute-knex'),
db = require('../../data/db');
store = store || new BruteKnex({
tablename: 'brute',
createTable: false,
knex: db.knex
});
userResetInstance = userResetInstance || new ExpressBrute(store,
_.extend({
attachResetToRequest: true,
failCallback: function (req, res, next, nextValidRequestDate) {
return next(new errors.TooManyRequestsError({
message: 'Too many password reset attempts try again in ' + moment(nextValidRequestDate).fromNow(true),
context: i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.error',
{rfa: spamUserReset.freeRetries + 1 || 5, rfp: spamUserReset.lifetime || 60 * 60}),
help: i18n.t('errors.middleware.spamprevention.forgottenPasswordEmail.context')
}));
},
handleStoreError: handleStoreError
}, _.pick(spamUserReset, spamConfigKeys))
);
return userResetInstance;
};
// 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
privateBlog = function privateBlog() {
var ExpressBrute = require('express-brute'),
BruteKnex = require('brute-knex'),
db = require('../../data/db');
store = store || new BruteKnex({
tablename: 'brute',
createTable: false,
knex: db.knex
});
privateBlogInstance = privateBlogInstance || new ExpressBrute(store,
_.extend({
attachResetToRequest: false,
failCallback: function (req, res, next, nextValidRequestDate) {
logging.error(new errors.GhostError({
message: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.error',
{rfa: spamPrivateBlog.freeRetries + 1 || 5, rfp: spamPrivateBlog.lifetime || 60 * 60}),
context: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.context')
}));
return next(new errors.GhostError({
message: 'Too many private sign-in attempts try again in ' + moment(nextValidRequestDate).fromNow(true)
}));
},
handleStoreError: handleStoreError
}, _.pick(spamPrivateBlog, spamConfigKeys))
);
return privateBlogInstance;
};
module.exports = {
globalBlock: globalBlock,
globalReset: globalReset,
userLogin: userLogin,
userReset: userReset,
privateBlog: privateBlog
};