A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.
via npm:
$ npm install express-brute
var ExpressBrute = require('express-brute'),
var store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
var bruteforce = new ExpressBrute(store);
app.post('/auth',
bruteforce.prevent, // error 429 if we hit this route too often
function (req, res, next) {
res.send('Success!');
}
);storeAn instance ofExpressBrute.MemoryStoreor some other ExpressBrute store (see a list of known stores below).optionsfreeRetriesThe number of retires the user has before they need to start waiting (default: 2)minWaitThe initial wait time (in milliseconds) after the user runs out of retries (default: 500 milliseconds)maxWaitThe maximum amount of time (in milliseconds) between requests the user needs to wait (default: 15 minutes). The wait for a given request is determined by adding the time the user needed to wait for the previous two requests.lifetimeThe length of time (in seconds since the last request) to remember the number of requests that have been made by an IP. By default it will be set tomaxWait * the number of attempts before you hit maxWaitto discourage simply waiting for the lifetime to expire before resuming an attack. With default values this is about 6 hours.failCallbackgets called with (req,resp,next,nextValidRequestDate) when a request is rejected (default: ExpressBrute.FailForbidden)proxyDepthSpecifies how many levels of theX-Forwarded-Forheader to trust. If your web server is behind a CDN and/or load balancer you'll need to set this to however many levels of proxying it's behind to get a valid IP. Setting this too high allows attackers to get around brute force protection by spoofing theX-Forwarded-Forheader, so don't set it higher than you need to (default: 0)attachResetToRequestSpecify whether or not a simplified reset method should be attached atreq.brute.reset. The simplified method takes only a callback, and resets allExpressBrutemiddleware that was called on the current request. If multiple instances ofExpressBrutehave middleware on the same request, only those withattachResetToRequestset to true will be reset (default: true)refreshTimeoutOnRequestDefines whether the remaininglifetimeof a counter should be based on the time since the last request (true) of the time since the first request (false). Useful for allowing limits over fixed periods of time, for example a limited number of requests per day. (Default: true)
An in-memory store for persisting request counts. Don't use this in production, instead choose one of the more robust store implementations listed below.
prevent(req, res, next)Middleware that will bounce requests that happen faster than the current wait time by callingfailCallback. Equivilent togetMiddleware(null)getMiddleware(options)Generates middleware that will bounce requests with the samekeyand IP address that happen faster than the current wait time by callingfailCallback. Also attaches a function atreq.brute.resetthat can be called to reset the counter for the current ip and key. This functions the theresetinstance method, but without the need to explicitly pass theipandkeyparamterskeycan be a string or alternatively it can be afunction(req, res, next)that or callsnext, passing a string as the first parameter.failCallbackAllows you to override the value offailCallbackfor this middleware
reset(ip, key, next)Resets the wait time between requests back to its initial value. You can passnullforkeyif you want to reset a request protected byprotect.getIPFromRequest(req)Uses the current proxy trust settings to get the current IP from a request object
There are some built-in callbacks that come with BruteExpress that handle some common use cases.
ExpressBrute.FailTooManyRquestsTerminates the request and responses with a 429 (Too Many Requests) error that has aRetry-Afterheader and a JSON error message.ExpressBrute.FailForbiddenTerminates the request and responds with a 403 (Forbidden) error that has aRetry-Afterheader and a JSON error message. This is provided for compatibility with ExpressBrute versions prior to v0.5.0, for new usersFailTooManyRequestsis the preferred behavior.ExpressBrute.FailMarkSets res.nextValidRequestDate, the Retry-After header and the res.status=429, then calls next() to pass the request on to the appropriate routes.
There are a number adapters that have been written to allow ExpressBrute to be used with different persistant storage implementations, some of the ones I know about include:
require('connect-flash');
var ExpressBrute = require('express-brute'),
MemcachedStore = require('express-brute-memcached'),
moment = require('moment'),
store;
if (config.environment == 'development'){
store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
} else {
// stores state with memcached
store = new MemcachedStore(['127.0.0.1'], {
prefix: 'NoConflicts'
});
}
var failCallback = function (req, res, next, nextValidRequestDate) {
req.flash('error', "You've made too many failed attempts in a short period of time, please try again "+moment(nextValidRequestDate).fromNow());
res.redirect('/login'); // brute force protection triggered, send them back to the login page
};
// Start slowing requests after 5 failed attempts to do something for the same user
var userBruteforce = new ExpressBrute(store, {
freeRetries: 5,
proxyDepth: 1,
minWait: 5*60*1000, // 5 minutes
maxWait: 60*60*1000, // 1 hour,
failCallback: failCallback
});
// No more than 1000 login attempts per day per IP
var globalBruteforce = new ExpressBrute(store, {
freeRetries: 1000,
proxyDepth: 1,
attachResetToRequest: false,
refreshTimeoutOnRequest: false,
minWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
maxWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
lifetime: 24*60*60, // 1 day (seconds not milliseconds)
failCallback: failCallback
});
app.post('/auth',
globalBruteforce.prevent,
userBruteforce.getMiddleware({
key: function(req, res, next) {
// prevent too many attempts for the same username
next(req.body.username);
}
}),
function (req, res, next) {
if (User.isValidLogin(req.body.username, req.body.password)) { // omitted for the sake of conciseness
// reset the failure counter so next time they log in they get 5 tries again before the delays kick in
req.brute.reset(function () {
res.redirect('/'); // logged in, send them to the home page
});
} else {
res.flash('error', "Invalid username or password")
res.redirect('/login'); // bad username/password, send them back to the login page
}
}
);- NEW: Added an additional
FailTooManyRequestsfailure callback, that returns a 429 (TooManyRequests) error instead of 403 (Forbidden). This is a more accurate error status code. - NEW: All the built in failure callbacks now set the "Retry-After" header to the number of seconds until it is safe to try again. Per RFC6585
- NEW: Documentation updated to list some known store implementations.
- CHANGED: Default failure callback is now
FailTooManyRequests.FailForbiddenremains an option for backwards compatiblity. - CHANGED: ExpressBrute.MemcachedStore is no longer included by default, and is now available as a separate module (because there are multiple store options it doesn't really make sense to include one by default).
- CHANGED:
FailMarkno longer sets returns 403 Forbidden, instead does 429 TooManyRequets.
- BUG: In some cases when no callbacks were supplied memcached would drop the request. Ensure that memcached always sees a callback even if ExpressBrute isn't given one.
- NEW:
refreshTimeoutOnRequestoption that allows you to prevent the remaininglifetimefor a timer from being reset on each request (useful for implementing limits for set time frames, e.g. requests per day) - BUG: Lifetimes were not previously getting extended properly for instances of
ExpressBrute.MemoryStore
- NEW:
attachResetToRequestparameter that lets you prevent the request object being decorated - NEW:
failCallbackcan be overriden bygetMiddleware - NEW:
proxyDepthoption onExpressBrutethat specifies how many levels of theX-Forwarded-Forheader to trust (inspired by express-bouncer). - NEW:
getIPFromRequestmethod that essentially allowsresetto used in a similar ways as in v0.2.2. This also respects the newproxyDepthsetting. - CHANGED:
getMiddlewarenow takes an options object instead of the key directly.
- NEW: Support for using custom keys to group requests further (e.g. grouping login requests by username)
- NEW: Support for middleware from multiple instances of
ExpressBruteon the same route. - NEW: Tracking
lifetimenow has a reasonable default derived from the other settings for that instance ofExpressBrute - NEW: Keys are now hashed before saving to a store, to prevent really long key names and reduce the possibility of collisions.
- NEW: There is now a convience method that gets attached to
reqobject asreq.brute.reset. It takes a single parameter (a callback), and will reset all the counters used byExpressBrutemiddleware that was called for the current route. - CHANGED: Tracking
lifetimeis now specified onExpressBruteinstead ofMemcachedStore. This also means lifetime is now supported by MemoryStore. - CHANGED: The function signature for
ExpressBrute.resethas changed. It now requires an IP and key be passed instead of a request object. - IMPROVED: Efficiency for large values of
freeRetries. - BUG: Removed a small chance of incorrectly triggering brute force protection.

