express-brute、express-limiter、express-rate-limit、express-slow-down、rate-limiter-flexible は、すべて Express.js アプリケーションでレート制限(アクセス制限)を実装するためのミドルウェアパッケージです。これらは API エンドポイントへの過剰なリクエストを防ぎ、DDoS 攻撃やブルートフォース攻撃からアプリケーションを保護します。express-rate-limit が最も広く採用されており、rate-limiter-flexible は柔軟なストア対応が特徴です。express-slow-down はリクエストをブロックせず速度を落とすアプローチを取ります。
Express.js アプリケーションで API 保護を実装する際、適切なレート制限パッケージの選択はセキュリティとパフォーマンスに直結します。express-brute、express-limiter、express-rate-limit、express-slow-down、rate-limiter-flexible の 5 つのパッケージは、それぞれ異なるアプローチと特徴を持っています。実務での選択基準を明確にするために、技術的な違いを深掘りします。
express-brute は 2015 年頃から存在する古いパッケージで、現在はアクティブなメンテナンスが行われていません。
// express-brute: 古い API スタイル
const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore();
const bruteforce = new ExpressBrute(store);
app.post('/login', bruteforce.prevent, loginHandler);
express-limiter も同様に更新が停滞しており、Redis 特化型の制限ミドルウェアです。
// express-limiter: Redis 必須
const expressLimiter = require('express-limiter');
const redis = require('redis');
const client = redis.createClient();
expressLimiter(app, client, {
lookup: 'connection.remoteAddress',
total: 100,
expire: 1000 * 60 * 60
});
express-rate-limit は現在もアクティブにメンテナンスされており、npm で最もダウンロードされています。
// express-rate-limit: 現代的な API
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'リクエストが多すぎます'
});
app.use('/api/', limiter);
express-slow-down は express-rate-limit の作者によって作成され、同じエコシステムで動作します。
// express-slow-down: 遅延による制限
const slowDown = require('express-slow-down');
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50,
delayMs: 500
});
app.use('/api/', speedLimiter);
rate-limiter-flexible は最新の設計で、複数のストレージバックエンドを公式にサポートしています。
// rate-limiter-flexible: 柔軟なストア対応
const { RateLimiterRedis } = require('rate-limiter-flexible');
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 10,
duration: 1
});
app.use('/api/', (req, res, next) => {
rateLimiter.consume(req.ip)
.then(() => next())
.catch(() => res.status(429).send('制限超過');
});
ストレージ選択は、アプリケーションのスケールに直接影響します。
| パッケージ | メモリ | Redis | MongoDB | Memcached | その他 |
|---|---|---|---|---|---|
express-brute | ✅ | ✅ | ❌ | ❌ | カスタムストア可能 |
express-limiter | ❌ | ✅ | ❌ | ❌ | Redis のみ |
express-rate-limit | ✅ | ✅ | ✅ | ✅ | 公式ストア多数 |
express-slow-down | ✅ | ✅ | ✅ | ✅ | express-rate-limit と共通 |
rate-limiter-flexible | ✅ | ✅ | ✅ | ✅ | 15+ ストア対応 |
express-rate-limit のストア設定例:
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');
const limiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redisClient.sendCommand(args)
}),
windowMs: 15 * 60 * 1000,
max: 100
});
rate-limiter-flexible のストア設定例:
const { RateLimiterMongo } = require('rate-limiter-flexible');
const { MongoClient } = require('mongodb');
const rateLimiter = new RateLimiterMongo({
storeClient: mongoClient,
tableName: 'rateLimits',
points: 10,
duration: 1
});
各パッケージは異なる方法でリクエストをカウントし、制限を適用します。
express-brute は失敗カウントに特化しており、ログイン試行などのブルートフォース対策に適しています。
// express-brute: 失敗時のみカウント
bruteforce.handleFailures((req, res, next, nextValidRequestDate) => {
if (req.body.username === 'admin') {
return res.status(403).send('アカウントロック中');
}
next();
});
express-limiter は単純なカウントベースで、Redis の TTL 機能を利用します。
// express-limiter: シンプルカウント
expressLimiter(app, client, {
lookup: 'headers.api-key',
total: 1000,
expire: 1000 * 60 * 60,
onRateLimited: (req, res) => {
res.status(429).send('レート制限超過');
}
});
express-rate-limit は柔軟なキー生成とカスタムメッセージをサポートします。
// express-rate-limit: カスタムキー生成
const limiter = rateLimit({
keyGenerator: (req) => {
return req.user?.id || req.ip;
},
handler: (req, res) => {
res.status(429).json({
error: '制限超過',
retryAfter: Math.ceil(res.get('Retry-After'))
});
}
});
express-slow-down はブロックではなく遅延を適用します。
// express-slow-down: 段階的遅延
const speedLimiter = slowDown({
delayAfter: 50,
delayMs: (hits) => Math.min(hits * 100, 60000),
skipFailedRequests: true
});
rate-limiter-flexible はポイントベースのシステムで、異なるアクションに異なるコストを設定できます。
// rate-limiter-flexible: ポイントベース
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 100,
duration: 60
});
// 重い操作は更多ポイント消費
await rateLimiter.consume(userId, 5); // 5 ポイント消費
制限超過時のレスポンス制御は、ユーザー体験に影響します。
express-brute はカスタムエラーハンドラを登録します:
bruteforce.getMiddleware({
onBlocked: (req, res, next, nextValidRequestDate) => {
res.status(429).json({
error: '一時的にブロックされています',
retryAfter: nextValidRequestDate
});
}
});
express-rate-limit は標準 HTTP ヘッダーを自動設定します:
// 自動設定されるヘッダー
// X-RateLimit-Limit: 100
// X-RateLimit-Remaining: 95
// X-RateLimit-Reset: 1640000000
// Retry-After: 60
rate-limiter-flexible は詳細な制限情報を返します:
try {
const response = await rateLimiter.consume(key);
res.set('Retry-After', Math.ceil(response.msBeforeNext / 1000));
next();
} catch (rej) {
res.set('Retry-After', Math.ceil(rej.msBeforeNext / 1000));
res.status(429).json({
pointsRemaining: rej.remainingPoints,
msBeforeNext: rej.msBeforeNext
});
}
推奨:express-rate-limit
設定がシンプルで、デフォルトのメモリストアでも十分な場合:
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', limiter);
推奨:express-rate-limit または rate-limiter-flexible
複数サーバー環境で Redis を使用する場合:
// express-rate-limit + Redis
const limiter = rateLimit({
store: new RedisStore({ sendCommand: redisClient.sendCommand }),
windowMs: 15 * 60 * 1000,
max: 100
});
// rate-limiter-flexible + Redis
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 100,
duration: 900
});
推奨:rate-limiter-flexible
異なるエンドポイントで異なる制限を適用する場合:
const loginLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 5,
duration: 300
});
const apiLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 1000,
duration: 3600
});
app.post('/login', async (req, res, next) => {
try {
await loginLimiter.consume(req.ip);
next();
} catch {
res.status(429).send('ログイン試行が多すぎます');
}
});
推奨:express-slow-down + express-rate-limit 併用
いきなりブロックせず、段階的に制限:
const limiter = rateLimit({ windowMs: 900000, max: 100 });
const speedLimiter = slowDown({ windowMs: 900000, delayAfter: 50, delayMs: 200 });
app.use('/api/', speedLimiter);
app.use('/api/', limiter);
express-brute と express-limiter は、新しいプロジェクトでの使用を避けるべきです:
既存システムで使用している場合は、移行計画を立てることを推奨します:
// 移行例:express-brute → express-rate-limit
// 旧コード
app.post('/login', bruteforce.prevent, loginHandler);
// 新コード
const loginLimiter = rateLimit({
windowMs: 5 * 60 * 1000,
max: 5,
message: 'ログイン試行が多すぎます'
});
app.post('/login', loginLimiter, loginHandler);
| 機能 | express-brute | express-limiter | express-rate-limit | express-slow-down | rate-limiter-flexible |
|---|---|---|---|---|---|
| メンテナンス | ❌ 停滞 | ❌ 停滞 | ✅ アクティブ | ✅ アクティブ | ✅ アクティブ |
| Redis 対応 | ✅ | ✅ | ✅ | ✅ | ✅ |
| メモリストア | ✅ | ❌ | ✅ | ✅ | ✅ |
| カスタムキー | ⚠️ 限定的 | ⚠️ 限定的 | ✅ | ✅ | ✅ |
| ポイントシステム | ❌ | ❌ | ❌ | ❌ | ✅ |
| 遅延機能 | ❌ | ❌ | ❌ | ✅ | ⚠️ 要実装 |
| TypeScript | ❌ | ❌ | ✅ | ✅ | ✅ |
| 新規推奨 | ❌ | ❌ | ✅ | ✅ | ✅ |
新規プロジェクトでの選択フロー:
express-rate-limitexpress-rate-limit または rate-limiter-flexiblerate-limiter-flexibleexpress-slow-down + express-rate-limitexpress-rate-limit へ重要な注意点:
express-brute と express-limiter は新規プロジェクトで使用しないでくださいレート制限パッケージの選択は、アプリケーションの規模、インフラ構成、セキュリティ要件によって異なります。express-rate-limit が最もバランスの取れた選択肢ですが、大規模システムでは rate-limiter-flexible の柔軟性が活きます。ユーザー体験を重視する場合は express-slow-down の併用を検討してください。
express-brute は古いパッケージで、現在はメンテナンスが停滞しています。ブルートフォース攻撃対策に特化していますが、新しいプロジェクトでの使用は推奨されません。既存のレガシーシステムで既に導入されている場合のみ検討し、新規プロジェクトでは express-rate-limit や rate-limiter-flexible の評価を推奨します。
express-limiter は Redis をストレージバックエンドとして使用する場合に適しています。Redis 環境が既に整っており、シンプルな設定で済ませたいプロジェクトに向いています。ただし、複数のストレージオプションを必要とする場合は rate-limiter-flexible の方が柔軟です。
express-rate-limit は最も成熟しており、コミュニティサポートが厚いパッケージです。シンプルな設定で始められ、メモリストアから Redis まで対応しています。新規プロジェクトで迷った場合の第一選択肢として推奨され、ドキュメントも充実しています。
express-slow-down はリクエストを完全にブロックせず、応答を遅延させるアプローチを取ります。ユーザー体験を損なわずに悪意のあるトラフィックを抑制したい場合に適しています。express-rate-limit と併用して、段階的な制限を実現できます。
rate-limiter-flexible は最も柔軟で、Redis、Memcached、MongoDB など複数のストアをサポートします。マイクロサービス環境や複雑な制限ロジックが必要な場合に適しています。学習コストはやや高いですが、大規模システムでの拡張性が高いです。
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.MemoryStoreattachResetToRequest parameter that lets you prevent the request object being decoratedfailCallback can be overriden by getMiddlewareproxyDepth 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 ExpressBrutereq 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.