API Rate Limiting & Security Headers
API Rate Limiting & Security Headers
O sistema de rate limiting protege a plataforma ForestWatch contra abuso de API, ataques de força bruta e enumeração de recursos. Utiliza um algoritmo de sliding window in-memory com integração transparente nos wrappers enhanceAction e enhanceRouteHandler.
Security headers são aplicados globalmente via middleware (proxy.ts) para proteger contra ataques comuns de web.
Componentes
| Componente | Caminho | Descrição |
|---|---|---|
RateLimitStore (port) | packages/fw/rate-limit/src/rate-limit-store.ts | Interface para armazenamento de contadores |
InMemoryRateLimitStore | packages/fw/rate-limit/src/in-memory-store.ts | Adapter in-memory com sliding window |
resolveRateLimitConfig | packages/fw/rate-limit/src/rate-limiter.ts | Resolução de config com tiers e overrides |
checkRateLimit | packages/fw/rate-limit/src/rate-limiter.ts | Verificação de rate limit contra o store |
getClientIdentifier | packages/fw/rate-limit/src/client-identifier.ts | Extração de IP do cliente via headers |
rateLimitHeaders | packages/fw/rate-limit/src/headers.ts | Geração de headers de rate limit na resposta |
SECURITY_HEADERS | packages/fw/rate-limit/src/security-headers.ts | Constantes de security headers |
applySecurityHeaders | packages/fw/rate-limit/src/security-headers.ts | Aplicação de security headers em respostas |
getRateLimitStore | packages/fw/rate-limit/src/store-instance.ts | Singleton do store compartilhado |
Como Funciona o Sliding Window
O sliding window é um algoritmo de rate limiting que controla quantas requisições um cliente pode fazer dentro de uma janela de tempo. Cada cliente é identificado pelo IP (extraído dos headers x-forwarded-for, x-real-ip, ou fallback 127.0.0.1).
Fluxo de uma Requisição
Requisição chega
│
▼
Extrai IP do cliente (x-forwarded-for → x-real-ip → 127.0.0.1)
│
▼
Monta chave: "route:/api/auth/check-email:10.0.0.1"
│
▼
Busca entrada no Map
│
├── Não existe ou janela expirou → Cria nova janela (count=1)
│
└── Janela ativa → Incrementa count
│
├── count ≤ limit → Permitido (retorna remaining)
│
└── count > limit → Bloqueado (retorna retryAfter)Exemplo Prático (tier strict: 5 req/60s)
Tempo 0s: Req 1 → count=1, allowed=true, remaining=4
Tempo 2s: Req 2 → count=2, allowed=true, remaining=3
Tempo 5s: Req 3 → count=3, allowed=true, remaining=2
Tempo 8s: Req 4 → count=4, allowed=true, remaining=1
Tempo 10s: Req 5 → count=5, allowed=true, remaining=0
Tempo 12s: Req 6 → count=6, allowed=false, retryAfter=48s ← BLOQUEADO
...
Tempo 60s: Janela expira, contador reseta
Tempo 61s: Req 7 → count=1, allowed=true, remaining=4 ← Nova janelaComplexidade
- Verificação: O(1) — lookup direto no
Map - Incremento: O(1) — atualização in-place
- Cleanup: O(n) — roda a cada 60s, remove entradas com mais de 5 minutos
Tiers de Rate Limiting
| Tier | Requisições | Janela | Uso |
|---|---|---|---|
strict | 5 | 60 segundos | Autenticação, validação de email |
standard | 30 | 60 segundos | APIs gerais, validação de slug, chat |
lenient | 60 | 60 segundos | Health check, endpoints de leitura |
Endpoints Protegidos
| Endpoint | Tier | Motivo |
|---|---|---|
/api/auth/check-email | strict | Previne enumeração de emails |
/api/onboarding/validate-email | strict | Previne enumeração de emails |
/api/test-email | strict | Previne abuso de envio de email |
/api/onboarding/validate-slug | standard | Previne enumeração de slugs |
/api/projects/validate-slug | standard | Previne enumeração de slugs |
/api/chat | standard | Limita uso do AI assistant |
/api/mobile/onboarding | standard | Proteção geral |
/api/health | lenient | Permite monitoramento frequente |
Endpoints Isentos
Webhooks do Stripe e DB (/api/billing/webhook, /api/billing/stripe-webhook, /api/db/webhook) são isentos de rate limiting pois são verificados por assinatura.
Security Headers
Quatro headers de segurança são aplicados em todas as respostas via proxy.ts:
| Header | Valor | Proteção |
|---|---|---|
X-Content-Type-Options | nosniff | Impede MIME sniffing |
X-Frame-Options | DENY | Bloqueia carregamento em iframes (clickjacking) |
Referrer-Policy | strict-origin-when-cross-origin | Limita informação no header Referer |
X-DNS-Prefetch-Control | off | Desabilita prefetch de DNS |
Por que cada header importa
X-Content-Type-Options: nosniff — Sem esse header, um atacante poderia fazer upload de um arquivo .txt contendo JavaScript. O browser poderia interpretar o conteúdo como script e executá-lo, mesmo que o Content-Type diga text/plain.
X-Frame-Options: DENY — Sem esse header, um atacante poderia embutir o ForestWatch num iframe invisível em outro site. O usuário pensaria estar clicando em algo inofensivo, mas estaria interagindo com o ForestWatch (clickjacking).
Referrer-Policy: strict-origin-when-cross-origin — Quando o usuário navega para um site externo, o browser envia o header Referer. Sem essa policy, o path completo seria enviado (ex: https://app.forestwatch.com/projects/123/settings), potencialmente vazando IDs e informações sensíveis.
X-DNS-Prefetch-Control: off — Browsers modernos fazem prefetch de DNS para links na página. Um observador de rede poderia inferir o conteúdo da página baseado nos domínios resolvidos.
Integração com enhanceAction e enhanceRouteHandler
Route Handlers (API Routes)
import { enhanceRouteHandler } from '@kit/next/routes';
export const POST = enhanceRouteHandler(
async function ({ request, body, user }) {
return NextResponse.json({ success: true });
},
{
auth: false,
rateLimit: { tier: 'strict' },
},
);Quando bloqueado, retorna status 429 com:
{
"error": "Too many requests",
"code": "RATE_LIMIT_EXCEEDED"
}E headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After.
Server Actions
import { enhanceAction } from '@kit/next/actions';
export const myAction = enhanceAction(
async function myAction(data, user) {
return { success: true };
},
{
schema: MySchema,
rateLimit: { tier: 'standard' },
},
);Quando bloqueado, lança Error('Too many requests. Please try again later.').
Opções de Configuração
// Usar tier pré-definido
rateLimit: { tier: 'strict' }
// Usar tier com override parcial
rateLimit: { tier: 'strict', maxRequests: 10 }
// Config totalmente customizada
rateLimit: { maxRequests: 100, windowMs: 120_000 }
// Usar defaults (tier standard)
rateLimit: true
// Sem rate limiting (padrão)
// Simplesmente não incluir a propriedadeFail-Open
Se o store de rate limiting falhar por qualquer motivo (erro de memória, bug, etc.), a requisição é permitida. Isso garante que um problema no rate limiting não derrube a aplicação.
Store falha → Log de erro → Requisição permitida → Aplicação continuaDesabilitar Rate Limiting
Para desenvolvimento local, defina a variável de ambiente:
RATE_LIMIT_DISABLED=trueIsso faz com que todas as verificações de rate limit sejam puladas. A variável está registrada no turbo.json globalEnv.
Configuração Centralizada
Toda configuração de tiers está em apps/web/config/api.config.ts:
export const API_RATE_LIMIT_TIERS = {
strict: { maxRequests: 5, windowMs: 60_000 },
standard: { maxRequests: 30, windowMs: 60_000 },
lenient: { maxRequests: 60, windowMs: 60_000 },
} as const;
export const API_RATE_LIMITING = {
enabled: process.env.RATE_LIMIT_DISABLED !== 'true',
tiers: API_RATE_LIMIT_TIERS,
} as const;Headers de Resposta
Toda resposta de endpoint com rate limiting inclui:
| Header | Descrição | Exemplo |
|---|---|---|
X-RateLimit-Limit | Limite total da janela | 5 |
X-RateLimit-Remaining | Requisições restantes | 3 |
X-RateLimit-Reset | Timestamp Unix de reset | 1707667260 |
Retry-After | Segundos até poder tentar (só quando bloqueado) | 47 |
Testes
O pacote possui 56 testes cobrindo:
- 7 suítes de testes de propriedade (fast-check, 100+ iterações cada)
- 1 suíte de testes unitários
- 4 suítes de testes de integração
Propriedades Verificadas
- Sliding window respeita limites
- Cadeia de prioridade do client identifier
- Headers de rate limit corretos
- Resolução de config com defaults e overrides
- Rate limiting bloqueia actions após exceder limite
- Rate limiting bloqueia route handlers após exceder limite
- Fail-open em erros do store
- Limpeza de entradas expiradas
- Rate limiting desabilitado permite tudo
- Security headers presentes em todas as respostas