Skip to content

ExpressBrute migration

Roman edited this page Apr 8, 2019 · 23 revisions

rate-limiter-flexible package provides options and API compatible middleware ExpressBruteFlexible, which makes it easy to migrate from non-maintained and vulnerable to race conditions ExpressBrute package.

ExpressBruteFlexible implements the same logic, but with atomic increments instead of get/set, which may get you into trouble in some cases. Note, it works slower on high traffic, as atomic increments cost some performance on store level.

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

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

const opts = {
  freeRetries: 10,
  minWait: 1000,
  maxWait: 10000,
  lifetime: 30,
  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!');
	}
);

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,
  refreshTimeoutOnRequest: false, // this is set for ExpressBrute, as ExpressBruteFlexible works only with fixed window
};

./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 500 requests per second, test ExpressBruteFlexible before going to production with it.