├── .coveralls.yml ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── AbstractClientStore.js └── MemoryStore.js ├── mock └── ResponseMock.js ├── package.json └── spec └── ExpessBrute.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: YVUmB2KqCrfPQhB2JLBqP969by9tSiuhA -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | coverage 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : false, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : false, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 4, // {int} Number of spaces to use for indentation 16 | "latedef" : true, // true: Require variables/functions to be defined before being used 17 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` & `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // true: Require all defined variables be used 30 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 31 | "maxparams" : false, // {int} Max number of formal params allowed per function 32 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 33 | "maxstatements" : false, // {int} Max number statements per function 34 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 35 | "maxlen" : false, // {int} Max number of characters per line 36 | 37 | // Relaxing 38 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 39 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 40 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 41 | "eqnull" : false, // true: Tolerate use of `== null` 42 | "es5" : true, // true: Allow ES5 syntax (ex: getters and setters) 43 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 44 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 45 | // (ex: `for each`, multiple try/catch, function expression…) 46 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 47 | "expr" : true, // true: Tolerate `ExpressionStatement` as Programs 48 | "funcscope" : false, // true: Tolerate defining variables inside control statements 49 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 50 | "iterator" : false, // true: Tolerate using the `__iterator__` property 51 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 52 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 53 | "laxcomma" : false, // true: Tolerate comma-first style coding 54 | "loopfunc" : false, // true: Tolerate functions being defined in loops 55 | "multistr" : false, // true: Tolerate multi-line strings 56 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 57 | "notypeof" : false, // true: Tolerate invalid typeof operator values 58 | "proto" : false, // true: Tolerate using the `__proto__` property 59 | "scripturl" : false, // true: Tolerate script-targeted URLs 60 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 61 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 62 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 63 | "validthis" : false, // true: Tolerate using this in a non-constructor function 64 | 65 | // Environments 66 | "browser" : true, // Web Browser (window, document, etc) 67 | "browserify" : false, // Browserify (node.js code in the browser) 68 | "couch" : false, // CouchDB 69 | "devel" : true, // Development/debugging (alert, confirm, etc) 70 | "dojo" : false, // Dojo Toolkit 71 | "jasmine" : false, // Jasmine 72 | "jquery" : false, // jQuery 73 | "mocha" : true, // Mocha 74 | "mootools" : false, // MooTools 75 | "node" : true, // Node.js 76 | "nonstandard" : true, // Widely adopted globals (escape, unescape, etc) 77 | "prototypejs" : false, // Prototype and Scriptaculous 78 | "qunit" : false, // QUnit 79 | "rhino" : false, // Rhino 80 | "shelljs" : false, // ShellJS 81 | "worker" : false, // Web Workers 82 | "wsh" : false, // Windows Scripting Host 83 | "yui" : false, // Yahoo User Interface 84 | 85 | // Custom Globals 86 | "globals" : { // additional predefined global variables 87 | "Promise": true // so we dont get errors from redefining Promise 88 | } 89 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | script: 4 | - ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 5 | node_js: 6 | - 0.10 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Adam Pflug 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | express-brute 2 | ============= 3 | [![NPM Version](https://badge.fury.io/js/express-brute.png)](http://badge.fury.io/js/express-brute) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/express-brute.svg?maxAge=2592000)](http://badge.fury.io/js/express-brute) 5 | [![Build Status](https://img.shields.io/travis/AdamPflug/express-brute.svg?maxAge=2592000)](https://travis-ci.org/AdamPflug/express-brute) 6 | [![Coverage Status](https://img.shields.io/coveralls/AdamPflug/express-brute.svg?maxAge=2592000)](http://coveralls.io/github/AdamPflug/express-brute?branch=master) 7 | [![Dependency Status](https://img.shields.io/david/AdamPflug/express-brute.svg?maxAge=2592000)](https://david-dm.org/adampflug/express-brute) 8 | 9 | A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence. 10 | 11 | Installation 12 | ------------ 13 | via npm: 14 | 15 | $ npm install express-brute 16 | 17 | A Simple Example 18 | ---------------- 19 | ``` js 20 | var ExpressBrute = require('express-brute'); 21 | 22 | // stores state locally, don't use this in production 23 | var store = new ExpressBrute.MemoryStore(); 24 | var bruteforce = new ExpressBrute(store); 25 | 26 | app.post('/auth', 27 | bruteforce.prevent, // error 429 if we hit this route too often 28 | function (req, res, next) { 29 | res.send('Success!'); 30 | } 31 | ); 32 | ``` 33 | 34 | Classes 35 | ------- 36 | ### ExpressBrute(store, options) 37 | - `store` An instance of `ExpressBrute.MemoryStore` or some other ExpressBrute store (see a list of known stores below). 38 | - `options` 39 | - `freeRetries` The number of retries the user has before they need to start waiting (default: 2) 40 | - `minWait` The initial wait time (in milliseconds) after the user runs out of retries (default: 500 milliseconds) 41 | - `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. 42 | - `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. 43 | - `failCallback` Gets called with (`req`, `resp`, `next`, `nextValidRequestDate`) when a request is rejected (default: ExpressBrute.FailForbidden) 44 | - `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) 45 | - `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 info](https://github.com/AdamPflug/express-brute/issues/14) 46 | - `handleStoreError` 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. 47 | 48 | ### ExpressBrute.MemoryStore() 49 | 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. 50 | 51 | 52 | `ExpressBrute` Instance Methods 53 | ------------------------------- 54 | - `prevent(req, res, next)` Middleware that will bounce requests that happen faster than 55 | the current wait time by calling `failCallback`. Equivilent to `getMiddleware(null)` 56 | - `getMiddleware(options)` Generates middleware that will bounce requests with the same `key` and IP address 57 | that happen faster than the current wait time by calling `failCallback`. 58 | Also attaches a function at `req.brute.reset` that can be called to reset the 59 | counter for the current ip and key. This functions as the `reset` instance method, 60 | but without the need to explicitly pass the `ip` and `key` paramters 61 | - `key` can be a string or alternatively it can be a `function(req, res, next)` 62 | that calls `next`, passing a string as the first parameter. 63 | - `failCallback` Allows you to override the value of `failCallback` for this middleware 64 | - `ignoreIP` Disregard IP address when matching requests if set to `true`. Defaults to `false`. 65 | - `reset(ip, key, next)` Resets the wait time between requests back to its initial value. You can pass `null` 66 | for `key` if you want to reset a request protected by `prevent`. 67 | 68 | Built-in Failure Callbacks 69 | --------------------------- 70 | There are some built-in callbacks that come with BruteExpress that handle some common use cases. 71 | - `ExpressBrute.FailTooManyRequests` Terminates the request and responses with a 429 (Too Many Requests) error that has a `Retry-After` header and a JSON error message. 72 | - `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. 73 | - `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. 74 | 75 | `ExpressBrute` stores 76 | --------------------- 77 | There 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: 78 | - [Memcached](https://github.com/AdamPflug/express-brute-memcached) 79 | - [Redis](https://github.com/AdamPflug/express-brute-redis) 80 | - [MongoDB](https://github.com/auth0/express-brute-mongo) 81 | - [Mongoose](https://github.com/cbargren/express-brute-mongoose) 82 | - [Sequelize (SQL)](https://github.com/maddy2get/express-brute-sequelize) 83 | - [Knex.js (SQL)](https://github.com/llambda/brute-knex) 84 | - [RethinkDB](https://github.com/llambda/brute-rethinkdb) 85 | - [Loki.js](https://github.com/Requarks/express-brute-loki) 86 | - [nedb](https://github.com/natsukagami/express-brute-nedb) 87 | - [PostgreSQL](https://github.com/dmfay/express-brute-pg) 88 | - [Couchbase](https://github.com/kvaillant/express-brute-couchbase) 89 | 90 | If you write your own store and want me to add it to the list, just drop me an [email](mailto:adam.pflug@gmail.com) or [create an issue](https://github.com/AdamPflug/express-brute/issues/new). 91 | 92 | A More Complex Example 93 | ---------------------- 94 | ``` js 95 | require('connect-flash'); 96 | var ExpressBrute = require('express-brute'), 97 | MemcachedStore = require('express-brute-memcached'), 98 | moment = require('moment'), 99 | store; 100 | 101 | if (config.environment == 'development'){ 102 | store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production 103 | } else { 104 | // stores state with memcached 105 | store = new MemcachedStore(['127.0.0.1'], { 106 | prefix: 'NoConflicts' 107 | }); 108 | } 109 | 110 | var failCallback = function (req, res, next, nextValidRequestDate) { 111 | req.flash('error', "You've made too many failed attempts in a short period of time, please try again "+moment(nextValidRequestDate).fromNow()); 112 | res.redirect('/login'); // brute force protection triggered, send them back to the login page 113 | }; 114 | var handleStoreError = function (error) { 115 | log.error(error); // log this error so we can figure out what went wrong 116 | // cause node to exit, hopefully restarting the process fixes the problem 117 | throw { 118 | message: error.message, 119 | parent: error.parent 120 | }; 121 | } 122 | // Start slowing requests after 5 failed attempts to do something for the same user 123 | var userBruteforce = new ExpressBrute(store, { 124 | freeRetries: 5, 125 | minWait: 5*60*1000, // 5 minutes 126 | maxWait: 60*60*1000, // 1 hour, 127 | failCallback: failCallback, 128 | handleStoreError: handleStoreError 129 | }); 130 | // No more than 1000 login attempts per day per IP 131 | var globalBruteforce = new ExpressBrute(store, { 132 | freeRetries: 1000, 133 | attachResetToRequest: false, 134 | refreshTimeoutOnRequest: false, 135 | minWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time) 136 | maxWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time) 137 | lifetime: 24*60*60, // 1 day (seconds not milliseconds) 138 | failCallback: failCallback, 139 | handleStoreError: handleStoreError 140 | }); 141 | 142 | app.set('trust proxy', 1); // Don't set to "true", it's not secure. Make sure it matches your environment 143 | app.post('/auth', 144 | globalBruteforce.prevent, 145 | userBruteforce.getMiddleware({ 146 | key: function(req, res, next) { 147 | // prevent too many attempts for the same username 148 | next(req.body.username); 149 | } 150 | }), 151 | function (req, res, next) { 152 | if (User.isValidLogin(req.body.username, req.body.password)) { // omitted for the sake of conciseness 153 | // reset the failure counter so next time they log in they get 5 tries again before the delays kick in 154 | req.brute.reset(function () { 155 | res.redirect('/'); // logged in, send them to the home page 156 | }); 157 | } else { 158 | res.flash('error', "Invalid username or password") 159 | res.redirect('/login'); // bad username/password, send them back to the login page 160 | } 161 | } 162 | ); 163 | ``` 164 | 165 | Behind Proxy Servers 166 | -------------------- 167 | If your application is behind a proxy (Apache, Nginx, load balancer, CDN, etc) you should not forget set the **trust proxy** param as appropriate for your Express application. For example: 168 | ```javascript 169 | app.set('trust proxy', 1); 170 | ``` 171 | Please note: don't use the value `true` because it tells express to trust the whole `X-Forwarded-For` chain, which could allow an attacker to bypass the express brute protections by spoofing source ips. The easiest solution is probably to set your proxy depth appropriately, but for more information on other options see [Express' behind proxies guide](https://expressjs.com/en/guide/behind-proxies.html) 172 | 173 | Changelog 174 | --------- 175 | ### v1.0.1 176 | * BUG: Fixed an edge case where freeretries weren't being respected if app servers had slightly different times 177 | 178 | ### v1.0.0 179 | * NEW: Updated to use `Express` 4.x as a peer dependency. 180 | * REMOVED: `proxyDepth` option on `ExpressBrute` has been removed. Use `app.set('trust proxy', x)` from Express 4 instead. [More Info](http://expressjs.com/en/guide/behind-proxies.html) 181 | * REMOVED: `getIPFromRequest(req)` has been removed from instances, use `req.ip` instead. 182 | 183 | ### v0.6.0 184 | * NEW: Added new ignoreIP option. (Thanks [Magnitus-](https://github.com/Magnitus-)!) 185 | * CHANGED: `.reset` callbacks are now always called asyncronously, regardless of the implementation of the store (particularly effects `MemoryStore`). 186 | * CHANGED: Unit tests have been converted from Jasmine to Mocha/Chai/Sinon 187 | * BUG: Fixed a crash when .reset was called without a callback function 188 | 189 | ### v0.5.3 190 | * NEW: Added the `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. 191 | * CHANGED: Errors thrown as a result of errors raised by the store now include the store's error as well, for debugging purposes. 192 | 193 | ### v0.5.2 194 | * CHANGED: Stopped using res.send(status, body), as it is deprecated in express 4.x. Instead call res.status and res.send separately (Thanks marinewater!) 195 | 196 | ### v0.5.1 197 | * BUG: When setting proxyDepth to 1, ips is never populated with proxied X-Forwarded-For IP. 198 | 199 | ### v0.5.0 200 | * NEW: Added an additional `FailTooManyRequests` failure callback, that returns a 429 (TooManyRequests) error instead of 403 (Forbidden). This is a more accurate error status code. 201 | * 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](https://tools.ietf.org/html/rfc6585#section-4) 202 | * NEW: Documentation updated to list some known store implementations. 203 | * CHANGED: Default failure callback is now `FailTooManyRequests`. `FailForbidden` remains an option for backwards compatiblity. 204 | * 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). 205 | * CHANGED: `FailMark` no longer sets returns 403 Forbidden, instead does 429 TooManyRequets. 206 | 207 | ### v0.4.2 208 | * 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. 209 | 210 | ### v0.4.1 211 | * NEW: `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) 212 | * BUG: Lifetimes were not previously getting extended properly for instances of `ExpressBrute.MemoryStore` 213 | 214 | ### v0.4.0 215 | * NEW: `attachResetToRequest` parameter that lets you prevent the request object being decorated 216 | * NEW: `failCallback` can be overriden by `getMiddleware` 217 | * NEW: `proxyDepth` option on `ExpressBrute` that specifies how many levels of the `X-Forwarded-For` header to trust (inspired by [express-bouncer](https://github.com/dkrutsko/express-bouncer/)). 218 | * NEW: `getIPFromRequest` method that essentially allows `reset` to used in a similar ways as in v0.2.2. This also respects the new `proxyDepth` setting. 219 | * CHANGED: `getMiddleware` now takes an options object instead of the key directly. 220 | 221 | ### v0.3.0 222 | * NEW: Support for using custom keys to group requests further (e.g. grouping login requests by username) 223 | * NEW: Support for middleware from multiple instances of `ExpressBrute` on the same route. 224 | * NEW: Tracking `lifetime` now has a reasonable default derived from the other settings for that instance of `ExpressBrute` 225 | * NEW: Keys are now hashed before saving to a store, to prevent really long key names and reduce the possibility of collisions. 226 | * NEW: There is now a convience method that gets attached to `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. 227 | * CHANGED: Tracking `lifetime` is now specified on `ExpressBrute` instead of `MemcachedStore`. This also means lifetime is now supported by MemoryStore. 228 | * CHANGED: The function signature for `ExpressBrute.reset` has changed. It now requires an IP and key be passed instead of a request object. 229 | * IMPROVED: Efficiency for large values of `freeRetries`. 230 | * BUG: Removed a small chance of incorrectly triggering brute force protection. 231 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var crypto = require('crypto'); 3 | 4 | var ExpressBrute = module.exports = function (store, options) { 5 | var i; 6 | ExpressBrute.instanceCount++; 7 | this.name = "brute"+ExpressBrute.instanceCount; 8 | _.bindAll(this, 'reset', 'getMiddleware'); 9 | 10 | // set options 11 | this.options = _.extend({}, ExpressBrute.defaults, options); 12 | if (this.options.minWait < 1) { 13 | this.options.minWait = 1; 14 | } 15 | this.store = store; 16 | 17 | // build delays array 18 | this.delays = [this.options.minWait]; 19 | while(this.delays[this.delays.length-1] < this.options.maxWait) { 20 | var nextNum = this.delays[this.delays.length-1] + (this.delays.length > 1 ? this.delays[this.delays.length-2] : 0); 21 | this.delays.push(nextNum); 22 | } 23 | this.delays[this.delays.length-1] = this.options.maxWait; 24 | 25 | // set default lifetime 26 | if (typeof this.options.lifetime == "undefined") { 27 | this.options.lifetime = (this.options.maxWait/1000)*(this.delays.length + this.options.freeRetries); 28 | this.options.lifetime = Math.ceil(this.options.lifetime); 29 | } 30 | 31 | // generate "prevent" middleware 32 | this.prevent = this.getMiddleware(); 33 | }; 34 | ExpressBrute.prototype.getMiddleware = function (options) { 35 | // standardize input 36 | options = _.extend({}, options); 37 | var keyFunc = options.key; 38 | if (typeof keyFunc !== 'function') { 39 | keyFunc = function (req, res, next) { next(options.key); }; 40 | } 41 | var getFailCallback = _.bind(function () { 42 | return typeof options.failCallback === 'undefined' ? this.options.failCallback : options.failCallback; 43 | }, this); 44 | 45 | // create middleware 46 | return _.bind(function (req, res, next) { 47 | keyFunc(req, res, _.bind(function (key) { 48 | if(!options.ignoreIP) { 49 | key = ExpressBrute._getKey([req.ip, this.name, key]); 50 | } else { 51 | key = ExpressBrute._getKey([this.name, key]); 52 | } 53 | 54 | // attach a simpler "reset" function to req.brute.reset 55 | if (this.options.attachResetToRequest) { 56 | var reset = _.bind(function (callback) { 57 | this.store.reset(key, function (err) { 58 | if (typeof callback == 'function') { 59 | process.nextTick(function () { 60 | callback(err); 61 | }); 62 | } 63 | }); 64 | }, this); 65 | if (req.brute && req.brute.reset) { 66 | // wrap existing reset if one exists 67 | var oldReset = req.brute.reset; 68 | var newReset = reset; 69 | reset = function (callback) { 70 | oldReset(function () { 71 | newReset(callback); 72 | }); 73 | }; 74 | } 75 | req.brute = { 76 | reset: reset 77 | }; 78 | } 79 | 80 | 81 | // filter request 82 | this.store.get(key, _.bind(function (err, value) { 83 | if (err) { 84 | this.options.handleStoreError({ 85 | req: req, 86 | res: res, 87 | next: next, 88 | message: "Cannot get request count", 89 | parent: err 90 | }); 91 | return; 92 | } 93 | 94 | var count = 0, 95 | delay = 0, 96 | lastValidRequestTime = this.now(), 97 | firstRequestTime = lastValidRequestTime; 98 | if (value) { 99 | count = value.count; 100 | lastValidRequestTime = value.lastRequest.getTime(); 101 | firstRequestTime = value.firstRequest.getTime(); 102 | 103 | var delayIndex = value.count - this.options.freeRetries - 1; 104 | if (delayIndex >= 0) { 105 | if (delayIndex < this.delays.length) { 106 | delay = this.delays[delayIndex]; 107 | } else { 108 | delay = this.options.maxWait; 109 | } 110 | } 111 | } 112 | var nextValidRequestTime = lastValidRequestTime+delay, 113 | remainingLifetime = this.options.lifetime || 0; 114 | 115 | if (!this.options.refreshTimeoutOnRequest && remainingLifetime > 0) { 116 | remainingLifetime = remainingLifetime - Math.floor((this.now() - firstRequestTime) / 1000); 117 | if (remainingLifetime < 1) { 118 | // it should be expired alredy, treat this as a new request and reset everything 119 | count = 0; 120 | delay = 0; 121 | nextValidRequestTime = firstRequestTime = lastValidRequestTime = this.now(); 122 | remainingLifetime = this.options.lifetime || 0; 123 | } 124 | } 125 | 126 | if (nextValidRequestTime <= this.now() || count <= this.options.freeRetries) { 127 | this.store.set(key, { 128 | count: count+1, 129 | lastRequest: new Date(this.now()), 130 | firstRequest: new Date(firstRequestTime) 131 | }, remainingLifetime, _.bind(function (err) { 132 | if (err) { 133 | this.options.handleStoreError({ 134 | req: req, 135 | res: res, 136 | next: next, 137 | message: "Cannot increment request count", 138 | parent: err 139 | }); 140 | return; 141 | } 142 | typeof next == 'function' && next(); 143 | },this)); 144 | } else { 145 | var failCallback = getFailCallback(); 146 | typeof failCallback === 'function' && failCallback(req, res, next, new Date(nextValidRequestTime)); 147 | } 148 | }, this)); 149 | },this)); 150 | }, this); 151 | }; 152 | ExpressBrute.prototype.reset = function (ip, key, callback) { 153 | key = ExpressBrute._getKey([ip, this.name, key]); 154 | this.store.reset(key, _.bind(function (err) { 155 | if (err) { 156 | this.options.handleStoreError({ 157 | message: "Cannot reset request count", 158 | parent: err, 159 | key: key, 160 | ip: ip 161 | }); 162 | } else { 163 | if (typeof callback == 'function') { 164 | process.nextTick(_.bind(function () { 165 | callback.apply(this, arguments); 166 | }, this)); 167 | } 168 | } 169 | },this)); 170 | }; 171 | ExpressBrute.prototype.now = function () { 172 | return Date.now(); 173 | }; 174 | 175 | var setRetryAfter = function (res, nextValidRequestDate) { 176 | var secondUntilNextRequest = Math.ceil((nextValidRequestDate.getTime() - Date.now())/1000); 177 | res.header('Retry-After', secondUntilNextRequest); 178 | }; 179 | ExpressBrute.FailTooManyRequests = function (req, res, next, nextValidRequestDate) { 180 | setRetryAfter(res, nextValidRequestDate); 181 | res.status(429); 182 | res.send({error: {text: "Too many requests in this time frame.", nextValidRequestDate: nextValidRequestDate}}); 183 | }; 184 | ExpressBrute.FailForbidden = function (req, res, next, nextValidRequestDate) { 185 | setRetryAfter(res, nextValidRequestDate); 186 | res.status(403); 187 | res.send({error: {text: "Too many requests in this time frame.", nextValidRequestDate: nextValidRequestDate}}); 188 | }; 189 | ExpressBrute.FailMark = function (req, res, next, nextValidRequestDate) { 190 | res.status(429); 191 | setRetryAfter(res, nextValidRequestDate); 192 | res.nextValidRequestDate = nextValidRequestDate; 193 | next(); 194 | }; 195 | ExpressBrute._getKey = function (arr) { 196 | var key = ''; 197 | _(arr).each(function (part) { 198 | if (part) { 199 | key += crypto.createHash('sha256').update(part).digest('base64'); 200 | } 201 | }); 202 | return crypto.createHash('sha256').update(key).digest('base64'); 203 | }; 204 | 205 | ExpressBrute.MemoryStore = require('./lib/MemoryStore'); 206 | ExpressBrute.defaults = { 207 | freeRetries: 2, 208 | proxyDepth: 0, 209 | attachResetToRequest: true, 210 | refreshTimeoutOnRequest: true, 211 | minWait: 500, 212 | maxWait: 1000*60*15, // 15 minutes 213 | failCallback: ExpressBrute.FailTooManyRequests, 214 | handleStoreError: function (err) { 215 | throw { 216 | message: err.message, 217 | parent: err.parent 218 | }; 219 | } 220 | }; 221 | ExpressBrute.instanceCount = 0; 222 | -------------------------------------------------------------------------------- /lib/AbstractClientStore.js: -------------------------------------------------------------------------------- 1 | var AbstractClientStore = module.exports = function () { 2 | 3 | }; 4 | AbstractClientStore.prototype.increment = function (key, lifetime, callback) { 5 | var self = this; 6 | this.get(key, function (err, value) { 7 | if (err) { 8 | callback(err); 9 | } else { 10 | var count = value ? value.count+1 : 1; 11 | self.set(key, {count: count, lastRequest: new Date(), firstRequest: new Date()}, lifetime, function (err) { 12 | var prevValue = { 13 | count: value ? value.count : 0, 14 | lastRequest: value ? value.lastRequest : null, 15 | firstRequest: value ? value.firstRequest : null 16 | }; 17 | typeof callback == 'function' && callback(err, prevValue); 18 | }); 19 | } 20 | }); 21 | }; -------------------------------------------------------------------------------- /lib/MemoryStore.js: -------------------------------------------------------------------------------- 1 | var AbstractClientStore = require('./AbstractClientStore'), 2 | _ = require('underscore'), 3 | longTimeout = require('long-timeout'); // not sure this is really neccessary, since it seems like node currently supports long timeouts natively 4 | 5 | var MemoryStore = module.exports = function (options) { 6 | this.data = {}; 7 | _.bindAll(this, 'set', 'get', 'reset'); 8 | this.options = _.extend({}, MemoryStore.defaults, options); 9 | }; 10 | MemoryStore.prototype = Object.create(AbstractClientStore.prototype); 11 | MemoryStore.prototype.set = function (key, value, lifetime, callback) { 12 | key = this.options.prefix+key; 13 | lifetime = lifetime || 0; 14 | value = JSON.stringify(value); 15 | 16 | if (!this.data[key]) { 17 | this.data[key] = {}; 18 | } else if (this.data[key].timeout) { 19 | longTimeout.clearTimeout(this.data[key].timeout); 20 | } 21 | this.data[key].value = value; 22 | 23 | if (lifetime) { 24 | this.data[key].timeout = longTimeout.setTimeout(_.bind(function () { 25 | delete this.data[key]; 26 | }, this), 1000*lifetime); 27 | } 28 | typeof callback == 'function' && callback(null); 29 | }; 30 | MemoryStore.prototype.get = function (key, callback) { 31 | key = this.options.prefix+key; 32 | var data = this.data[key] && this.data[key].value; 33 | if (data) { 34 | data = JSON.parse(data); 35 | data.lastRequest = new Date(data.lastRequest); 36 | data.firstRequest = new Date(data.firstRequest); 37 | } 38 | typeof callback == 'function' && callback(null, data); 39 | }; 40 | MemoryStore.prototype.reset = function (key, callback) { 41 | key = this.options.prefix+key; 42 | 43 | if (this.data[key] && this.data[key].timeout) { 44 | longTimeout.clearTimeout(this.data[key].timeout); 45 | } 46 | delete this.data[key]; 47 | typeof callback == 'function' && callback(null); 48 | }; 49 | MemoryStore.defaults = { 50 | prefix: '' 51 | }; -------------------------------------------------------------------------------- /mock/ResponseMock.js: -------------------------------------------------------------------------------- 1 | var sinon = require("sinon"); 2 | module.exports = function () { 3 | return { 4 | status: sinon.stub(), 5 | send: sinon.stub(), 6 | header: sinon.stub() 7 | }; 8 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-brute", 3 | "version": "1.0.1", 4 | "description": "A brute-force protection middleware for express routes that rate limits incoming requests", 5 | "keywords": [ 6 | "brute", 7 | "force", 8 | "bruteforce", 9 | "attack", 10 | "fibonacci", 11 | "rate", 12 | "limit", 13 | "security" 14 | ], 15 | "license": "BSD", 16 | "private": false, 17 | "scripts": { 18 | "test": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha spec" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:AdamPflug/express-brute.git" 23 | }, 24 | "devDependencies": { 25 | "chai": "~3.5.0", 26 | "coveralls": "~2.11.9", 27 | "istanbul": "~0.4.3", 28 | "mocha": "~2.4.5", 29 | "mocha-lcov-reporter": "~1.2.0", 30 | "sinon": "~1.17.3", 31 | "sinon-chai": "~2.8.0" 32 | }, 33 | "dependencies": { 34 | "long-timeout": "~0.1.1", 35 | "underscore": "~1.8.3" 36 | }, 37 | "peerDependencies": { 38 | "express": "4.x" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spec/ExpessBrute.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | should = chai.should(), 3 | sinon = require('sinon'), 4 | sinonChai = require('sinon-chai'), 5 | ExpressBrute = require("../index"), 6 | ResponseMock = require('../mock/ResponseMock'); 7 | 8 | chai.use(sinonChai); 9 | 10 | describe("express brute", function () { 11 | var clock; 12 | before(function () { 13 | clock = sinon.useFakeTimers(); 14 | }); 15 | after(function () { 16 | clock.restore(); 17 | }); 18 | describe("basic functionality", function () { 19 | it("has some memory stores", function () { 20 | ExpressBrute.MemoryStore.should.exist; 21 | }); 22 | it("can be initialized", function () { 23 | var store = new ExpressBrute.MemoryStore(); 24 | var brute = new ExpressBrute(store); 25 | brute.should.be.an.instanceof(ExpressBrute); 26 | }); 27 | }); 28 | describe("behavior", function () { 29 | var brute, store, errorSpy, nextSpy, req, req2; 30 | beforeEach(function () { 31 | store = new ExpressBrute.MemoryStore(); 32 | errorSpy = sinon.stub(); 33 | nextSpy = sinon.stub(); 34 | req = function () { return { ip: '1.2.3.4' }; }; 35 | req2 = function () { return { ip: '5.6.7.8' }; }; 36 | brute = new ExpressBrute(store, { 37 | freeRetries: 0, 38 | minWait: 10, 39 | maxWait: 100, 40 | failCallback: errorSpy 41 | }); 42 | }); 43 | it('correctly calculates delays', function () { 44 | brute.delays.should.deep.equal([10,10,20,30,50,80,100]); 45 | }); 46 | it('respects free retries', function () { 47 | brute = new ExpressBrute(store, { 48 | freeRetries: 1, 49 | minWait: 10, 50 | maxWait: 100, 51 | failCallback: errorSpy 52 | }); 53 | brute.prevent(req(), new ResponseMock(), nextSpy); 54 | brute.prevent(req(), new ResponseMock(), nextSpy); 55 | errorSpy.should.not.have.been.called; 56 | brute.prevent(req(), new ResponseMock(), nextSpy); 57 | errorSpy.should.have.been.called; 58 | }); 59 | it('respects free retries even with clock skew', function() { 60 | brute = new ExpressBrute(store, { 61 | freeRetries: 1, 62 | minWait: 10, 63 | maxWait: 100, 64 | failCallback: errorSpy 65 | }); 66 | brute.prevent(req(), new ResponseMock(), nextSpy); 67 | clock.tick(-100); 68 | brute.prevent(req(), new ResponseMock(), nextSpy); 69 | errorSpy.should.not.have.been.called; 70 | brute.prevent(req(), new ResponseMock(), nextSpy); 71 | errorSpy.should.have.been.called; 72 | }); 73 | it('correctly calculates delays when min and max wait are the same', function () { 74 | brute = new ExpressBrute(store, { 75 | freeRetries: 0, 76 | minWait: 10, 77 | maxWait: 10, 78 | failCallback: errorSpy 79 | }); 80 | brute.delays.should.deep.equal([10]); 81 | }); 82 | it ('calls next when the request is allowed', function () { 83 | brute.prevent(req(), new ResponseMock(), nextSpy); 84 | nextSpy.should.have.been.calledOnce; 85 | brute.prevent(req(), new ResponseMock(), nextSpy); 86 | nextSpy.should.have.been.calledOnce; 87 | }); 88 | it ('calls the error callback when requests come in too quickly', function () { 89 | brute.prevent(req(), new ResponseMock(), nextSpy); 90 | errorSpy.should.not.have.been.called; 91 | brute.prevent(req(), new ResponseMock(), nextSpy); 92 | errorSpy.should.have.been.called; 93 | }); 94 | it ('allows requests as long as you wait long enough', function () { 95 | 96 | brute.prevent(req(), new ResponseMock(), nextSpy); 97 | errorSpy.should.not.have.been.called; 98 | clock.tick(brute.delays[0]+1); 99 | brute.prevent(req(), new ResponseMock(), nextSpy); 100 | errorSpy.should.not.have.been.called; 101 | }); 102 | it ('allows requests if you reset the timer', function (done) { 103 | brute.prevent(req(), new ResponseMock(), nextSpy); 104 | errorSpy.should.not.have.been.called; 105 | var async = false; 106 | brute.reset('1.2.3.4', null, function () { 107 | async.should.be.true; 108 | brute.prevent(req(), new ResponseMock(), nextSpy); 109 | errorSpy.should.not.have.been.called; 110 | done(); 111 | }); 112 | async = true; 113 | 114 | }); 115 | it('adds a reset shortcut to the request object', function (done) { 116 | var reqObj = req(); 117 | brute.prevent(reqObj, new ResponseMock(), nextSpy); 118 | errorSpy.should.not.have.been.called; 119 | should.exist(reqObj.brute); 120 | should.exist(reqObj.brute.reset); 121 | reqObj.brute.reset(function () { 122 | brute.prevent(req(), new ResponseMock(), nextSpy); 123 | errorSpy.should.not.have.been.called; 124 | done(); 125 | }); 126 | }); 127 | it("resets even if you don't pass a callback", function (done) { 128 | brute.prevent(req(), new ResponseMock(), nextSpy); 129 | brute.reset('1.2.3.4', null); 130 | process.nextTick(function () { 131 | brute.prevent(req(), new ResponseMock(), nextSpy); 132 | errorSpy.should.not.have.been.called; 133 | done(); 134 | }); 135 | }); 136 | it ('allows requests if you use different ips', function () { 137 | brute.prevent(req(), new ResponseMock(), nextSpy); 138 | errorSpy.should.not.have.been.called; 139 | nextSpy.should.have.been.calledOnce; 140 | brute.prevent(req2(), new ResponseMock(), nextSpy); 141 | errorSpy.should.not.have.been.called; 142 | nextSpy.should.have.been.calledTwice; 143 | }); 144 | it ('passes the correct next request time', function () { 145 | var curTime = Date.now(), 146 | expectedTime = curTime+brute.delays[0]; 147 | 148 | var oldNow = brute.now; 149 | brute.now = function () { return curTime; }; 150 | brute.prevent(req(), new ResponseMock(), nextSpy); 151 | brute.now = oldNow; 152 | 153 | clock.tick(); // ensure some time has passed before calling the next time, caught a bug 154 | 155 | brute.prevent(req(), new ResponseMock(), errorSpy); 156 | errorSpy.should.have.been.called; 157 | errorSpy.lastCall.args[3].getTime().should.equal(expectedTime); 158 | }); 159 | it('works even after the maxwait is reached', function () { 160 | brute = new ExpressBrute(store, { 161 | freeRetries: 0, 162 | minWait: 10, 163 | maxWait: 10, 164 | failCallback: function () {} 165 | }); 166 | 167 | brute.prevent(req(), new ResponseMock(), nextSpy); 168 | brute.prevent(req(), new ResponseMock(), nextSpy); 169 | brute.options.failCallback = errorSpy; 170 | 171 | clock.tick(brute.delays[0]+1); 172 | 173 | var curTime = Date.now(), 174 | expectedTime = curTime+brute.delays[0], 175 | oldNow = brute.now; 176 | brute.now = function () { return curTime; }; 177 | brute.prevent(req(), new ResponseMock(), nextSpy); 178 | brute.now = oldNow; 179 | brute.prevent(req(), new ResponseMock(), nextSpy); 180 | errorSpy.should.have.been.called; 181 | errorSpy.lastCall.args[3].getTime().should.equal(expectedTime); 182 | }); 183 | it('correctly calculates default lifetime', function () { 184 | brute = new ExpressBrute(store, { 185 | freeRetries: 1, 186 | minWait: 100, 187 | maxWait: 1000, 188 | failCallback: errorSpy 189 | }); 190 | brute.options.lifetime.should.equal(8); 191 | }); 192 | it('allows requests after the lifetime causes them to expire', function () { 193 | brute = new ExpressBrute(store, { 194 | freeRetries: 0, 195 | minWait: 10000, 196 | maxWait: 10000, 197 | lifetime: 1, 198 | failCallback: errorSpy 199 | }); 200 | brute.prevent(req(), new ResponseMock(), nextSpy); 201 | errorSpy.should.not.have.been.called; 202 | brute.prevent(req(), new ResponseMock(), nextSpy); 203 | errorSpy.should.have.been.called; 204 | 205 | clock.tick((brute.options.lifetime*1000)+1); 206 | 207 | brute.prevent(req(), new ResponseMock(), nextSpy); 208 | errorSpy.should.have.been.calledOnce; 209 | }); 210 | it("doesn't extend the lifetime if refreshTimeoutOnRequest is false", function () { 211 | brute = new ExpressBrute(store, { 212 | freeRetries: 0, 213 | minWait: 10000, 214 | maxWait: 10000, 215 | lifetime: 1, 216 | refreshTimeoutOnRequest: false, 217 | failCallback: errorSpy 218 | }); 219 | brute.prevent(req(), new ResponseMock(), nextSpy); 220 | errorSpy.should.not.have.been.called; 221 | brute.prevent(req(), new ResponseMock(), nextSpy); 222 | errorSpy.should.have.been.calledOnce; 223 | 224 | clock.tick((brute.options.lifetime*500)); 225 | 226 | brute.prevent(req(), new ResponseMock(), nextSpy); 227 | errorSpy.should.have.been.calledTwice; 228 | 229 | clock.tick((brute.options.lifetime*500)+1); 230 | 231 | brute.prevent(req(), new ResponseMock(), nextSpy); 232 | errorSpy.should.have.been.calledTwice; 233 | }); 234 | it('does extend the lifetime if refreshTimeoutOnRequest is true', function () { 235 | brute = new ExpressBrute(store, { 236 | freeRetries: 1, 237 | minWait: 10000, 238 | maxWait: 10000, 239 | lifetime: 1, 240 | failCallback: errorSpy 241 | }); 242 | brute.prevent(req(), new ResponseMock(), nextSpy); 243 | errorSpy.should.not.have.been.called; 244 | 245 | clock.tick((brute.options.lifetime*500)); 246 | 247 | brute.prevent(req(), new ResponseMock(), nextSpy); 248 | errorSpy.should.not.have.been.called; 249 | 250 | clock.tick((brute.options.lifetime*500)+1); 251 | 252 | brute.prevent(req(), new ResponseMock(), nextSpy); 253 | errorSpy.should.have.been.calledOnce; 254 | }); 255 | it('allows failCallback to be overridden', function () { 256 | brute = new ExpressBrute(store, { 257 | freeRetries: 0, 258 | minWait: 10000, 259 | maxWait: 10000, 260 | lifetime: 1, 261 | failCallback: errorSpy 262 | }); 263 | var errorSpy2 = sinon.stub(); 264 | var mid = brute.getMiddleware({ 265 | failCallback: errorSpy2 266 | }); 267 | 268 | mid(req(), new ResponseMock(), nextSpy); 269 | errorSpy.should.not.have.been.called; 270 | errorSpy2.should.not.have.been.called; 271 | 272 | mid(req(), new ResponseMock(), nextSpy); 273 | errorSpy.should.not.have.been.called; 274 | errorSpy2.should.have.been.called; 275 | }); 276 | }); 277 | describe("multiple keys", function () { 278 | var brute, store, errorSpy, nextSpy, req; 279 | beforeEach(function () { 280 | store = new ExpressBrute.MemoryStore(); 281 | errorSpy = sinon.stub(); 282 | nextSpy = sinon.stub(); 283 | req = function () { return { ip: '1.2.3.4' }; }; 284 | brute = new ExpressBrute(store, { 285 | freeRetries: 0, 286 | minWait: 10, 287 | maxWait: 100, 288 | failCallback: errorSpy 289 | }); 290 | }); 291 | it ('tracks keys separately', function () { 292 | var first = brute.getMiddleware({key: 'first' }); 293 | var second = brute.getMiddleware({key: 'second' }); 294 | 295 | first(req(), new ResponseMock(), nextSpy); 296 | nextSpy.should.have.been.calledOnce; 297 | second(req(), new ResponseMock(), nextSpy); 298 | nextSpy.should.have.been.calledTwice; 299 | 300 | first(req(), new ResponseMock(), nextSpy); 301 | nextSpy.should.have.been.calledTwice; 302 | second(req(), new ResponseMock(), nextSpy); 303 | nextSpy.should.have.been.calledTwice; 304 | }); 305 | it ('supports key functions', function () { 306 | req = function () { 307 | return { 308 | ip: '1.2.3.4', 309 | someData: "something cool" 310 | }; 311 | }; 312 | var first = brute.getMiddleware({key: function(req, res, next) { next(req.someData); } }); 313 | var second = brute.getMiddleware({key: "something cool" }); 314 | 315 | first(req(), new ResponseMock(), nextSpy); 316 | nextSpy.should.have.been.calledOnce; 317 | first(req(), new ResponseMock(), nextSpy); 318 | nextSpy.should.have.been.calledOnce; 319 | second(req(), new ResponseMock(), nextSpy); 320 | nextSpy.should.have.been.calledOnce; 321 | }); 322 | it('supports ignoring IP', function() { 323 | var req = function () { 324 | return { 325 | ip: '1.2.3.4' 326 | }; 327 | }; 328 | var req2 = function () { 329 | return { 330 | ip: '4.3.2.1' 331 | }; 332 | }; 333 | var first = brute.getMiddleware({key: "something cool", ignoreIP: true}); 334 | first(req(), new ResponseMock(), nextSpy); 335 | nextSpy.should.have.been.calledOnce; 336 | first(req2(), new ResponseMock(), nextSpy); 337 | nextSpy.should.have.been.calledOnce; 338 | }); 339 | it ('supports brute.reset', function () { 340 | var mid = brute.getMiddleware({key: 'withAKey' }); 341 | 342 | mid(req(), new ResponseMock(), nextSpy); 343 | nextSpy.should.have.been.calledOnce; 344 | brute.reset("1.2.3.4", "withAKey"); 345 | mid(req(), new ResponseMock(), nextSpy); 346 | nextSpy.should.have.been.calledTwice; 347 | }); 348 | it ('supports req.reset shortcut', function () { 349 | var firstReq, mid = brute.getMiddleware({key: 'withAKey' }); 350 | 351 | mid(firstReq = req(), new ResponseMock(), nextSpy); 352 | nextSpy.should.have.been.calledOnce; 353 | firstReq.brute.reset(); 354 | mid(req(), new ResponseMock(), nextSpy); 355 | nextSpy.should.have.been.calledTwice; 356 | }); 357 | it ('respects the attachResetToRequest', function () { 358 | brute.options.attachResetToRequest = false; 359 | var firstReq; 360 | 361 | brute.prevent(firstReq = req(), new ResponseMock(), nextSpy); 362 | nextSpy.should.have.been.calledOnce; 363 | should.not.exist(firstReq.brute); 364 | }); 365 | }); 366 | describe("multiple brute instances", function () { 367 | var brute, brute2, store, errorSpy, errorSpy2, nextSpy, req; 368 | beforeEach(function () { 369 | store = new ExpressBrute.MemoryStore(); 370 | errorSpy = sinon.stub(); 371 | errorSpy2 = sinon.stub(); 372 | nextSpy = sinon.stub(); 373 | req = function () { return { ip: '1.2.3.4' }; }; 374 | brute = new ExpressBrute(store, { 375 | freeRetries: 0, 376 | minWait: 100, 377 | maxWait: 1000, 378 | failCallback: errorSpy, 379 | lifetime: 0 380 | }); 381 | brute2 = new ExpressBrute(store, { 382 | freeRetries: 1, 383 | minWait: 100, 384 | maxWait: 1000, 385 | failCallback: errorSpy2, 386 | lifetime: 0 387 | }); 388 | }); 389 | it ('tracks hits separately for each instance', function () { 390 | brute.prevent(req(), new ResponseMock(), nextSpy); 391 | brute2.prevent(req(), new ResponseMock(), nextSpy); 392 | 393 | errorSpy.should.not.have.been.called; 394 | errorSpy2.should.not.have.been.called; 395 | 396 | brute.prevent(req(), new ResponseMock(), nextSpy); 397 | brute2.prevent(req(), new ResponseMock(), nextSpy); 398 | 399 | 400 | errorSpy.should.have.been.called; 401 | errorSpy2.should.not.have.been.called; 402 | 403 | brute.prevent(req(), new ResponseMock(), nextSpy); 404 | brute2.prevent(req(), new ResponseMock(), nextSpy); 405 | 406 | nextSpy.should.have.been.calledThrice; 407 | errorSpy2.should.have.been.called; 408 | }); 409 | it ('resets both brute instances when the req.reset shortcut is called', function (done) { 410 | var failReq = req(); 411 | var successSpy = sinon.stub(); 412 | 413 | brute.prevent(req(), new ResponseMock(), nextSpy); 414 | brute2.prevent(req(), new ResponseMock(), nextSpy); 415 | brute2.prevent(req(), new ResponseMock(), nextSpy); 416 | errorSpy.should.not.have.been.called; 417 | errorSpy2.should.not.have.been.called; 418 | 419 | brute.prevent(failReq, new ResponseMock(), nextSpy); 420 | brute2.prevent(failReq, new ResponseMock(), nextSpy); 421 | errorSpy.should.have.been.called; 422 | errorSpy2.should.have.been.called; 423 | 424 | failReq.brute.reset(function () { 425 | brute.prevent(failReq, new ResponseMock(), successSpy); 426 | brute2.prevent(failReq, new ResponseMock(), successSpy); 427 | successSpy.should.have.been.calledTwice; 428 | done(); 429 | }); 430 | }); 431 | it ('resets only one brute instance when the req.reset shortcut is called but attachResetToRequest is false on one', function (done) { 432 | brute2 = new ExpressBrute(store, { 433 | freeRetries: 1, 434 | minWait: 100, 435 | maxWait: 1000, 436 | failCallback: errorSpy2, 437 | lifetime: 0, 438 | attachResetToRequest: false 439 | }); 440 | 441 | var failReq = req(); 442 | var successStub = sinon.stub(); 443 | 444 | brute.prevent(req(), new ResponseMock(), nextSpy); 445 | brute2.prevent(req(), new ResponseMock(), nextSpy); 446 | brute2.prevent(req(), new ResponseMock(), nextSpy); 447 | errorSpy.should.not.have.been.called; 448 | errorSpy2.should.not.have.been.called; 449 | 450 | brute.prevent(failReq, new ResponseMock(), nextSpy); 451 | brute2.prevent(failReq, new ResponseMock(), nextSpy); 452 | errorSpy.should.have.been.called; 453 | errorSpy2.should.have.been.called; 454 | 455 | failReq.brute.reset(function () { 456 | brute.prevent(failReq, new ResponseMock(), successStub); 457 | brute2.prevent(failReq, new ResponseMock(), successStub); 458 | successStub.should.have.been.called.once; 459 | done(); 460 | }); 461 | }); 462 | }); 463 | describe("failure handlers", function () { 464 | var brute, store, req, nextSpy; 465 | beforeEach(function () { 466 | store = new ExpressBrute.MemoryStore(); 467 | req = function () { return { ip: '1.2.3.4' }; }; 468 | nextSpy = sinon.stub(); 469 | 470 | }); 471 | it('can return a 429 Too Many Requests', function () { 472 | var res = new ResponseMock(); 473 | brute = new ExpressBrute(store, { 474 | freeRetries: 0, 475 | minWait: 10, 476 | maxWait: 100, 477 | failCallback: ExpressBrute.FailTooManyRequests 478 | }); 479 | brute.prevent(req(), res, nextSpy); 480 | brute.prevent(req(), res, nextSpy); 481 | res.send.should.have.been.called; 482 | res.status.lastCall.args[0].should.equal(429); 483 | }); 484 | it('can return a 403 Forbidden', function () { 485 | var res = new ResponseMock(); 486 | brute = new ExpressBrute(store, { 487 | freeRetries: 0, 488 | minWait: 10, 489 | maxWait: 100, 490 | failCallback: ExpressBrute.FailForbidden 491 | }); 492 | brute.prevent(req(), res, nextSpy); 493 | brute.prevent(req(), res, nextSpy); 494 | res.send.should.have.been.called; 495 | res.status.lastCall.args[0].should.equal(403); 496 | }); 497 | it('can mark a response as failed, but continue processing', function () { 498 | var res = new ResponseMock(); 499 | brute = new ExpressBrute(store, { 500 | freeRetries: 0, 501 | minWait: 10, 502 | maxWait: 100, 503 | failCallback: ExpressBrute.FailMark 504 | }); 505 | brute.prevent(req(), res, nextSpy); 506 | brute.prevent(req(), res, nextSpy); 507 | res.status.should.have.been.calledWith(429); 508 | nextSpy.should.have.been.calledTwice; 509 | res.nextValidRequestDate.should.exist; 510 | res.nextValidRequestDate.should.be.instanceof(Date); 511 | }); 512 | it('sets Retry-After', function () { 513 | var res = new ResponseMock(); 514 | brute = new ExpressBrute(store, { 515 | freeRetries: 0, 516 | minWait: 10, 517 | maxWait: 100, 518 | failCallback: ExpressBrute.FailTooManyRequests 519 | }); 520 | brute.prevent(req(), res, nextSpy); 521 | brute.prevent(req(), res, nextSpy); 522 | res.header.should.have.been.calledWith('Retry-After', 1); 523 | }); 524 | }); 525 | describe("store error handling", function () { 526 | var brute, store, errorSpy, storeErrorSpy, nextSpy, req, res, err; 527 | beforeEach(function () { 528 | store = new ExpressBrute.MemoryStore(); 529 | errorSpy = sinon.stub(); 530 | storeErrorSpy = sinon.stub(); 531 | nextSpy = sinon.stub(); 532 | req = { ip: '1.2.3.4' }; 533 | res = new ResponseMock(); 534 | err = "Example Error"; 535 | brute = new ExpressBrute(store, { 536 | freeRetries: 0, 537 | minWait: 10, 538 | maxWait: 100, 539 | failCallback: errorSpy, 540 | handleStoreError: storeErrorSpy 541 | }); 542 | }); 543 | it('should handle get errors', function () { 544 | sinon.stub(store, 'get', function (key, callback) { 545 | callback(err); 546 | }); 547 | brute.prevent(req, res, nextSpy); 548 | storeErrorSpy.should.have.been.calledWithMatch({ 549 | req: req, 550 | res: res, 551 | next: nextSpy, 552 | message: 'Cannot get request count', 553 | parent: err 554 | }); 555 | errorSpy.should.not.have.been.called; 556 | nextSpy.should.not.have.been.called; 557 | }); 558 | it('should handle set errors', function () { 559 | sinon.stub(store, 'set', function (key, value, lifetime, callback) { 560 | callback(err); 561 | }); 562 | brute.prevent(req, res, nextSpy); 563 | storeErrorSpy.should.have.been.calledWithMatch({ 564 | req: req, 565 | res: res, 566 | next: nextSpy, 567 | message: 'Cannot increment request count', 568 | parent: err 569 | }); 570 | errorSpy.should.not.have.been.called; 571 | nextSpy.should.not.have.been.called; 572 | }); 573 | it('should handle reset errors', function () { 574 | sinon.stub(store, 'reset', function (key, callback) { 575 | callback(err); 576 | }); 577 | var key = 'testKey'; 578 | brute.reset('1.2.3.4', key, nextSpy); 579 | storeErrorSpy.should.have.been.calledWithMatch({ 580 | message: "Cannot reset request count", 581 | parent: err, 582 | key: ExpressBrute._getKey(['1.2.3.4', brute.name, key]), 583 | ip: '1.2.3.4' 584 | }); 585 | errorSpy.should.not.have.been.called; 586 | nextSpy.should.not.have.been.called; 587 | }); 588 | it('should throw an exception by default', function () { 589 | brute = new ExpressBrute(store, { 590 | freeRetries: 0, 591 | minWait: 10, 592 | maxWait: 100, 593 | failCallback: errorSpy 594 | }); 595 | sinon.stub(store, 'get', function (key, callback) { 596 | callback(err); 597 | }); 598 | (function () { 599 | brute.prevent(req, res, nextSpy); 600 | }).should.throw({ 601 | message: 'Cannot get request count', 602 | parent: err 603 | }); 604 | errorSpy.should.not.have.been.called; 605 | nextSpy.should.not.have.been.called; 606 | }); 607 | }); 608 | describe('MemoryStore', function () { 609 | it('supports timeouts of greater than 24.8 days (64 bit timeouts)', function () { 610 | var yearInSeconds = 60*60*24*365; 611 | var store = new ExpressBrute.MemoryStore(); 612 | var errorSpy = sinon.stub(); 613 | var nextSpy = sinon.stub(); 614 | var req = function () { return { ip: '1.2.3.4' }; }; 615 | var brute = new ExpressBrute(store, { 616 | freeRetries: 0, 617 | minWait: (yearInSeconds+100)*1000, 618 | maxWait: (yearInSeconds+100)*1000, 619 | lifetime: yearInSeconds, 620 | failCallback: errorSpy 621 | }); 622 | brute.prevent(req(), new ResponseMock(), nextSpy); 623 | errorSpy.should.not.have.been.called; 624 | clock.tick((brute.options.lifetime-100)*1000); 625 | 626 | brute.prevent(req(), new ResponseMock(), nextSpy); 627 | errorSpy.should.have.been.called; 628 | 629 | clock.tick(101*1000); 630 | 631 | brute.prevent(req(), new ResponseMock(), nextSpy); 632 | errorSpy.should.have.been.calledOnce; 633 | }); 634 | }); 635 | }); 636 | --------------------------------------------------------------------------------