Skip to content

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

#AchadoSeveridadeStatus
1verify-otp: bloqueios declarado DEPOIS de bloqueios.push() (TDZ bug)ALTO✅ Corrigido
2send-otp: ip_origem usava x-forwarded-for em vez de clientIP unificadoMÉDIO✅ Corrigido
3me/index.ts: variável errorMessage renomeada para _reasonBAIXO✅ Corrigido

Achados aceitos (sem ação)

#AchadoJustificativa
4verify-otp OTP hash comparison via SQL .eq()Seguro — comparação no banco, não em código JS
5send-otp exibe últimos 4 dígitos do telefonePadrão de mercado (UX intencional)
6jwt.ts não valida audienceMitigado via guard portal_type em auth-helpers
7select-role sem rate limitingRisco baixo — requer autenticação prévia

Testes de integração

EFTestesStatus
send-otp6 testes (CORS, input, anti-enumeração, contrato)✅ 6/6
verify-otp7 testes (CORS, input, timing, leakage)✅ 7/7
select-role6 testes (CORS, auth guard, contrato, leakage)✅ 6/6
me9 testes (CORS, auth, portal, contrato)✅ 9/9
logout5 testes (CORS, idempotência, cookies, contrato)✅ 5/5
Total33 testes✅ 33/33

Fase 1.5 — Race Condition, IDOR, XSS (Cross-cutting) ✅ CONCLUÍDA

Achados corrigidos

#TipoAchadoSeveridadeStatus
RC-1Race ConditionBanner increment read-then-writeMÉDIO✅ RPC increment_banner_metric atômico
IDOR-3Timing AttackPortal OTP hash !== comparisonMÉDIOtimingSafeEqual implementado
XSS-1XSSpdf-helpers innerHTML com dados do bancoMÉDIOescapeHtml() sanitiza inputs
XSS-5XSSwindow.location.href sem validação de protocoloMÉDIO✅ Guard https:// ou / em 4 arquivos

Achados aceitos (sem ação)

#TipoAchadoJustificativa
RC-2Race ConditionPortal OTP tentativas_falhasRisco baixo — janela mínima + rate limiting mitiga
RC-3Race ConditionBanner ordem TOCTOURisco baixo — operação rara, admin-only
IDOR-1IDORadmin-faturas body.idAdmin-only — reavaliar se escopo expandir

Defesas verificadas (já sólidas)

  • IDOR escola: user.escola_id do JWT em todas as EFs ✅
  • XSS Markdown: ReactMarkdown sem innerHTML ✅
  • XSS Comunicação: escapeHtml() em document.write ✅
  • Race (vídeos): RPC increment_video_view atômico ✅
  • Cookie security: HttpOnly, SameSite, Secure ✅
  • Portal segregation: Guard portal_type centralizado ✅

Fase 2 — Gestão de Escolas ✅ CONCLUÍDA

EFs auditadas: admin-escolas, admin-escola-dados, tarefas-escola, escola-dados

Achados corrigidos

#TipoAchadoSeveridadeStatus
SEC-1Filter Injectionadmin-escolas searchTerm sem sanitização no .or() PostgRESTALTO✅ Regex sanitiza caracteres especiais
SEC-2Loggingadmin-escola-dados create/update_assinatura sem registrarLogMÉDIO✅ registrarLog fire-and-forget adicionado
SEC-3RLS Validationadmin-escola-dados update_assinatura sem validar resultadoMÉDIO.maybeSingle() + check !data → 403
SEC-5IDOR Defensetarefas-escola update sem .eq("escola_id")MÉDIO✅ Guard adicionado + .maybeSingle()
SEC-6IDOR Defensetarefas-escola delete sem .eq("escola_id")MÉDIO✅ Guard adicionado + .select("id").maybeSingle()
SEC-9Rollbackadmin-escolas rollback deletes sem validaçãoMÉDIO.select('id') valida resultado
SEC-10Bugadmin-escolas numero_fatura manual colide com triggerBAIXO✅ Removido — trigger gerar_numero_fatura gera
SEC-4Consistênciaadmin-escola-dados usa error em vez de messageBAIXO✅ Padronizado para message
SEC-8Refatoraçãotarefas-escola monolito 1211→~960 linhas✅ list_historico + batch_init_agenda extraídos para _shared/tarefas-helpers.ts

Achados aceitos (sem ação)

#TipoAchadoJustificativa
SEC-7RLStarefas-escola get_anotacao usa supabaseSystemescola_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

