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
- 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).
- Fail-fast com fail-close — Em caso de erro ou dúvida, BLOQUEAR a operação. Nunca permitir por padrão.
- Erros nunca expostos — Todo
catchdeve usargetUserFriendlyError()no frontend e mensagens genéricas no backend. Nenhum.messagecru de banco/RLS pode chegar ao usuário. - 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. - supabaseAdmin → supabase — Variáveis de cliente autenticado (com RLS) devem ser nomeadas
supabase, nuncasupabaseAdmin(reservado paracreateSupabaseSystem()).
Correções Já Implementadas
gestao-resultados/index.ts (2026-03-14)
| Gap | Correção | Status |
|---|---|---|
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ão | Verifica escola_olimpiadas.status = 'ativa' antes de processar | ✅ |
set_premiacoes_manual — sem validação de ownership das inscrições | Verifica que TODAS as inscricaoIds pertencem à escola_id do usuário | ✅ |
recompute — erro técnico exposto: "Erro no recálculo: " + err.message | Substituído por mensagem genérica | ✅ |
Parâmetro supabaseAdmin em recomputeFaseForOlimpiada | Renomeado 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.ts—getUserFriendlyErrorem 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-dashboard—requireRole('administrador') - [ ]
admin-escolas— Validação cross-escola, erros normalizados - [ ]
admin-usuarios— Validação de vínculo escola, erros normalizados - [ ]
admin-usuarios-escola—createSupabaseSystem()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-monitor—requireRole('administrador') - [ ]
admin-planos— CRUD de planos (admin only) - [ ]
admin-escola-dados— Validação de escola_id - [ ]
especialista-olimpiadas—requireRole('especialista'), CRUD completo - [ ]
especialista-cursos—requireRole('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: reqem todos osregistrarLog()
Frontend (hooks):
[ ]
useAdminDashboardMetrics.ts—getUserFriendlyError[ ]
useAdminEscolas.ts—getUserFriendlyErrorem todas as mutations[ ]
useAdminUsuarios.ts—getUserFriendlyError[ ]
useAdminAssinaturas.ts—getUserFriendlyError[ ]
useAdminFaturas.ts—getUserFriendlyError[ ]
useAdminLogs.ts—getUserFriendlyError[ ]
useAdminSmsLogs.ts—getUserFriendlyError[ ]
useAdminCronMonitor.ts—getUserFriendlyError[ ]
useAdminPlanos.ts—getUserFriendlyError[ ]
useAdminIncidentes.ts—getUserFriendlyError[ ]
useAdminUsuariosEscola.ts—getUserFriendlyError
Fase 3 — Escola (gestor, coordenador, diretor)
Edge Functions:
- [ ]
gestao-alunos— CRUD comescola_idem todas as mutations - [ ]
gestao-turmas— CRUD comescola_id - [ ]
gestao-usuarios-escola— Vínculo e permissões - [ ]
gestao-responsaveis— CRUD comescola_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 comescola_id - [ ]
eventos-calendario-trial— Versão trial - [ ]
tarefas-escola— CRUD de tarefas comescola_id - [ ]
comunicacao-escola— Mensagens comescola_id - [ ]
inscricoes-olimpiada— Inscrições comescola_id - [ ]
mural-escola— Publicações do mural comescola_id - [ ]
user-permissions— Permissões dinâmicas - [ ]
user-profile— Perfil do usuário
Checklist por função:
- [ ]
escola_idextraí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: reqem todos osregistrarLog()
Frontend (hooks):
- [ ]
useGestaoAlunos.ts— ✅getUserFriendlyErroraplicado (2026-03-14) - [ ]
useGestaoTurmas.ts—getUserFriendlyErrorem mutations - [ ]
useGestaoResultados.ts— ✅getUserFriendlyErroraplicado (2026-03-14) - [ ]
useGestaoResponsaveis.ts—getUserFriendlyError - [ ]
useImportacaoResultados.ts— ✅getUserFriendlyErroraplicado (2026-03-14) - [ ]
useImportacaoSessao.ts—getUserFriendlyError - [ ]
useEscolaDados.ts—getUserFriendlyError - [ ]
useEscolaDashboard.ts—getUserFriendlyError - [ ]
useEscolaPagamentos.ts—getUserFriendlyError - [ ]
useEscolaLimite.ts—getUserFriendlyError - [ ]
useOlimpiadasCoordenador.ts—getUserFriendlyError - [ ]
useOlimpiadasData.ts—getUserFriendlyError - [x]
useInscricoesOlimpiada.ts— ✅getUserFriendlyErroraplicado (2026-03-27) - [ ]
useVideosCoord.ts—getUserFriendlyError - [ ]
useDiretorDashboard.ts—getUserFriendlyError - [ ]
useEventosCalendario.ts—getUserFriendlyError - [ ]
useEventosCalendarioTrial.ts—getUserFriendlyError - [x]
useTarefasEscola.ts— ✅getUserFriendlyErroraplicado (2026-03-27) - [ ]
useComunicacaoEscola.ts—getUserFriendlyError - [x]
useMuralEscola.ts— ✅getUserFriendlyErroraplicado (2026-03-27, já usava em todos onError) - [ ]
useUsuariosEscola.ts—getUserFriendlyError - [ ]
useTransferenciaAlunos.ts—getUserFriendlyError - [ ]
useMyPermissions.ts—getUserFriendlyError - [ ]
usePortalConfig.ts—getUserFriendlyError - [ ]
useAnoLetivo.ts—getUserFriendlyError
Fase 4 — Mural Olímpico (Aluno e Responsável)
Edge Functions:
- [ ]
portal-escola— Rate limiting por escola e IP, cookieolp_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_muralcom 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_idealuno_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.ts—getUserFriendlyError - [ ]
usePortalMetrics.ts—getUserFriendlyError - [ ]
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 emcheck_status, vazamento dempResult.error/detailsremovido - [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:
| Gap | Correção | Severidade |
|---|---|---|
check_status sem ownership check — qualquer autenticado consultava qualquer fatura | Adicionado guard isAdmin || isGestorDaEscola antes de processar | 🔴 ALTO |
mpResult.error + mpResult.raw vazados no response de create_preference | Substituído por mensagem genérica "Erro ao criar link de pagamento. Tente novamente." | 🔴 ALTO |
| HMAC inválido não rejeitava webhook — processava normalmente | Adicionado 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 cron | DB 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 emcreate_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: reqem 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_SECRETconfigurado - [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_MESSAGESeERROR_PATTERNS— mapear novos erros encontrados durante as fases 1-5 - [ ] Verificar que NENHUM hook expõe
err.messagediretamente 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ódulo | Erro Técnico | Mensagem Amigável | Status |
|---|---|---|---|
| gestao-resultados | duplicate key value violates unique constraint "resultados_aluno_inscricao_id_fase_id_key" | "Este resultado já foi inserido para esta fase" | 🟡 Pendente |
| gestao-resultados | violates foreign key constraint "resultados_aluno_inscricao_id_fkey" | "Inscrição não encontrada" | 🟡 Pendente |
| gestao-alunos | duplicate key value violates unique constraint "alunos_escola_id_matricula_key" | "Já existe um aluno com esta matrícula" | 🟡 Pendente |
| gestao-turmas | violates foreign key constraint | "Não é possível excluir: existem alunos vinculados" | 🟡 Pendente |
| (adicionar durante auditoria) |
Cronograma Sugerido
| Fase | Estimativa | Dependências |
|---|---|---|
| Fase 1 — Autenticação | 1 sessão | Nenhuma |
| Fase 2 — Admin/Especialista | 2 sessões | Nenhuma |
| Fase 3 — Escola/Coordenador | 3 sessões | Nenhuma |
| Fase 4 — Portal | 1 sessão | Nenhuma |
| Fase 5 — Pagamentos | 1 sessão | Nenhuma |
| Fase 6 — Frontend Global | 2 sessões | Fases 1-5 (para mapear erros) |
Total estimado: 10 sessões de trabalho
Como Executar Cada Fase
- Ler a Edge Function completa
- Verificar contra o checklist da fase
- Documentar gaps encontrados como issues neste documento
- Corrigir gaps de segurança críticos imediatamente
- Mapear novos erros para
error-helpers.ts - Atualizar este documento com status ✅
- Testar via
supabase--test_edge_functionsquando 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
| # | Arquivo | Mecanismo | Dados injetados | Defesa | Status |
|---|---|---|---|---|---|
| 1 | src/lib/pdf-helpers.ts L139 | innerHTML | escola.nome, CNPJ, plano, método, gateway_id, numero_fatura | escapeHtml() em todos os campos | ✅ |
| 2 | src/lib/pdf-helpers.ts L258 | innerHTML | Relatório anual (faturas, status, numero_fatura) | escapeHtml() em todos os campos | ✅ |
| 3 | src/components/comunicacao/index.tsx L378 | document.write | Assunto + conteúdo de comunicado (texto livre do coordenador) | escapeHtml() | ✅ (ver nota) |
| 4 | src/components/ui/chart.tsx L83 | dangerouslySetInnerHTML | CSS hardcoded (THEMES) | Sem input externo | ✅ N/A |
| 5 | src/lib/render-markdown.tsx | ReactMarkdown | Conteúdo markdown | Seguro 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:
- Instalar
dompurify+@types/dompurify - Substituir
escapeHtml(editableContent)porDOMPurify.sanitize(editableContent)no ponto 3 - Manter
escapeHtml()nos pontos 1-2 (campos puros) - 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