Skip to content

Rate-Limits e Proteção Anti-Brute-Force

Consolidação de RATE_LIMIT_SECURITY_REPORT.md — Última atualização: 2026-03-10 (v4)


Resumo

ComponenteRate-LimitBurst In-MemoryLockoutAnti-TimingAlertas PushCleanupObservabilidade
Sistema Principal (send-otp)✅ Por usuário + IP✅ 15 min fixo✅ 500-1000ms✅ Falha WhatsAppmaintenance-cron
Sistema Principal (verify-otp)✅ 5 tentativas✅ 15 min fixo✅ 500-1000ms✅ Brute Forcemaintenance-cron
Portal (portal-escola)✅ Dual (IP + escola_id)✅ 150/5s por IP✅ Progressivo✅ 500-1000ms✅ ntfy (lockout + OTP + pico)maintenance-cronportal_rate_metrics

1. Sistema Principal de Autenticação

1.1 send-otp — Envio de OTP

LimiteValorJanelaPropósito
OTPs por usuário3 máximo15 minutosEvitar spam de mensagens
OTPs por IP10 máximo1 horaEvitar abuso massivo
typescript
const MAX_OTPS_PER_USER = 3;
const OTP_WINDOW_MINUTES = 15;
const MAX_OTPS_PER_IP = 10;
const IP_WINDOW_HOURS = 1;

Bloqueio: Retorna HTTP 429 com mensagem descritiva.

1.2 verify-otp — Verificação de OTP

LimiteValorJanela
Tentativas falhas totais515 minutos
Tentativas por código OTP3Validade do OTP
typescript
const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_MINUTES = 15;
  • 3 erros no mesmo OTP → OTP invalidado permanentemente
  • 5 falhas totais → Bloqueio por IP (15 min) + log auth.bloqueio_ip

1.3 OTP — Parâmetros

ParâmetroValor
Validade5 minutos
HashSHA-256
Tabelalogin_otps
Cleanupmaintenance-cron (diário, registros > 24h)

1.4 JWT do Sistema

ParâmetroValor
AlgoritmoHS256
SecretOLP_JWT_SECRET
Expiração8 horas
Cookieolp_auth (HttpOnly, Secure, SameSite=None)

2. Mural Olímpico (Aluno/Responsável)

2.1 Arquitetura de Defesa em Camadas (v4)

Request → [Burst In-Memory 150/5s] → [portal_check_guards RPC] → [Lockout por Identificador]
              ↓ (bloqueado)                  ↓ (bloqueado)                ↓ (bloqueado)
          429 (zero DB)              429 (1 query DB)           429 (dados da mesma query)

Camadas em ordem de execução:

  1. Burst in-memory (150 req/5s por IP) — Bloqueia scripts triviais e loops acidentais sem custo de banco. Delegado ao Cloudflare Worker em produção; fallback Map<string, number[]> em dev.
  2. Rate limit por escola (200/min) — Controle principal contra ataques coordenados com múltiplos IPs.
  3. Rate limit por IP (400/5min) — Proteção secundária anti-bot, janela de 5 min suaviza picos de NAT.
  4. Lockout progressivo por identificador (3/6/10 falhas) — Defesa real contra brute force, bloqueios mais longos.

2.2 Rate-Limits — v4 (Conservador com Janelas Independentes)

Mudança v4 (2026-03-10): Valores mais conservadores + janelas independentes escola/IP.

  • RPC portal_check_guards agora aceita p_window_minutes_ip separado
  • Escola reduzida de 1400→200/min (margem 2.5x sobre pico real de 80/min)
  • IP reduzido de 1800/1min→400/5min (janela mais longa suaviza NAT sem expor superfície)
  • Lookup/Series reduzidos proporcionalmente (pré-login, menos permissivos)
  • Lockouts mais longos (2/10/60 min vs 1/5/30 min)
  • Alerta de pico: threshold reduzido de 80%→70%

Burst In-Memory

