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