Global WatchGlobal Watch Docs
Features

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

ComponenteCaminhoDescrição
RateLimitStore (port)packages/fw/rate-limit/src/rate-limit-store.tsInterface para armazenamento de contadores
InMemoryRateLimitStorepackages/fw/rate-limit/src/in-memory-store.tsAdapter in-memory com sliding window
resolveRateLimitConfigpackages/fw/rate-limit/src/rate-limiter.tsResolução de config com tiers e overrides
checkRateLimitpackages/fw/rate-limit/src/rate-limiter.tsVerificação de rate limit contra o store
getClientIdentifierpackages/fw/rate-limit/src/client-identifier.tsExtração de IP do cliente via headers
rateLimitHeaderspackages/fw/rate-limit/src/headers.tsGeração de headers de rate limit na resposta
SECURITY_HEADERSpackages/fw/rate-limit/src/security-headers.tsConstantes de security headers
applySecurityHeaderspackages/fw/rate-limit/src/security-headers.tsAplicação de security headers em respostas
getRateLimitStorepackages/fw/rate-limit/src/store-instance.tsSingleton 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 janela

Complexidade

  • 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

TierRequisiçõesJanelaUso
strict560 segundosAutenticação, validação de email
standard3060 segundosAPIs gerais, validação de slug, chat
lenient6060 segundosHealth check, endpoints de leitura

Endpoints Protegidos

EndpointTierMotivo
/api/auth/check-emailstrictPrevine enumeração de emails
/api/onboarding/validate-emailstrictPrevine enumeração de emails
/api/test-emailstrictPrevine abuso de envio de email
/api/onboarding/validate-slugstandardPrevine enumeração de slugs
/api/projects/validate-slugstandardPrevine enumeração de slugs
/api/chatstandardLimita uso do AI assistant
/api/mobile/onboardingstandardProteção geral
/api/healthlenientPermite 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:

HeaderValorProteção
X-Content-Type-OptionsnosniffImpede MIME sniffing
X-Frame-OptionsDENYBloqueia carregamento em iframes (clickjacking)
Referrer-Policystrict-origin-when-cross-originLimita informação no header Referer
X-DNS-Prefetch-ControloffDesabilita 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 propriedade

Fail-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 continua

Desabilitar Rate Limiting

Para desenvolvimento local, defina a variável de ambiente:

RATE_LIMIT_DISABLED=true

Isso 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:

HeaderDescriçãoExemplo
X-RateLimit-LimitLimite total da janela5
X-RateLimit-RemainingRequisições restantes3
X-RateLimit-ResetTimestamp Unix de reset1707667260
Retry-AfterSegundos 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

  1. Sliding window respeita limites
  2. Cadeia de prioridade do client identifier
  3. Headers de rate limit corretos
  4. Resolução de config com defaults e overrides
  5. Rate limiting bloqueia actions após exceder limite
  6. Rate limiting bloqueia route handlers após exceder limite
  7. Fail-open em erros do store
  8. Limpeza de entradas expiradas
  9. Rate limiting desabilitado permite tudo
  10. Security headers presentes em todas as respostas

Documentação Relacionada

On this page