Auditoria de Segurança — Plano de Fases
Fase 1 — Auth (5 EFs) ✅ CONCLUÍDA
EFs auditadas: send-otp, verify-otp, select-role, me, logout
Achados corrigidos
| # | Achado | Severidade | Status |
|---|---|---|---|
| 1 | verify-otp: bloqueios declarado DEPOIS de bloqueios.push() (TDZ bug) | ALTO | ✅ Corrigido |
| 2 | send-otp: ip_origem usava x-forwarded-for em vez de clientIP unificado | MÉDIO | ✅ Corrigido |
| 3 | me/index.ts: variável errorMessage renomeada para _reason | BAIXO | ✅ Corrigido |
Achados aceitos (sem ação)
| # | Achado | Justificativa |
|---|---|---|
| 4 | verify-otp OTP hash comparison via SQL .eq() | Seguro — comparação no banco, não em código JS |
| 5 | send-otp exibe últimos 4 dígitos do telefone | Padrão de mercado (UX intencional) |
| 6 | jwt.ts não valida audience | Mitigado via guard portal_type em auth-helpers |
| 7 | select-role sem rate limiting | Risco baixo — requer autenticação prévia |
Testes de integração
| EF | Testes | Status |
|---|---|---|
send-otp | 6 testes (CORS, input, anti-enumeração, contrato) | ✅ 6/6 |
verify-otp | 7 testes (CORS, input, timing, leakage) | ✅ 7/7 |
select-role | 6 testes (CORS, auth guard, contrato, leakage) | ✅ 6/6 |
me | 9 testes (CORS, auth, portal, contrato) | ✅ 9/9 |
logout | 5 testes (CORS, idempotência, cookies, contrato) | ✅ 5/5 |
| Total | 33 testes | ✅ 33/33 |
Fase 1.5 — Race Condition, IDOR, XSS (Cross-cutting) ✅ CONCLUÍDA
Achados corrigidos
| # | Tipo | Achado | Severidade | Status |
|---|---|---|---|---|
| RC-1 | Race Condition | Banner increment read-then-write | MÉDIO | ✅ RPC increment_banner_metric atômico |
| IDOR-3 | Timing Attack | Portal OTP hash !== comparison | MÉDIO | ✅ timingSafeEqual implementado |
| XSS-1 | XSS | pdf-helpers innerHTML com dados do banco | MÉDIO | ✅ escapeHtml() sanitiza inputs |
| XSS-5 | XSS | window.location.href sem validação de protocolo | MÉDIO | ✅ Guard https:// ou / em 4 arquivos |
Achados aceitos (sem ação)
| # | Tipo | Achado | Justificativa |
|---|---|---|---|
| RC-2 | Race Condition | Portal OTP tentativas_falhas | Risco baixo — janela mínima + rate limiting mitiga |
| RC-3 | Race Condition | Banner ordem TOCTOU | Risco baixo — operação rara, admin-only |
| IDOR-1 | IDOR | admin-faturas body.id | Admin-only — reavaliar se escopo expandir |
Defesas verificadas (já sólidas)
- IDOR escola:
user.escola_iddo JWT em todas as EFs ✅ - XSS Markdown: ReactMarkdown sem innerHTML ✅
- XSS Comunicação: escapeHtml() em document.write ✅
- Race (vídeos): RPC
increment_video_viewatômico ✅ - Cookie security: HttpOnly, SameSite, Secure ✅
- Portal segregation: Guard
portal_typecentralizado ✅
Fase 2 — Gestão de Escolas ✅ CONCLUÍDA
EFs auditadas: admin-escolas, admin-escola-dados, tarefas-escola, escola-dados
Achados corrigidos
| # | Tipo | Achado | Severidade | Status |
|---|---|---|---|---|
| SEC-1 | Filter Injection | admin-escolas searchTerm sem sanitização no .or() PostgREST | ALTO | ✅ Regex sanitiza caracteres especiais |
| SEC-2 | Logging | admin-escola-dados create/update_assinatura sem registrarLog | MÉDIO | ✅ registrarLog fire-and-forget adicionado |
| SEC-3 | RLS Validation | admin-escola-dados update_assinatura sem validar resultado | MÉDIO | ✅ .maybeSingle() + check !data → 403 |
| SEC-5 | IDOR Defense | tarefas-escola update sem .eq("escola_id") | MÉDIO | ✅ Guard adicionado + .maybeSingle() |
| SEC-6 | IDOR Defense | tarefas-escola delete sem .eq("escola_id") | MÉDIO | ✅ Guard adicionado + .select("id").maybeSingle() |
| SEC-9 | Rollback | admin-escolas rollback deletes sem validação | MÉDIO | ✅ .select('id') valida resultado |
| SEC-10 | Bug | admin-escolas numero_fatura manual colide com trigger | BAIXO | ✅ Removido — trigger gerar_numero_fatura gera |
| SEC-4 | Consistência | admin-escola-dados usa error em vez de message | BAIXO | ✅ Padronizado para message |
| SEC-8 | Refatoração | tarefas-escola monolito 1211→~960 linhas | — | ✅ list_historico + batch_init_agenda extraídos para _shared/tarefas-helpers.ts |
Achados aceitos (sem ação)
| # | Tipo | Achado | Justificativa |
|---|---|---|---|
| SEC-7 | RLS | tarefas-escola get_anotacao usa supabaseSystem | escola_anotacoes e coordenador_cores não têm SELECT policies para papel escola — uso de service_role documentado como exceção em _shared/tarefas-helpers.ts |
Fase 3 — Gestão de Usuários ✅ CONCLUÍDA
EFs auditadas: admin-usuarios, admin-usuarios-escola, gestao-usuarios-escola, user-permissions, user-profile
Achados corrigidos
| # | Tipo | Achado | Severidade | Status |
|---|---|---|---|---|
| SEC-1 | Filter Injection | admin-usuarios searchTerm sem sanitização no .or() | ALTO | ✅ Regex sanitiza caracteres especiais |
| SEC-6 | Escalação | gestao-usuarios-escola hard_delete remove papéis de TODAS as escolas | ALTO | ✅ Filtro .eq("escola_id") + delete condicional |
| SEC-2 | Info Leak | admin-usuarios-escola vaza deleteErr.message técnico | MÉDIO | ✅ Mensagem genérica |
| SEC-3 | RLS Silent Fail | admin-usuarios-escola desativar sem validar resultado | MÉDIO | ✅ .select().maybeSingle() + check !data → 403 |
| SEC-7 | Race Condition | gestao-usuarios-escola delete+insert de papéis sem transação | MÉDIO | ✅ Substituído por deactivate+activate |
| SEC-8 | Input Validation | user-profile sem validação de comprimento/formato | BAIXO | ✅ Validação nome (2-200), email, biografia (500) |
| SEC-9 | Padrão Código | user-permissions usa serve() legado | BAIXO | ✅ Migrado para Deno.serve() |
| SEC-10 | Padrão Código | admin-usuarios + admin-usuarios-escola usam serve() legado | BAIXO | ✅ Migrado para Deno.serve() |
| SEC-4 | Refatoração | Código duplicado massivo entre admin-usuarios e admin-usuarios-escola | — | ✅ Extraído para _shared/usuarios-helpers.ts |
| SEC-5 | Refatoração | gestao-usuarios-escola monolito 1537→~1100 linhas | — | ✅ create, buscar_cpf, enviar_solicitacao extraídos para _shared/gestao-usuarios-helpers.ts |
Fase 4 — Olimpíadas e Resultados ✅ CONCLUÍDA
EFs auditadas: especialista-olimpiadas, coordenador-olimpiadas, inscricoes-olimpiada, gestao-resultados
Achados corrigidos
| # | Tipo | Achado | Severidade | Status |
|---|---|---|---|---|
| SEC-1 | Filter Injection | especialista-olimpiadas searchTerm sem sanitização no .or() | ALTO | ✅ Regex sanitiza caracteres especiais |
| SEC-2 | Refatoração | especialista-olimpiadas monolito 2150→1181 linhas | — | ✅ Extraído para _shared/olimpiada-helpers.ts (726 linhas) |
| SEC-3 | Info Leak | especialista-olimpiadas vaza error.message técnico em 6 respostas | BAIXO | ✅ Removido campo error de todas as respostas |
| SEC-4 | RLS Silent Fail | especialista-olimpiadas archive e update_edicao_status sem validação | MÉDIO | ✅ .select().maybeSingle() + check !data → 403 |
| SEC-5 | RLS Silent Fail | coordenador-olimpiadas remover_adesao delete sem validação | MÉDIO | ✅ .select("id").maybeSingle() + check !deletado → 403 |
| SEC-6 | Error Handling | coordenador-olimpiadas catch não trata FeatureBlockedError | BAIXO | ✅ FeatureBlockedError check adicionado |
| SEC-7 | RLS Silent Fail | inscricoes-olimpiada update_status e cancel sem validação | MÉDIO | ✅ .select("id").maybeSingle() + check → 403 |
| SEC-8 | N+1 Performance | coordenador-olimpiadas niveisComSeries faz N queries | MÉDIO | ✅ Consolidado em .in("nivel_id", ids) + agrupamento em memória |
Achados aceitos (sem ação)
| # | Tipo | Achado | Justificativa |
|---|---|---|---|
| SEC-9 | INFO | gestao-resultados start_import_session usa supabaseSystem | Validação de adesão feita em processarImportacaoBackground |
Documentação atualizada
AUDIT_CHECKLIST.md: Nova seção 11.5 — IDOR e Escalação de Privilégios com checklist dedicado
Fase 5 — Pagamentos
EFs: faturamento-cron, mercadopago-preference, mercadopago-webhookStatus: ✅ CONCLUÍDA (auditada previamente)
Fase 6 — Portal Público ✅ CONCLUÍDA
Arquivos auditados: portal-escola/index.ts, _shared/portal-login-aluno.ts, _shared/portal-login-responsavel.ts, _shared/portal-cadastro.ts, _shared/portal-dashboard.ts, _shared/portal-config.ts, _shared/portal-security.ts
Achados corrigidos
| # | Tipo | Achado | Severidade | Status |
|---|---|---|---|---|
| SEC-1 | Timing Inconsistency | verify_otp_aluno comparava hash via .eq() SQL em vez de timingSafeEqual | ALTO | ✅ Alinhado com padrão timing-safe |
| SEC-2 | Bug (redeclaração) | const encoder declarado 2x no mesmo escopo em portal-login-responsavel.ts e portal-cadastro.ts | MÉDIO | ✅ Segunda declaração removida |
| SEC-5 | Menor privilégio | get_series_escola usava createSupabaseSystem() para dados públicos | BAIXO | ⚠️ REVERTIDO — turmas/series_escolares não têm policy pública; service_role necessário pré-login (documentado como exceção) |
| SEC-6 | RLS Silent Fail | handleAutoDesvincular delete sem validação de resultado | MÉDIO | ✅ .select("aluno_id").maybeSingle() + check → 403 |
| SEC-7 | IDOR | handleGetAlunoDashboard não verificava vínculo responsável-aluno | MÉDIO | ✅ Verificação explícita de vínculo adicionada |
Achados aceitos (sem ação)
| # | Tipo | Achado | Justificativa |
|---|---|---|---|
| SEC-3 | Timing (data_nascimento) | Comparação direta !== de data_nascimento | Mitigado: resposta genérica idêntica + anti-timing delay no path de CPF |
| SEC-4 | Enumeração CPF | verificar_cpf_responsavel revela existência de CPF | Por design (UX) — rate limit 30/30min mitiga enumeração em massa |
Defesas verificadas (já sólidas)
- CORS:
handleCorsPrelight+getCorsHeadersem todas respostas ✅ - Rate limiting em todas as ações públicas (lookup, login, OTP, cadastro, vínculo) ✅
- Lockout progressivo (3/6/10 falhas) com fail-close em erro de DB ✅
- Anti-timing delay em paths de CPF não encontrado ✅
- CPF validado com
isValidCPF()antes de qualquer query ✅ - Mensagens genéricas para login falho ✅
- Cookie HttpOnly, Secure, SameSite=None ✅
- Token portal verificado com blacklist jti ✅
- Segregação portal/sistema via
extractAuthenticatedUser✅ - Logging fire-and-forget em paths de sucesso ✅
- Parentesco validado contra whitelist ✅
- Input validation (telefone, email, nome) no update_perfil ✅
Fase 7 — Frontend Global (Error Sanitization) ✅ CONCLUÍDA
Verificações
| Item | Status |
|---|---|
getUserFriendlyError usado em 57+ arquivos | ✅ |
Zero error.message exposto em olpToast.error | ✅ |
invokeAction retorna { success, message } sem vazamento | ✅ |
| 401/403 semânticos corretos (401=não autenticado, 403=IDOR/permissão) | ✅ |
4 constraint patterns adicionais mapeados em error-helpers.ts | ✅ |
Error patterns adicionados
| Constraint | Mensagem amigável |
|---|---|
resultados_aluno_inscricao_id_fase_id_key | Este resultado já foi inserido para esta fase. |
resultados_aluno_inscricao_id_fkey | Inscrição não encontrada. |
alunos_escola_id_matricula_key | Já existe um aluno com esta matrícula nesta escola. |
Resumo Final da Auditoria
| Fase | Escopo | Testes | Status |
|---|---|---|---|
| 1 | Auth (5 EFs) | 33 | ✅ |
| 1.5 | Race Condition, IDOR, XSS | — | ✅ |
| 2 | Gestão de Escolas (4 EFs) | ~20 | ✅ |
| 3 | Gestão de Usuários (5 EFs) | ~25 | ✅ |
| 4 | Olimpíadas e Resultados (4 EFs) | ~20 | ✅ |
| 5 | Pagamentos (3 EFs) | — | ✅ |
| 6 | Portal Público (1 orq + 5 helpers) | 23 | ✅ |
| 7 | Frontend Global | — | ✅ |
| Total | ~27 EFs + frontend | ~130+ | ✅ COMPLETA |