Skip to content

Plano de Auditoria Global de Segurança — Plataforma OLP

Status: 🟡 Planejado
Criado em: 2026-03-14
Última atualização: 2026-03-14
Objetivo: Varredura exaustiva de segurança, tratamento de erros e validação de ownership em TODAS as Edge Functions e hooks do frontend.


Princípios Norteadores

  1. Nunca confiar no frontend — O backend DEVE revalidar tudo que o frontend validou, e validar o que o frontend NÃO pode validar (ownership, RLS, permissões de banco).
  2. Fail-fast com fail-close — Em caso de erro ou dúvida, BLOQUEAR a operação. Nunca permitir por padrão.
  3. Erros nunca expostos — Todo catch deve usar getUserFriendlyError() no frontend e mensagens genéricas no backend. Nenhum .message cru de banco/RLS pode chegar ao usuário.
  4. RLS silent failure — UPDATE/DELETE bloqueados por RLS retornam { count: 0 }, NÃO erro. O backend DEVE verificar se a mutação teve efeito real.
  5. supabaseAdmin → supabase — Variáveis de cliente autenticado (com RLS) devem ser nomeadas supabase, nunca supabaseAdmin (reservado para createSupabaseSystem()).

Correções Já Implementadas

gestao-resultados/index.ts (2026-03-14)

GapCorreçãoStatus
Variável supabaseAdmin enganosa (era createSupabaseClient, não service_role)Renomeada para supabase em todo o arquivo
delete — RLS silent failure (retorna sucesso com 0 rows)Ownership check via JOIN inscricoes_olimpiada.escola_id antes do delete
recompute — sem validação de adesãoVerifica escola_olimpiadas.status = 'ativa' antes de processar
set_premiacoes_manual — sem validação de ownership das inscriçõesVerifica que TODAS as inscricaoIds pertencem à escola_id do usuário
recompute — erro técnico exposto: "Erro no recálculo: " + err.messageSubstituído por mensagem genérica
Parâmetro supabaseAdmin em recomputeFaseForOlimpiadaRenomeado para supabase na assinatura e todas as chamadas

Fases da Auditoria

Fase 1 — Autenticação e Controle de Acesso (login → seleção de perfil)

Edge Functions:

  • [ ] send-otp — Rate limiting, bloqueio PAPEIS_SEM_TELA, validação de telefone
  • [ ] verify-otp — Expiração OTP, timing-safe comparison, bloqueio após N falhas
  • [ ] select-role — Validação de papel vs PAPEIS_SEM_TELA, re-assinatura JWT
  • [ ] me — Validação de cookie, refresh de dados do usuário
  • [ ] logout — Invalidação de cookie, log de sessão

Checklist por função:

  • [ ] Rate limiting implementado e testado
  • [ ] Erros retornam mensagens genéricas (não expõe se CPF existe)
  • [ ] Cookies HttpOnly com flags corretos (Secure, SameSite)
  • [ ] Logs de transação para login.success, login.failure, logout
  • [ ] Bloqueio de conta após tentativas excessivas

Frontend (hooks/components):

  • [ ] useRealtimeAuth.tsgetUserFriendlyError em todos os catch
  • [ ] login-unified.tsx — Validações client-side espelhando backend
  • [ ] role-switcher.tsx — Bloqueio visual de papéis sem tela/escola suspensa

Fase 2 — Admin e Especialista (papéis globais)

Edge Functions:

  • [ ] admin-dashboardrequireRole('administrador')
  • [ ] admin-escolas — Validação cross-escola, erros normalizados
  • [ ] admin-usuarios — Validação de vínculo escola, erros normalizados
  • [ ] admin-usuarios-escolacreateSupabaseSystem() justificado (RLS bypass necessário)
  • [ ] admin-assinaturas — Validação de plano/escola
  • [ ] admin-faturas — Proteção contra alteração de faturas de outras escolas
  • [ ] admin-logs — Somente leitura, sem exposição de dados sensíveis
  • [ ] admin-sms-logs — Somente leitura
  • [ ] admin-cron-monitorrequireRole('administrador')
  • [ ] admin-planos — CRUD de planos (admin only)
  • [ ] admin-escola-dados — Validação de escola_id
  • [ ] especialista-olimpiadasrequireRole('especialista'), CRUD completo
  • [ ] especialista-cursosrequireRole('especialista')
  • [ ] especialista-curso-videos — Upload + CRUD
  • [ ] especialista-templates — CRUD de templates
  • [ ] especialista-tutoriais — CRUD de tutoriais
  • [ ] especialista-headers — CRUD de headers de novidades
  • [ ] especialista-banners — CRUD de banners de login