CamadaChaveLimiteJanelaImplementaçãoCusto DB
BurstIP1505 segCloudflare Worker (prod) / Map (dev)ZERO
typescript
const BURST_LIMIT_IP = { max: 150, windowMs: 5_000 };

Limitação aceita: Em dev/preview, o Map é por instância Deno isolate. Em produção, o Cloudflare Worker (processo único) garante contagem global.

Leitura Pública

AçãoChaveLimiteJanela
lookup_escolaIP1001 min
lookup_escolaescola_id2001 min
get_series_escolaIP1001 min
check_slug_availabilityIP601 min

Login Método A (Matrícula + Nascimento — sem custo SMS)

AçãoChaveLimiteJanela
login_aluno_aBurst IP1505 seg
login_aluno_aescola_id2001 min
login_aluno_aIP4005 min

Justificativa escola=200/min: Escola pico real ~80 req/min. 200 dá 2.5x margem. Justificativa IP=400/5min: Janela de 5 minutos suaviza picos de NAT denso sem expor superfície ampla. Alerta de pico: ntfy dispara quando escola atinge 70% do rate limit (throttled 1x/15min). RPC: portal_check_guards usa p_window_minutes=1 para escola e p_window_minutes_ip=5 para IP (janelas independentes).

Login Método B / OTP (custo de mensagens WhatsApp/SMS)

AçãoChaveLimiteJanela
send_otp_alunoescola_id10015 min
send_otp_alunoIP5015 min
send_otp_responsavelescola_id10015 min
send_otp_responsavelIP5015 min

Cadastro / Vínculo

AçãoChaveLimiteJanela
cadastro_responsavelIP3030 min
auto_vincular_matriculaIP3060 min
typescript
// v4 — Constantes conservadoras (2026-03-10)
const BURST_LIMIT_IP            = { max: 150, windowMs: 5_000 };
const RATE_LIMIT_LOOKUP_IP      = { max: 100, windowMinutes: 1 };
const RATE_LIMIT_LOOKUP_ESCOLA  = { max: 200, windowMinutes: 1 };
const RATE_LIMIT_SERIES_IP      = { max: 100, windowMinutes: 1 };
const RATE_LIMIT_LOGIN_A_ESCOLA = { max: 200, windowMinutes: 1 };
const RATE_LIMIT_LOGIN_A_IP     = { max: 400, windowMinutes: 5 };  // janela independente
const RATE_ALERT_THRESHOLD_PERCENT = 0.7;
const RATE_LIMIT_OTP_IP         = { max: 50, windowMinutes: 15 };
const RATE_LIMIT_OTP_ESCOLA     = { max: 100, windowMinutes: 15 };
const RATE_LIMIT_CHECK_SLUG     = { max: 60, windowMinutes: 1 };
const RATE_LIMIT_CADASTRO       = { max: 30, windowMinutes: 30 };
const RATE_LIMIT_VINCULO        = { max: 30, windowMinutes: 60 };

Mensagens 429 Contextuais

TipoMensagem
Burst por IP"Muitos acessos da sua rede. Aguarde alguns segundos e tente novamente."
Rate limit por IP"Muitos acessos da sua rede. Aguarde alguns minutos e tente novamente."
Rate limit por escola"Muitos acessos simultâneos. Aguarde um momento e tente novamente."
Lockout por identificador"Acesso temporariamente bloqueado. Tente novamente em X minuto(s)."

2.3 Lockout Progressivo (por identificador APENAS)

Falhas AcumuladasBloqueioJanela de Contagem
3 falhas2 minutos24 horas
6 falhas10 minutos24 horas
10 falhas60 minutos24 horas

IMPORTANTE: Lockout opera exclusivamente por identificador (matrícula ou CPF), NÃO por IP. Em ambiente NAT escolar, todos os alunos compartilham o mesmo IP. Usar IP no lockout causava bloqueio de escola inteira quando poucos alunos erravam dados.

2.3.1 Throttle de Alertas Push (ntfy)

Alertas de lockout severo (10+ falhas) são throttled via Map em memória:

  • Máximo 1 alerta por identificador (matrícula/CPF) a cada 5 minutos
  • Evita flood de notificações sob carga

