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!');
}
);
store
An instance of ExpressBrute.MemoryStore
or some other ExpressBrute store (see a list of known stores below).options
freeRetries
The number of retires the user has before they need to start waiting (default: 2)minWait
The initial wait time (in milliseconds) after the user runs out of retries (default: 500 milliseconds)maxWait
The 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.lifetime
The 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 to maxWait * the number of attempts before you hit maxWait
to discourage simply waiting for the lifetime to expire before resuming an attack. With default values this is about 6 hours.failCallback
Gets called with (req
, resp
, next
, nextValidRequestDate
) when a request is rejected (default: ExpressBrute.FailForbidden)attachResetToRequest
Specify whether or not a simplified reset method should be attached at req.brute.reset
. The simplified method takes only a callback, and resets all ExpressBrute
middleware that was called on the current request. If multiple instances of ExpressBrute
have middleware on the same request, only those with attachResetToRequest
set to true will be reset (default: true)refreshTimeoutOnRequest
Defines whether the lifetime
counts from the time of the last request that ExpressBrute didn't prevent for a given IP (true) or from of that IP's first request (false). Useful for allowing limits over fixed periods of time, for example: a limited number of requests per day. (Default: true). More infohandleStoreError
Gets called whenever an error occurs with the persistent store from which ExpressBrute cannot recover. It is passed an object containing the properties message
(a description of the message), parent
(the error raised by the session store), and [key
, ip
] or [req
, res
, next
] depending on whether or the error occurs during reset
or in the middleware itself.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.
ExpressBrute
Instance Methodsprevent(req, res, next)
Middleware that will bounce requests that happen faster than
the current wait time by calling failCallback
. Equivilent to getMiddleware(null)
getMiddleware(options)
Generates middleware that will bounce requests with the same key
and IP address
that happen faster than the current wait time by calling failCallback
.
Also attaches a function at req.brute.reset
that can be called to reset the
counter for the current ip and key. This functions as the reset
instance method,
but without the need to explicitly pass the ip
and key
paramters
key
can be a string or alternatively it can be a function(req, res, next)
that or calls next
, passing a string as the first parameter.failCallback
Allows you to override the value of failCallback
for this middlewareignoreIP
Disregard IP address when matching requests if set to true
. Defaults to false
.reset(ip, key, next)
Resets the wait time between requests back to its initial value. You can pass null
for key
if you want to reset a request protected by protect
.There are some built-in callbacks that come with BruteExpress that handle some common use cases.
ExpressBrute.FailTooManyRquests
Terminates the request and responses with a 429 (Too Many Requests) error that has a Retry-After
header and a JSON error message.ExpressBrute.FailForbidden
Terminates the request and responds with a 403 (Forbidden) error that has a Retry-After
header and a JSON error message. This is provided for compatibility with ExpressBrute versions prior to v0.5.0, for new users FailTooManyRequests
is the preferred behavior.ExpressBrute.FailMark
Sets res.nextValidRequestDate, the Retry-After header and the res.status=429, then calls next() to pass the request on to the appropriate routes.ExpressBrute
storesThere are a number adapters that have been written to allow ExpressBrute to be used with different persistent storage implementations, some of the ones I know about include:
If you write your own store and want me to add it to the list, just drop me an email or create an issue.
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
};
var handleStoreError = handleStoreError: function (error) {
log.error(error); // log this error so we can figure out what went wrong
// cause node to exit, hopefully restarting the process fixes the problem
throw {
message: error.message,
parent: error.parent
};
}
// Start slowing requests after 5 failed attempts to do something for the same user
var userBruteforce = new ExpressBrute(store, {
freeRetries: 5,
minWait: 5*60*1000, // 5 minutes
maxWait: 60*60*1000, // 1 hour,
failCallback: failCallback,
handleStoreError: handleStoreError
}
});
// No more than 1000 login attempts per day per IP
var globalBruteforce = new ExpressBrute(store, {
freeRetries: 1000,
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,
handleStoreError: handleStoreError
});
app.set('trust proxy', 1); // Don't set to "true", it's not secure. Make sure it matches your environment
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
}
}
);
Express
4.x as a peer dependency.proxyDepth
option on ExpressBrute
has been removed. Use app.set('trust proxy', x)
from Express 4 instead. More InfogetIPFromRequest(req)
has been removed from instances, use req.ip
instead..reset
callbacks are now always called asyncronously, regardless of the implementation of the store (particularly effects MemoryStore
).handleStoreError
option to allow more customizable handling of errors that are thrown by the persistent store. Default behavior is to throw the errors as an exception - there is nothing ExpressBrute can do to recover.FailTooManyRequests
failure callback, that returns a 429 (TooManyRequests) error instead of 403 (Forbidden). This is a more accurate error status code.FailTooManyRequests
. FailForbidden
remains an option for backwards compatiblity.FailMark
no longer sets returns 403 Forbidden, instead does 429 TooManyRequets.refreshTimeoutOnRequest
option that allows you to prevent the remaining lifetime
for a timer from being reset on each request (useful for implementing limits for set time frames, e.g. requests per day)ExpressBrute.MemoryStore
attachResetToRequest
parameter that lets you prevent the request object being decoratedfailCallback
can be overriden by getMiddleware
proxyDepth
option on ExpressBrute
that specifies how many levels of the X-Forwarded-For
header to trust (inspired by express-bouncer).getIPFromRequest
method that essentially allows reset
to used in a similar ways as in v0.2.2. This also respects the new proxyDepth
setting.getMiddleware
now takes an options object instead of the key directly.ExpressBrute
on the same route.lifetime
now has a reasonable default derived from the other settings for that instance of ExpressBrute
req
object as req.brute.reset
. It takes a single parameter (a callback), and will reset all the counters used by ExpressBrute
middleware that was called for the current route.lifetime
is now specified on ExpressBrute
instead of MemcachedStore
. This also means lifetime is now supported by MemoryStore.ExpressBrute.reset
has changed. It now requires an IP and key be passed instead of a request object.freeRetries
.