Checklist por função:

  • [ ] requireRole() como primeira validação após auth
  • [ ] Nenhuma operação cross-escola sem validação explícita
  • [ ] Erros de banco mascarados (constraint violations, RLS blocks)
  • [ ] Logs de transação para todas as mutations
  • [ ] req: req em todos os registrarLog()

Frontend (hooks):

  • [ ] useAdminDashboardMetrics.tsgetUserFriendlyError

  • [ ] useAdminEscolas.tsgetUserFriendlyError em todas as mutations

  • [ ] useAdminUsuarios.tsgetUserFriendlyError

  • [ ] useAdminAssinaturas.tsgetUserFriendlyError

  • [ ] useAdminFaturas.tsgetUserFriendlyError

  • [ ] useAdminLogs.tsgetUserFriendlyError

  • [ ] useAdminSmsLogs.tsgetUserFriendlyError

  • [ ] useAdminCronMonitor.tsgetUserFriendlyError

  • [ ] useAdminPlanos.tsgetUserFriendlyError

  • [ ] useAdminIncidentes.tsgetUserFriendlyError

  • [ ] useAdminUsuariosEscola.tsgetUserFriendlyError


Fase 3 — Escola (gestor, coordenador, diretor)

Edge Functions:

  • [ ] gestao-alunos — CRUD com escola_id em todas as mutations
  • [ ] gestao-turmas — CRUD com escola_id
  • [ ] gestao-usuarios-escola — Vínculo e permissões
  • [ ] gestao-responsaveis — CRUD com escola_id
  • [ ] gestao-resultados — ✅ Auditado (2026-03-14)
  • [ ] escola-dados — Configurações da escola
  • [ ] escola-dashboard — Métricas da escola
  • [ ] escola-pagamentos — Faturas e pagamentos
  • [ ] coordenador-olimpiadas — Adesão a olimpíadas
  • [ ] coordenador-videos — Vídeos/cursos do coordenador
  • [ ] diretor-dashboard — Painéis gerenciais
  • [ ] eventos-calendario — CRUD de eventos com escola_id
  • [ ] eventos-calendario-trial — Versão trial
  • [ ] tarefas-escola — CRUD de tarefas com escola_id
  • [ ] comunicacao-escola — Mensagens com escola_id
  • [ ] inscricoes-olimpiada — Inscrições com escola_id
  • [ ] mural-escola — Publicações do mural com escola_id
  • [ ] user-permissions — Permissões dinâmicas
  • [ ] user-profile — Perfil do usuário

Checklist por função:

  • [ ] escola_id extraído do JWT (nunca do body do request)
  • [ ] Todas as mutations verificam ownership via escola_id
  • [ ] RLS silent failure tratado (verificar count após UPDATE/DELETE)
  • [ ] Erros de banco mascarados
  • [ ] Logs de transação para mutations
  • [ ] req: req em todos os registrarLog()

Frontend (hooks):

  • [ ] useGestaoAlunos.ts — ✅ getUserFriendlyError aplicado (2026-03-14)
  • [ ] useGestaoTurmas.tsgetUserFriendlyError em mutations
  • [ ] useGestaoResultados.ts — ✅ getUserFriendlyError aplicado (2026-03-14)
  • [ ] useGestaoResponsaveis.tsgetUserFriendlyError
  • [ ] useImportacaoResultados.ts — ✅ getUserFriendlyError aplicado (2026-03-14)
  • [ ] useImportacaoSessao.tsgetUserFriendlyError
  • [ ] useEscolaDados.tsgetUserFriendlyError
  • [ ] useEscolaDashboard.tsgetUserFriendlyError
  • [ ] useEscolaPagamentos.tsgetUserFriendlyError
  • [ ] useEscolaLimite.tsgetUserFriendlyError
  • [ ] useOlimpiadasCoordenador.tsgetUserFriendlyError
  • [ ] useOlimpiadasData.tsgetUserFriendlyError
  • [x] useInscricoesOlimpiada.ts — ✅ getUserFriendlyError aplicado (2026-03-27)
  • [ ] useVideosCoord.tsgetUserFriendlyError
  • [ ] useDiretorDashboard.tsgetUserFriendlyError
  • [ ] useEventosCalendario.tsgetUserFriendlyError
  • [ ] useEventosCalendarioTrial.tsgetUserFriendlyError
  • [x] useTarefasEscola.ts — ✅ getUserFriendlyError aplicado (2026-03-27)
  • [ ] useComunicacaoEscola.tsgetUserFriendlyError
  • [x] useMuralEscola.ts — ✅ getUserFriendlyError aplicado (2026-03-27, já usava em todos onError)
  • [ ] useUsuariosEscola.tsgetUserFriendlyError
  • [ ] useTransferenciaAlunos.tsgetUserFriendlyError
  • [ ] useMyPermissions.tsgetUserFriendlyError
  • [ ] usePortalConfig.tsgetUserFriendlyError
  • [ ] useAnoLetivo.tsgetUserFriendlyError