2.4 Anti-Timing Attack

Delay aleatório de 500-1000ms em respostas de erro para impedir timing attacks:

typescript
const randomDelay = () => new Promise(resolve => 
  setTimeout(resolve, 500 + Math.random() * 500)
);

2.5 OTP e JWT do Mural

ParâmetroOTPJWT
Validade5 min2 horas (PORTAL_JWT_EXPIRY_HOURS)
Hash/AlgoSHA-256HS256
Tabelaportal_otps
Cookieolp_mural (HttpOnly, Secure, SameSite=None)
SecretOLP_JWT_SECRET (unificado)

2.6 checkRateLimit com keyField

typescript
checkRateLimit(supabase, keyValue, tipo, config, keyField)
// keyField: "ip" (default) | "escola_id" | "identificador"

Handlers com dual check (escola_id + IP): se qualquer um falhar, bloqueia.

2.7 Tabela de Tentativas

  • Colunas chave: ip, escola_id, identificador, tipo_tentativa, sucesso
  • Rate limit: Conta TODAS as rows (sucesso + falha) por tipo + chave
  • Lockout: Conta apenas sucesso=false por identificador
  • Cleanup: maintenance-cron remove registros > 7 dias

3. Observabilidade — Mural Rate Metrics

3.1 Tabela portal_rate_metrics

Agrega contagens de requests do mural por janela de 1 minuto, tipo de ação e escola:

ColunaTipoDescrição
janela_iniciotimestamptzTruncado ao minuto
tipo_acaotextlogin_aluno_a, lookup, send_otp_aluno, etc.
escola_iduuidFK para escolas (nullable para lookups)
contagemintTotal de requests na janela
contagem_bloqueadaintRequests que receberam 429
  • RLS: Ativo sem policies (acesso apenas via service_role)
  • Cleanup: maintenance-cron remove registros > 30 dias
  • Gravação: Fire-and-forget via RPC portal_increment_metric (não bloqueia resposta)
  • UNIQUE index: Funcional com COALESCE(escola_id, sentinel_uuid) para tratar NULL corretamente

3.2 RPC portal_increment_metric

sql
portal_increment_metric(p_tipo text, p_escola_id uuid, p_blocked boolean DEFAULT false)

Upsert atômico com ON CONFLICT usando COALESCE para tratar escola_id = NULL.

3.3 Cobertura de Métricas (v3)

ActionMétricaStatus
lookup_escolaExistente
login_aluno_aExistente
send_otp_alunoAdicionado v3
verify_otp_alunoAdicionado v3
send_otp_responsavelAdicionado v3
verify_otp_responsavelAdicionado v3
cadastro_responsavelAdicionado v3
auto_vincular_matriculaAdicionado v3

3.4 Dashboard Admin

Seção "Mural — Tráfego em Tempo Real" no dashboard administrativo:

  • Gráfico de barras: Requests/minuto por ação (período configurável: 1h, 2h, 6h, 24h)
  • Badges: Total de requests e % bloqueados
  • Tabela: Top 10 escolas por volume (última hora) com indicador de % do rate limit
  • Auto-refresh: A cada 60 segundos

3.5 Alertas Push de Pico

Quando uma escola atinge 70% do rate limit em qualquer janela:

⚠️ Portal Pico: Rate Limit Proximo
Escola {escola_id} atingiu 140/200 req/min (login_aluno_a)

Throttled: 1 alerta por escola a cada 15 minutos (via Map em memória).


4. Comparativo Sistema vs Mural

Rate-Limits

MétricaSistemaMural
Burst por IPN/A150/5s (in-memory)
Limite por usuário (OTP)3 OTPs/15minN/A (lockout por identificador)
Limite por IP (OTP)10/hora50/15min
Limite por escola (OTP)N/A100/15min
Limite por IP (lookup)N/A100/min
Limite por escola (lookup)N/A200/min
Limite por escola (login A)N/A200/min
Limite por IP (login A)N/A400/5min

Lockouts

