Skip to content

ExpressBrute migration

Roman edited this page Nov 10, 2023 · 23 revisions

Migration example

Advantages of migration:

  1. Keep limiting logic with the same options and API.
  2. Use atomic increments to count allowed requests. Get/set approach implemented in ExpressBrute may get you into trouble.
  3. Remove unnecessary long-timeout and underscore dependencies.

Note: there is another way to protect login endpoints described here. It has much better performance and flexibility as it counts failed attempts only.

Options

  • freeRetries The number of retries 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: 1000 milliseconds)
  • maxWait The maximum amount of time (in milliseconds) between requests 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: ExpressBruteFlexible.FailForbidden)
  • attachResetToRequest Specify whether or not a simplified reset method should be attached at req.brute.reset.(default: true)
  • refreshTimeoutOnRequest Always false for ExpressBruteFlexible.
  • handleStoreError Gets called whenever an error occurs with the persistent store from which ExpressBruteFlexible 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.
  • prefix Useful when several middlewares should have different counters for the same key. (default: '')

getMiddleware options

  • key can be a string or alternatively it can be a function(req, res, next) that calls next, passing a string as the first parameter.
  • failCallback Set custom callback on the final fail after all attempts.
  • ignoreIP Disregard IP address when matching requests if set to true. (default: false)
  • prefix Useful when several middlewares should have different counters for the same key. (default: '')

reset counter

req.brute.reset can be called for current request, if attachResetToRequest is true. Alternatively, reset(ip, key, callback) can be called from ExpressBruteFlexible instance. The first argument ip should be set to NULL, if ignoreIp option is true.

Redis example

const ExpressBruteFlexible = require('rate-limiter-flexible/lib/ExpressBruteFlexible');
const redis = require('redis');
const http = require('http');
const express = require('express');

const redisClient = await redis.createClient({
  enable_offline_queue: false,
})
.connect();

const opts = {
  freeRetries: 10,
  minWait: 1000, // 1 second
  maxWait: 10000, // 10 seconds
  lifetime: 30, // 30 seconds
  storeClient: redisClient,
};

const bruteforce = new ExpressBruteFlexible(
  ExpressBruteFlexible.LIMITER_TYPES.REDIS, 
  opts
);

const app = express();

app.post('/auth',
  bruteforce.prevent, // error 429 if we hit this route too often
  function (req, res, next) {
    res.send('Success!');
  }
);

How to migrate your code

ExpressBruteFlexible constructor requires to set a limiter type one from ExpressBruteFlexible.LIMITER_TYPES.*. The second argument is options.

Options are the same except:

  1. storeClient should be added in case of using any limiter type except MEMORY and CLUSTER.
  2. dbName may be set if necessary. It depends on limiter type.
  3. tableName may be set if all limits data should be stored in one table.
  4. storeType should be set to 'knex', if it is used.

Other notes:

  1. ExpressBruteFlexible always works with refreshTimeoutOnRequest=false option.
  2. it works only with seconds since rate-limiter-flexible duration is in seconds. For example, if minWait=500 it is 1 second.

Benchmark

Express app is launched in 4 processes with redis:5.0.4-alpine in Docker container.

const opts = {
  freeRetries: 20,
  minWait: 1000,
  maxWait: 10000,
  lifetime: 30,
// the next option is for ExpressBrute, as ExpressBruteFlexible works only with fixed window
  refreshTimeoutOnRequest: false,
};

./bombardier -c 500 -l -d 30s -r 1000 -t 5s shoots two endpoints limited by ExpressBrute and ExpressBruteFlexible 1000 times per second during 30 seconds. Every request is assigned with one of 100 random keys.

ExpressBrute

Statistics        Avg      Stdev        Max
  Reqs/sec       997.49     275.09    3663.47
  Latency        5.97ms     6.07ms    84.51ms
  Latency Distribution
     50%     4.17ms
     75%     5.24ms
     90%    10.14ms
     95%    16.06ms
     99%    37.72ms
  HTTP codes:
    1xx - 0, 2xx - 2884, 3xx - 0, 4xx - 27109, 5xx - 0

ExpressBruteFlexible

Statistics        Avg      Stdev        Max
  Reqs/sec      1000.76     440.44    3796.22
  Latency       23.94ms    19.53ms   204.91ms
  Latency Distribution
     50%    16.79ms
     75%    28.64ms
     90%    49.46ms
     95%    66.53ms
     99%   105.55ms
  HTTP codes:
    1xx - 0, 2xx - 2601, 3xx - 0, 4xx - 27395, 5xx - 0

Conclusion

Atomic increments slow down requests processing. If your authorization endpoint processes more than 1000 requests per second, test ExpressBruteFlexible before going to production with it.