Fase 4 — Mural Olímpico (Aluno e Responsável)

Edge Functions:

  • [ ] portal-escola — Rate limiting por escola e IP, cookie olp_mural, bloqueio local
  • [ ] send-otp (contexto mural) — Rate limiting separado, template WhatsApp correto
  • [ ] verify-otp (contexto mural) — Validação de vínculo aluno-escola

Checklist por função:

  • [ ] Cookie olp_mural com flags HttpOnly, Secure, SameSite
  • [ ] Rate limiting por escola (configurable), por IP, e por identificador
  • [ ] Bloqueio progressivo (3, 5, 10 tentativas)
  • [ ] Dados do portal filtrados por escola_id e aluno_id/responsavel_id
  • [ ] Nenhum dado de outras escolas acessível
  • [ ] Erros genéricos (não expor se matrícula/CPF existe)

Frontend (hooks/components):

  • [ ] useMuralPortal.tsgetUserFriendlyError
  • [ ] usePortalMetrics.tsgetUserFriendlyError
  • [ ] usePortalInactivityTimeout.ts — Timeout configurável
  • [ ] MuralLoginAluno.tsx — Validações client-side
  • [ ] MuralLoginResponsavel.tsx — Validações client-side

Fase 5 — Pagamentos e Webhooks ✅ (2026-03-28)

Edge Functions:

  • [x] mercadopago-preference — Ownership check adicionado em check_status, vazamento de mpResult.error/details removido
  • [x] mercadopago-webhook — HMAC enforcement: rejeita com 401 quando secret configurado e assinatura inválida
  • [x] faturamento-cron — DB errors mascarados nos throws e catch global

Gaps encontrados e corrigidos:

GapCorreçãoSeveridade
check_status sem ownership check — qualquer autenticado consultava qualquer faturaAdicionado guard isAdmin || isGestorDaEscola antes de processar🔴 ALTO
mpResult.error + mpResult.raw vazados no response de create_preferenceSubstituído por mensagem genérica "Erro ao criar link de pagamento. Tente novamente."🔴 ALTO
HMAC inválido não rejeitava webhook — processava normalmenteAdicionado bloco de rejeição com 401 + log de segurança após validação HMAC🔴 ALTO
throw new Error(errorAss.message) expunha erro de DB no response do cronDB errors logados internamente; response usa mensagem segura🟡 MÉDIO
MP_WEBHOOK_SECRET ausente nos secrets do Supabase⚠️ PENDENTE — Adicionar secret para ativar HMAC enforcement🟡 MÉDIO

O que já estava bom:

  • Auth via extractAuthenticatedUser + ownership em create_preference
  • CRON_SECRET validado no faturamento-cron
  • Lock otimista (eq("status_pagamento", "pendente"))
  • Deduplicação + idempotência no webhook
  • Logs de transação com req: req em todas as operações CUD
  • Validação completa de valor (valor_match, moeda, external_reference)
  • Erros mascarados nos catch globais

Testes de integração Deno: 18 testes black-box criados e passando

  • mercadopago-preference/index.test.ts — 5 testes (CORS, auth guard, formato)
  • mercadopago-webhook/index.test.ts — 7 testes (CORS, HMAC, filtragem, formato)
  • faturamento-cron/index.test.ts — 6 testes (CORS, CRON_SECRET guard, formato)

Checklist por função:

  • [x] Webhook signature validation (HMAC) — Enforçado quando MP_WEBHOOK_SECRET configurado
  • [x] Idempotência (não processar mesmo evento 2x) — Lock otimista + deduplicação
  • [x] Valores validados contra plano contratado — calcularValorLiquido + valor_match
  • [x] Logs de transação para todas as operações financeiras
  • [x] Erros mascarados no response

Fase 6 — Frontend Global (Tratamento de Erros)

Objetivo: Garantir que getUserFriendlyError() é usado em TODOS os hooks com mutations.

Arquivo central: src/lib/error-helpers.ts

Tarefas:

  • [ ] Auditar ERROR_MESSAGES e ERROR_PATTERNS — mapear novos erros encontrados durante as fases 1-5
  • [ ] Verificar que NENHUM hook expõe err.message diretamente no toast
  • [ ] Padronizar: onError: (err) => olpToast.error('Título', { description: getUserFriendlyError(err) })
  • [ ] Mapear erros de constraint do Postgres que possam ocorrer em cada módulo
  • [ ] Testar cenários: rede indisponível, timeout, RLS block, constraint violation, 401/403

