Rate limiting e proteção contra brute force
1. Conceitos Fundamentais de Rate Limiting
Rate limiting é uma técnica de controle de tráfego que limita o número de requisições que um cliente pode fazer a um servidor em um determinado período de tempo. É essencial para segurança porque protege contra ataques de brute force, DDoS e abuso de API.
Diferenças importantes:
- Rate limiting: limita a taxa de requisições (ex.: 100 requisições/minuto)
- Throttling: reduz a velocidade de processamento após um limite
- Backpressure: mecanismo onde o consumidor sinaliza ao produtor para reduzir a taxa de envio
Ataques de brute force tentam adivinhar senhas testando milhares de combinações por segundo. Sem rate limiting, um atacante pode testar 1 milhão de senhas em minutos. DDoS usa múltiplos clientes para sobrecarregar o servidor.
2. Estratégias Comuns de Rate Limiting
Fixed Window
Divide o tempo em janelas fixas (ex.: 1 minuto) e conta requisições dentro de cada janela.
Janela: 00:00 - 00:01 | Contagem: 50 requisições
Janela: 00:01 - 00:02 | Contagem: 100 requisições (bloqueado após limite)
Vantagem: Simples de implementar. Desvantagem: Pode permitir picos no final de uma janela e início da próxima.
Sliding Window Log
Mantém um log de timestamps de cada requisição. Calcula a contagem baseada em um intervalo contínuo.
Timestamp: 12:00:01, 12:00:15, 12:00:30, 12:00:45
Intervalo: últimos 60 segundos -> 4 requisições
Vantagem: Mais preciso. Desvantagem: Maior consumo de memória.
Token Bucket
Um bucket com tokens que são reabastecidos a uma taxa constante. Cada requisição consome um token. Se não há tokens, a requisição é bloqueada.
Bucket: 10 tokens
Taxa: 1 token/segundo
Requisição consome 1 token -> 9 tokens restantes
Vantagem: Permite bursts controlados. Desvantagem: Complexidade de implementação.
3. Implementação Prática com Middleware
Rate limiting em APIs REST com Express.js
// Instalação: npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// Limite global: 100 requisições por minuto
const globalLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 100,
message: { error: 'Muitas requisições. Tente novamente em 1 minuto.' }
});
// Limite por endpoint de login: 5 tentativas por minuto
const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
keyGenerator: (req) => req.ip + ':' + req.body.username,
message: { error: 'Muitas tentativas de login. Aguarde 1 minuto.' }
});
app.use('/api', globalLimiter);
app.post('/login', loginLimiter, (req, res) => {
// Lógica de login
});
Rate limiting em APIs GraphQL com Apollo Server
const { ApolloServer } = require('apollo-server-express');
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)], // Máximo 5 níveis de profundidade
context: ({ req }) => {
// Rate limiting por IP
const ip = req.ip;
// Implementar contagem de requisições por usuário
return { ip, userId: req.user?.id };
}
});
4. Proteção Específica contra Brute Force em Login
Limitação por tentativas de login
// Estratégia: contagem por usuário e por IP
const loginAttempts = {};
function checkLoginAttempts(username, ip) {
const key = `${username}:${ip}`;
const now = Date.now();
if (!loginAttempts[key]) {
loginAttempts[key] = { count: 0, firstAttempt: now };
}
const attempts = loginAttempts[key];
// Reset após 15 minutos
if (now - attempts.firstAttempt > 15 * 60 * 1000) {
attempts.count = 0;
attempts.firstAttempt = now;
}
if (attempts.count >= 5) {
return false; // Bloqueado
}
attempts.count++;
return true;
}
Delay progressivo (exponential backoff)
function getDelay(attemptCount) {
// 1, 2, 4, 8, 16 segundos...
return Math.min(1000 * Math.pow(2, attemptCount - 1), 30000);
}
// Após 3 falhas consecutivas: delay de 4 segundos
// Após 5 falhas: delay de 16 segundos
CAPTCHA após falhas consecutivas
if (failedAttempts >= 3) {
// Exibir CAPTCHA no frontend
return { requireCaptcha: true };
}
5. Headers e Respostas para Rate Limiting
// Configuração de headers padrão
const limiter = rateLimit({
// ...
headers: true, // Habilita headers automáticos
standardHeaders: true, // X-RateLimit-*
legacyHeaders: false // Desabilita X-RateLimit-Limit (legado)
});
// Resposta 429 com Retry-After
app.use((err, req, res, next) => {
if (err.status === 429) {
res.set('Retry-After', Math.ceil(err.retryAfter / 1000));
res.status(429).json({
error: 'Muitas requisições',
retryAfter: err.retryAfter
});
}
});
Headers recomendados:
- X-RateLimit-Limit: limite máximo de requisições
- X-RateLimit-Remaining: requisições restantes
- X-RateLimit-Reset: timestamp de reset do limite
6. Armazenamento e Escalabilidade do Rate Limiting
Uso de Redis para contagem distribuída
const Redis = require('ioredis');
const redis = new Redis();
async function checkRateLimit(key, limit, windowMs) {
const current = await redis.incr(key);
if (current === 1) {
await redis.pexpire(key, windowMs);
}
return current <= limit;
}
// Uso
const allowed = await checkRateLimit('user:123:login', 5, 60000);
Alternativas
- Redis: Baixa latência, atômico, suporta TTL, ideal para múltiplas instâncias
- Banco relacional: Tabela de logs com índices, mas mais lento
- Memória local: Rápido, mas não escalável entre instâncias
7. Evitando Bypass e Ataques Avançados
Identificação precisa do cliente
// Considerar proxy reverso
const clientIp = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
// Combinar múltiplos fatores
const clientKey = `${clientIp}:${req.headers['user-agent']}`;
// Para usuários autenticados
const userKey = `user:${req.user.id}`;
Proteção contra distributed brute force
// Rate limit por IP + por usuário + por rota
const ipLimiter = rateLimit({ windowMs: 60000, max: 100 });
const userLimiter = rateLimit({ windowMs: 60000, max: 10 });
const routeLimiter = rateLimit({ windowMs: 60000, max: 5 });
app.use('/api', ipLimiter);
app.use('/api/user', userLimiter);
app.post('/api/reset-password', routeLimiter);
Logging e monitoramento
// Registrar tentativas de brute force
app.post('/login', (req, res) => {
const ip = req.ip;
const username = req.body.username;
if (loginFailed) {
logger.warn(`Tentativa de brute force: IP=${ip}, Usuário=${username}`);
}
});
8. Testes e Validação da Implementação
Testes com k6
// script.js - Teste de rate limiting
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
for (let i = 0; i < 10; i++) {
const res = http.post('http://localhost:3000/login', {
username: 'user',
password: 'pass'
});
if (res.status === 429) {
console.log('Rate limit atingido');
break;
}
sleep(0.1);
}
}
Simulação de brute force em staging
# Comando para simular 100 tentativas em 5 segundos
for i in {1..100}; do
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"senha'$i'"}' &
done
Verificação de falsos positivos
- Monitore logs de bloqueio
- Analise padrões de uso real
- Ajuste limites baseado em métricas (ex.: 95º percentil de requisições)
Referências
- Express Rate Limit - Documentação Oficial — Middleware de rate limiting para Express.js com exemplos de configuração e personalização
- OWASP - Rate Limiting Guide — Guia oficial da OWASP sobre rate limiting e prevenção de DoS
- Redis - Rate Limiting Patterns — Documentação oficial sobre uso de Redis para rate limiting com exemplos de implementação
- K6 - Load Testing Tool — Ferramenta de testes de carga com guia específico para testar rate limiting
- GitHub - graphql-depth-limit — Biblioteca para limitar profundidade de queries GraphQL, prevenindo ataques de brute force
- Cloudflare - Rate Limiting Best Practices — Guia da Cloudflare sobre melhores práticas de rate limiting em produção
- Auth0 - Brute Force Protection Strategies — Artigo técnico sobre estratégias de proteção contra brute force em autenticação