MétricaSistemaMural
TipoFixoProgressivo
Tentativas → bloqueio53/6/10
Duração15 min2/10/60 min
Janela de contagem15 min24 horas

JWTs

MétricaSistemaMural
Expiração8 horas2 horas
Cookieolp_autholp_mural
SecretOLP_JWT_SECRETOLP_JWT_SECRET (unificado)

5. Validação Frontend como Camada de Defesa

ValidaçãoComponenteEfeito
Botão desabilitado com campos vaziosMuralLoginAluno (Método A)Impede request vazio
max no input de dataMuralLoginAluno (Método A)Impede data futura no HTML
Validação JS de data futurahandleLoginMetodoARejeita antes do request
Validação de CPF inlineMuralLoginAluno (Método B)Botão desabilitado com CPF inválido

Nota: Validação frontend é defesa em profundidade, não substitui validação backend.


6. Problemas Identificados e Recomendações

#SeveridadeProblemaStatusRecomendação
1✅ ResolvidoAnti-timing ausente no sistema principalImplementadoDelay 500-1000ms
2✅ ResolvidoPortal limitava apenas por IPImplementado (Fase A.1)Dual check IP + escola_id
3✅ ResolvidoConsole.logs vazando dados sensíveisImplementadoRemovidos
4✅ ResolvidoAlertas push ausentes no portalImplementado (Fase B)ntfy
5✅ ResolvidoValidação CPF ausente no backendImplementado (Fase B)isValidCPF()
6✅ ResolvidoLockout usava OR com IP (tóxico para NAT)CorrigidoLockout apenas por identificador
7✅ Resolvidontfy flood sem throttleCorrigidoCooldown 5min
8✅ ResolvidoRate limits altos permitiam DoS (81% CPU)Corrigido (Fase A.2)Reduzido + observabilidade
9✅ ResolvidoRate limits muito baixos para NAT realCorrigido (v3)1400/1800 + burst 150/5s
10✅ ResolvidoMétricas só em 2 de ~10 actionsCorrigido (v3)Métricas em todas as actions
11✅ Resolvidoportal_increment_metric NULL bugCorrigido (v3)COALESCE no unique index
12🟢 BaixaJanelas de tempo inconsistentes (15min vs 24h)AceitoTrade-off consciente
13🟢 InfoSistema usa lockout fixo vs progressivo no portalAceitoConsiderar migrar

7. Monitoramento

Eventos logados em logs_transacoes:

EventoAção de Log
Bloqueio por IPauth.bloqueio_ip
Escola suspensaauth.bloqueio_escola_status
Sem assinaturaauth.bloqueio_sem_assinatura
Perfil sem telaauth.bloqueio_perfil_sem_tela

Alertas Push (ntfy.sh)

EventoPrioridadeEndpoint
Brute force lockout (5+ falhas)highverify-otp
Falha no envio de OTP (WhatsApp/SMS)highsend-otp
Reuso de token revogadohighauth-helpers
Lockout severo mural (10+ falhas)highportal-escola
Rate limit OTP escola atingidohighportal-escola
Pico de tráfego (70% rate limit)highportal-escola

Observabilidade (Dashboard Admin)

MétricaFontePeríodo
Requests/minuto por açãoportal_rate_metricsConfigurável (1h–24h)
% requests bloqueados (429)portal_rate_metricsConfigurável
Top escolas por volumeportal_rate_metricsÚltima hora
% do rate limit por escolaCalculado (requests/200)Última hora

Referências

  • supabase/functions/send-otp/index.ts
  • supabase/functions/verify-otp/index.ts
  • supabase/functions/portal-escola/index.ts
  • supabase/functions/maintenance-cron/index.ts
  • supabase/functions/admin-dashboard/index.ts
  • supabase/functions/_shared/auth-helpers.ts
  • supabase/functions/_shared/supabase-client.ts — Singleton createSupabaseSystem()
  • supabase/functions/_shared/jwt-portal.ts
  • src/hooks/usePortalMetrics.ts
  • Testes de Rate-Limit — Mural Olímpico