#TipoAchadoSeveridadeStatus
SEC-1Filter Injectionadmin-usuarios searchTerm sem sanitização no .or()ALTO✅ Regex sanitiza caracteres especiais
SEC-6Escalaçãogestao-usuarios-escola hard_delete remove papéis de TODAS as escolasALTO✅ Filtro .eq("escola_id") + delete condicional
SEC-2Info Leakadmin-usuarios-escola vaza deleteErr.message técnicoMÉDIO✅ Mensagem genérica
SEC-3RLS Silent Failadmin-usuarios-escola desativar sem validar resultadoMÉDIO.select().maybeSingle() + check !data → 403
SEC-7Race Conditiongestao-usuarios-escola delete+insert de papéis sem transaçãoMÉDIO✅ Substituído por deactivate+activate
SEC-8Input Validationuser-profile sem validação de comprimento/formatoBAIXO✅ Validação nome (2-200), email, biografia (500)
SEC-9Padrão Códigouser-permissions usa serve() legadoBAIXO✅ Migrado para Deno.serve()
SEC-10Padrão Códigoadmin-usuarios + admin-usuarios-escola usam serve() legadoBAIXO✅ Migrado para Deno.serve()
SEC-4RefatoraçãoCódigo duplicado massivo entre admin-usuarios e admin-usuarios-escola✅ Extraído para _shared/usuarios-helpers.ts
SEC-5Refatoraçãogestao-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

#TipoAchadoSeveridadeStatus
SEC-1Filter Injectionespecialista-olimpiadas searchTerm sem sanitização no .or()ALTO✅ Regex sanitiza caracteres especiais
SEC-2Refatoraçãoespecialista-olimpiadas monolito 2150→1181 linhas✅ Extraído para _shared/olimpiada-helpers.ts (726 linhas)
SEC-3Info Leakespecialista-olimpiadas vaza error.message técnico em 6 respostasBAIXO✅ Removido campo error de todas as respostas
SEC-4RLS Silent Failespecialista-olimpiadas archive e update_edicao_status sem validaçãoMÉDIO.select().maybeSingle() + check !data → 403
SEC-5RLS Silent Failcoordenador-olimpiadas remover_adesao delete sem validaçãoMÉDIO.select("id").maybeSingle() + check !deletado → 403
SEC-6Error Handlingcoordenador-olimpiadas catch não trata FeatureBlockedErrorBAIXOFeatureBlockedError check adicionado
SEC-7RLS Silent Failinscricoes-olimpiada update_status e cancel sem validaçãoMÉDIO.select("id").maybeSingle() + check → 403
SEC-8N+1 Performancecoordenador-olimpiadas niveisComSeries faz N queriesMÉDIO✅ Consolidado em .in("nivel_id", ids) + agrupamento em memória

Achados aceitos (sem ação)

#TipoAchadoJustificativa
SEC-9INFOgestao-resultados start_import_session usa supabaseSystemValidaçã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

#TipoAchadoSeveridadeStatus
SEC-1Timing Inconsistencyverify_otp_aluno comparava hash via .eq() SQL em vez de timingSafeEqualALTO✅ Alinhado com padrão timing-safe
SEC-2Bug (redeclaração)const encoder declarado 2x no mesmo escopo em portal-login-responsavel.ts e portal-cadastro.tsMÉDIO✅ Segunda declaração removida
SEC-5Menor privilégioget_series_escola usava createSupabaseSystem() para dados públicosBAIXO⚠️ REVERTIDO — turmas/series_escolares não têm policy pública; service_role necessário pré-login (documentado como exceção)
SEC-6RLS Silent FailhandleAutoDesvincular delete sem validação de resultadoMÉDIO.select("aluno_id").maybeSingle() + check → 403
SEC-7IDORhandleGetAlunoDashboard não verificava vínculo responsável-alunoMÉDIO✅ Verificação explícita de vínculo adicionada

Achados aceitos (sem ação)

#TipoAchadoJustificativa
SEC-3Timing (data_nascimento)Comparação direta !== de data_nascimentoMitigado: resposta genérica idêntica + anti-timing delay no path de CPF
SEC-4Enumeração CPFverificar_cpf_responsavel revela existência de CPFPor design (UX) — rate limit 30/30min mitiga enumeração em massa

Defesas verificadas (já sólidas)

  • CORS: handleCorsPrelight + getCorsHeaders em 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

ItemStatus
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

ConstraintMensagem amigável
resultados_aluno_inscricao_id_fase_id_keyEste resultado já foi inserido para esta fase.
resultados_aluno_inscricao_id_fkeyInscrição não encontrada.
alunos_escola_id_matricula_keyJá existe um aluno com esta matrícula nesta escola.

Resumo Final da Auditoria

FaseEscopoTestesStatus
1Auth (5 EFs)33
1.5Race Condition, IDOR, XSS
2Gestão de Escolas (4 EFs)~20
3Gestão de Usuários (5 EFs)~25
4Olimpíadas e Resultados (4 EFs)~20
5Pagamentos (3 EFs)
6Portal Público (1 orq + 5 helpers)23
7Frontend Global
Total~27 EFs + frontend~130+✅ COMPLETA