Mapeamento de Erros por Módulo

Ao auditar cada fase, registrar aqui os novos padrões de erro encontrados para adicionar ao error-helpers.ts.

MóduloErro TécnicoMensagem AmigávelStatus
gestao-resultadosduplicate key value violates unique constraint "resultados_aluno_inscricao_id_fase_id_key""Este resultado já foi inserido para esta fase"🟡 Pendente
gestao-resultadosviolates foreign key constraint "resultados_aluno_inscricao_id_fkey""Inscrição não encontrada"🟡 Pendente
gestao-alunosduplicate key value violates unique constraint "alunos_escola_id_matricula_key""Já existe um aluno com esta matrícula"🟡 Pendente
gestao-turmasviolates foreign key constraint"Não é possível excluir: existem alunos vinculados"🟡 Pendente
(adicionar durante auditoria)

Cronograma Sugerido

FaseEstimativaDependências
Fase 1 — Autenticação1 sessãoNenhuma
Fase 2 — Admin/Especialista2 sessõesNenhuma
Fase 3 — Escola/Coordenador3 sessõesNenhuma
Fase 4 — Portal1 sessãoNenhuma
Fase 5 — Pagamentos1 sessãoNenhuma
Fase 6 — Frontend Global2 sessõesFases 1-5 (para mapear erros)

Total estimado: 10 sessões de trabalho


Como Executar Cada Fase

  1. Ler a Edge Function completa
  2. Verificar contra o checklist da fase
  3. Documentar gaps encontrados como issues neste documento
  4. Corrigir gaps de segurança críticos imediatamente
  5. Mapear novos erros para error-helpers.ts
  6. Atualizar este documento com status ✅
  7. Testar via supabase--test_edge_functions quando possível

Auditoria XSS — Sanitização de HTML Dinâmico (2026-03-28)

Status: ✅ Concluído (escapeHtml) | 🟡 Futuro (DOMPurify)

Levantamento de pontos de injeção HTML no frontend

#ArquivoMecanismoDados injetadosDefesaStatus
1src/lib/pdf-helpers.ts L139innerHTMLescola.nome, CNPJ, plano, método, gateway_id, numero_faturaescapeHtml() em todos os campos
2src/lib/pdf-helpers.ts L258innerHTMLRelatório anual (faturas, status, numero_fatura)escapeHtml() em todos os campos
3src/components/comunicacao/index.tsx L378document.writeAssunto + conteúdo de comunicado (texto livre do coordenador)escapeHtml()✅ (ver nota)
4src/components/ui/chart.tsx L83dangerouslySetInnerHTMLCSS hardcoded (THEMES)Sem input externo✅ N/A
5src/lib/render-markdown.tsxReactMarkdownConteúdo markdownSeguro por design (sem innerHTML)✅ N/A

Decisão: escapeHtml() vs DOMPurify

escapeHtml() é a defesa correta para TODOS os casos atuais porque:

  • Pontos 1-2: campos são textos puros (nomes, IDs, valores) — nenhum HTML legítimo esperado
  • Ponto 3: comunicado é texto plano — escapeHtml() protege sem perda funcional
  • Pontos 4-5: sem input externo

DOMPurify seria necessário APENAS se:

  • O editor de comunicação evoluir para rich text (WYSIWYG/markdown renderizado para impressão)
  • Nesse caso, escapeHtml() destruiria a formatação (<b>, <ul>, <a>)
  • DOMPurify permitiria preservar HTML seguro e remover apenas vetores perigosos (<script>, onerror=, javascript:)

🟡 Ação Futura (PENDENTE)

Se/quando o editor de comunicação aceitar rich text:

  1. Instalar dompurify + @types/dompurify
  2. Substituir escapeHtml(editableContent) por DOMPurify.sanitize(editableContent) no ponto 3
  3. Manter escapeHtml() nos pontos 1-2 (campos puros)
  4. Configurar allowlist: DOMPurify.sanitize(html, { ALLOWED_TAGS: ['b','i','u','p','br','ul','ol','li','a','h1','h2','h3'], ALLOWED_ATTR: ['href','target'] })

Referências

  • AUTHENTICATION.md — Fluxo de login e JWT
  • RLS_POLICIES.md — Políticas de Row Level Security
  • RATE_LIMITS.md — Rate limiting e lockouts
  • AUDIT_LOG.md — Sistema de logs de transação
  • SECURITY_AUDIT_2026-02-28.md — Auditoria anterior
  • Knowledge: guidelines/backend-rls-validation — RLS silent failure
  • Knowledge: security/fail-close-architectural-principle — Fail-close
  • Knowledge: constraints/padrao-seguranca-jwt-rls — Padrão JWT/RLS