Rate Limiting & DDoS
Protégez vos API contre le brute force, le scraping et les attaques volumétriques.
01 Pourquoi rate limiter ?
Une API sans rate limiting est exposée à plusieurs classes d'attaques :
| Attaque | Description | Impact |
|---|---|---|
| Brute force | Des milliers de tentatives de mot de passe sur /login | Compromission de comptes |
| Credential stuffing | Rejouer des paires email/mdp issues de fuites | Account takeover massif |
| Scraping | Récupération automatique de tout le catalogue | Vol de données, coûts serveur |
| DDoS applicatif | Saturer les endpoints lourds (recherche, rapports) | Indisponibilité du service |
| API abuse | Dépasser les quotas d'usage prévu (coûts cloud) | Surcoûts imprévisibles |
02 Stratégies de rate limiting
🪟 Fixed Window
Compte les requêtes sur une fenêtre fixe (ex: 100 req/minute). Simple mais vulnérable aux bursts en fin/début de fenêtre.
🌊 Sliding Window
Fenêtre glissante — compte les N dernières secondes en temps réel. Lisse les pics, plus précis que Fixed.
🪣 Token Bucket
Chaque client a un "seau" de tokens. Chaque requête consomme 1 token. Le seau se remplit progressivement. Autorise les bursts contrôlés.
💧 Leaky Bucket
Les requêtes entrent dans une file FIFO à débit constant. Lisse parfaitement le trafic. Idéal pour les APIs à débit garanti.
| Stratégie | Burst autorisé | Complexité | Usage typique |
|---|---|---|---|
| Fixed Window | Oui (en transition) | Simple | APIs simples, MVP |
| Sliding Window | Non | Modérée | Auth, APIs sensibles |
| Token Bucket | Oui (contrôlé) | Modérée | APIs publiques, SDK |
| Leaky Bucket | Non | Élevée | Intégrations, webhooks |
03 express-rate-limit — configuration
const rateLimit = require('express-rate-limit'); // Limiter global : 100 req / 15 min par IP const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, message: { error: 'Trop de requêtes, réessayez dans 15 min.' }, standardHeaders: true, // Renvoie RateLimit-* headers (RFC 6585) legacyHeaders: false, // Désactive X-RateLimit-* legacy }); // Limiter auth : 5 req / 15 min par IP (brute force) const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, message: { error: 'Trop de tentatives de connexion.' }, skipSuccessfulRequests: true, // Ne compte pas les connexions réussies }); app.use(globalLimiter); app.use('/api/auth', authLimiter);
Options importantes
| Option | Type | Description |
|---|---|---|
windowMs | number | Durée de la fenêtre en millisecondes |
max | number | fn | Nombre max de requêtes par fenêtre |
keyGenerator | function | Clé d'identification du client (défaut: IP) |
skip | function | Retourner true pour ignorer cette requête |
skipSuccessfulRequests | boolean | Ne compte pas les réponses 2xx |
handler | function | Handler personnalisé quand la limite est atteinte |
store | Store | Backend de stockage (mémoire, Redis…) |
04 Rate limiting différencié par route
Toutes les routes n'ont pas le même profil de risque. Une bonne stratégie différencie :
// Routes d'authentification — très restrictif (brute force) const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 min max: 5, keyGenerator: (req) => req.body?.email || req.ip, // Par email + IP }); // API données — modéré const dataLimiter = rateLimit({ windowMs: 60 * 1000, // 1 min max: 100, }); // Endpoints publics (documentation, statut) const publicLimiter = rateLimit({ windowMs: 60 * 1000, max: 500, }); app.use('/auth/login', authLimiter); app.use('/auth/register', authLimiter); app.use('/api/data', dataLimiter); app.use('/docs', publicLimiter); // keyGenerator par utilisateur authentifié const perUserLimiter = rateLimit({ keyGenerator: (req) => req.user?.id || req.ip, max: 1000, windowMs: 60 * 60 * 1000, // 1h });
05 Redis pour le rate limiting distribué
Par défaut, express-rate-limit stocke les compteurs en mémoire — chaque instance de votre serveur a son propre état. Avec 3 instances derrière un load balancer, un client peut envoyer 3× la limite autorisée.
const { RedisStore } = require('rate-limit-redis'); const { createClient } = require('redis'); const redisClient = createClient({ url: process.env.REDIS_URL }); await redisClient.connect(); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args), }), });
| Stockage | Multi-instance | Performance | Usage |
|---|---|---|---|
| Mémoire (défaut) | Non | Très rapide | Développement, instance unique |
| Redis | Oui | Rapide (sub-ms) | Production, clusters |
| MongoDB/SQL | Oui | Modérée | Si Redis non disponible |