Skip to content

Relatório Técnico de Segurança — Plataforma OLP

Versão: 1.0.0
Data: 2026-02-28 14:30 BRT
Auditor: Lovable AI (análise automatizada de código-fonte)
Escopo: Todas as camadas — Frontend, Worker Cloudflare, API (Edge Functions), Supabase, Autenticação, Portal Público, Admin, Banco de Dados, Infraestrutura, Dependências
Classificação: Documento interno — uso restrito à equipe de engenharia e segurança


Sumário Executivo

A plataforma OLP apresenta um nível de maturidade de segurança acima da média para aplicações de seu porte, com destaque para:

  • ✅ Autenticação JWT customizada com cookies HttpOnly (sem fallback para Authorization header)
  • ✅ Separação completa de contextos admin/portal via claims JWT distintas (secret unificado OLP_JWT_SECRET por exigência do PostgREST/RLS)
  • ✅ RLS ativo em 100% das tabelas (57 tabelas à data da auditoria — verificar contagem atual)
  • ✅ Rate limiting por IP e por usuário em endpoints críticos
  • ✅ Lockout progressivo no portal público
  • ✅ Logs estruturados com geolocalização via Cloudflare
  • ✅ Monitoramento CRON com escalação automática (push ntfy + alertas in-app)

Gaps críticos identificados:

  • ✅ Headers de segurança HTTP completos (meta tags no frontend + 6 headers HTTP no Cloudflare Worker, incluindo HSTS)
  • ✅ Revogação server-side de tokens JWT (jti + token_blacklist) — Implementado em 2026-03-01
  • ✅ Alertas push via ntfy.sh para anomalias críticas — Implementado em 2026-03-01
  • ✅ Auditoria de dependências: Dependabot configurado, xlsx (CVE-2023-30533) migrado para ExcelJS — 2026-03-01

Índice

  1. Autenticação JWT Customizada
  2. Autorização e RBAC
  3. Isolamento Multi-Tenant
  4. IDOR e Enumeração de UUID
  5. Rate Limiting
  6. Lockout Progressivo
  7. Proteção contra Brute Force
  8. CORS
  9. Headers de Segurança HTTP
  10. Proteção contra Replay de Token
  11. Logs Estruturados
  12. Monitoramento e Alertas
  13. Segurança de Dependências (Supply Chain)
  14. Proteção contra Scraping e DoS
  15. Segurança de Secrets e Variáveis de Ambiente

1. Autenticação JWT Customizada

Status: ✅ Implementado

O que está implementado

A plataforma utiliza autenticação JWT customizada (não usa Supabase Auth nativo) com separação completa entre sistema administrativo e portal público.

Dois contextos de JWT:

AspectoSistema AdminPortal Aluno/Responsável
SecretOLP_JWT_SECRETOLP_JWT_SECRET (unificado para RLS)
Cookieolp_autholp_mural
Expiração8 horas2 horas
Claims diferenciantesprincipal_role, escola_idportal_type, escopo: portal_readonly
AlgoritmoHS256 (jose)HS256 (jose)
Flags do CookieHttpOnly; Secure; SameSite=None; Path=/HttpOnly; Secure; SameSite=None; Path=/

Medidas implementadas:

  1. Cookie-only authentication — Fallback para Authorization header foi removido explicitamente em auth-helpers.ts:46-48 e supabase-client.ts:40-42. Comentários no código documentam a decisão:

    typescript
    // SEGURANÇA: Fallback Authorization header REMOVIDO
    // Autenticação exclusivamente via cookie HttpOnly para prevenir XSS
  2. Cross-context blocking — Tokens de portal não são aceitos pelo sistema admin. O helper verifyPortalToken() valida presença de portal_type e rejeita tokens com principal_role (jwt-portal.ts:98-101):

    typescript
    if ((payload as any).principal_role && !typedPayload.portal_type) {
      throw new Error('Token de sistema não pode acessar portal');
    }
  3. Logout multi-domínio — Remove cookies em ambos os domínios (com e sem Domain=.olp.digital) usando headers.append() (não join com vírgula). Evidência: logout/index.ts:70-73.

  4. OTP com hash SHA-256 — OTP nunca é armazenado em texto plano. Hash gerado via crypto.subtle.digest('SHA-256', ...) em send-otp/index.ts:387-391.

  5. OTP com expiração curta — 5 minutos (send-otp/index.ts:394).

  6. Mensagens genéricas — Não revela se usuário existe ou está inativo (send-otp/index.ts:140-148, verify-otp/index.ts:96):

    typescript
    const genericAuthError = 'Não foi possível processar sua solicitação. Verifique os dados ou contate o suporte.';

O que NÃO está implementado

  1. Rotação automática de secretsOLP_JWT_SECRET não possui política de rotação. Uma troca exige invalidação de todas as sessões ativas.

Riscos

RiscoCenário de AtaqueImpacto
Token válido após logoutUsuário faz logout mas atacante capturou cookie via rede (não XSS, pois é HttpOnly). Token permanece válido por até 8h.Médio-Alto
Timing side-channelAtacante mede tempo de resposta de send-otp para inferir se CPF/INEP existe no sistema. Delay entre lookup primário (1 query) vs secundário (2 queries) é mensurável.Baixo-Médio
Secret comprometidoSe OLP_JWT_SECRET vazar, TODOS os tokens (admin + portal) podem ser forjados.Crítico

Criticidade: Médio-Alto

A ausência de revogação server-side é o gap mais significativo. Mitigação parcial: expiração relativamente curta (8h/2h).

Como testar manualmente (staging)

Teste 1: Token válido após logout

bash
# 1. Fazer login e capturar cookie
curl -c cookies.txt -X POST https://[SUPABASE_URL]/functions/v1/verify-otp \
  -H "Content-Type: application/json" \
  -H "Origin: https://olp.digital" \
  -d '{"codigo":"CPF_VALIDO","otp":"CODIGO_OTP"}'

# 2. Verificar sessão ativa
curl -b cookies.txt https://[SUPABASE_URL]/functions/v1/me \
  -H "Origin: https://olp.digital"
# Esperado: success: true

# 3. Fazer logout
curl -b cookies.txt -X POST https://[SUPABASE_URL]/functions/v1/logout \
  -H "Origin: https://olp.digital"

# 4. Reusar o cookie antigo manualmente (simular token não revogado)
curl -b cookies.txt https://[SUPABASE_URL]/functions/v1/me \
  -H "Origin: https://olp.digital"
# ATUAL: success: true (token ainda válido — GAP!)
# IDEAL: success: false (token na blacklist)

Teste 2: Cross-context blocking

bash
# Capturar cookie olp_mural (login de aluno)
# Tentar usar em endpoint admin (ex: /me para sistema)
curl -b "olp_mural=TOKEN_MURAL" https://[SUPABASE_URL]/functions/v1/me \
  -H "Origin: https://olp.digital"
# Esperado: success: false (me.ts procura olp_auth, não olp_mural)

Teste 3: Timing side-channel

bash
# Medir tempo de resposta para CPF existente vs inexistente
for i in {1..50}; do
  time curl -s -X POST https://[SUPABASE_URL]/functions/v1/send-otp \
    -H "Content-Type: application/json" \
    -d '{"codigo":"12345678901"}' # CPF existente
done

for i in {1..50}; do
  time curl -s -X POST https://[SUPABASE_URL]/functions/v1/send-otp \
    -H "Content-Type: application/json" \
    -d '{"codigo":"99999999999"}' # CPF inexistente
done
# Comparar distribuição de tempos

Como testar via Playwright

typescript
import { test, expect } from '@playwright/test';

test('token de sistema não funciona em portal', async ({ page }) => {
  // Login no sistema admin
  await page.goto('/');
  await page.fill('[data-testid="codigo-input"]', 'CPF_ADMIN');
  await page.click('[data-testid="btn-enviar-codigo"]');
  // ... completar fluxo OTP
  
  // Capturar cookie olp_auth
  const cookies = await page.context().cookies();
  const olpAuth = cookies.find(c => c.name === 'olp_auth');
  expect(olpAuth).toBeDefined();
  
  // Tentar acessar portal com cookie de sistema
  await page.goto('/escola/slug-teste');
  // Portal não deve reconhecer sessão de sistema
  expect(page.locator('[data-testid="portal-login-form"]')).toBeVisible();
});

test('logout remove cookie', async ({ page }) => {
  // Login
  // ... fluxo de login
  
  // Logout
  await page.click('[data-testid="btn-logout"]');
  
  // Verificar cookie removido
  const cookies = await page.context().cookies();
  const olpAuth = cookies.find(c => c.name === 'olp_auth');
  expect(olpAuth).toBeUndefined();
});

Como validar via logs

sql
-- Verificar logins bem-sucedidos
SELECT 
  criado_em, nome_usuario, papel_principal, ip, cidade, estado,
  detalhes->>'tipo_codigo' as tipo_codigo,
  detalhes->>'principal_role' as role_jwt
FROM logs_transacoes
WHERE acao = 'login.success'
ORDER BY criado_em DESC
LIMIT 20;

-- Verificar logouts
SELECT criado_em, nome_usuario, ip, detalhes
FROM logs_transacoes
WHERE acao = 'login.logout'
ORDER BY criado_em DESC
LIMIT 20;

-- Verificar bloqueios de escola/assinatura no login
SELECT criado_em, nome_usuario, acao, detalhes
FROM logs_transacoes
WHERE acao LIKE 'auth.bloqueio%'
ORDER BY criado_em DESC
LIMIT 20;

Monitoramento e alertas

  • ✅ Logins bem-sucedidos logados em logs_transacoes com ação login.success
  • ✅ Logouts logados com ação login.logout
  • ✅ Bloqueios por status de escola logados (auth.bloqueio_escola_status)
  • ✅ Bloqueios por perfil sem tela logados (auth.bloqueio_perfil_sem_tela)
  • Não há alerta automático para login de IP suspeito ou geolocalização atípica
  • Não há dashboard de sessões ativas ou histórico de logins por usuário

Reação automática

CenárioReaçãoEvidência
Escola suspensa/encerradaBloqueia envio de OTP + loginsend-otp/index.ts:251-287
Trial expiradoBloqueia envio de OTPsend-otp/index.ts:291-324
Perfil sem tela implementadaBloqueia envio de OTPsend-otp/index.ts:197-226
Usuário inativoMensagem genérica (não revela inatividade)send-otp/index.ts:151-159

Evidência técnica

ArquivoFunção/TrechoDescrição
supabase/functions/_shared/jwt.ts:33-56signAuthToken()Assina JWT admin com HS256, 8h, role: authenticated
supabase/functions/_shared/jwt-portal.ts:45-72signPortalToken()Assina JWT portal com 2h (PORTAL_JWT_EXPIRY_HOURS), portal_type, escopo: portal_readonly
supabase/functions/_shared/auth-helpers.ts:26-51extractToken()Extrai token APENAS do cookie olp_auth (sem fallback header)
supabase/functions/_shared/auth-helpers.ts:57-85extractAuthenticatedUser()Valida e decodifica JWT do sistema
supabase/functions/_shared/jwt-portal.ts:79-110verifyPortalToken()Valida JWT do portal + bloqueia cross-context
supabase/functions/verify-otp/index.ts:491-501Cookie setupHttpOnly; Secure; SameSite=None; Path=/; Max-Age=28800 (8h)
supabase/functions/logout/index.ts:54-74LogoutRemove cookie em ambos domínios (com/sem Domain)
supabase/functions/_shared/supabase-client.ts:32-60createSupabaseClient()Extrai token do cookie olp_auth para RLS
supabase/functions/_shared/supabase-client.ts:137-164createSupabasePortalAuth()Extrai token do cookie olp_mural para RLS

2. Autorização e RBAC

Status: ✅ Implementado

O que está implementado

Sistema RBAC (Role-Based Access Control) com 7+ papéis e verificação em todas as Edge Functions autenticadas.

Papéis do sistema:

administrador > especialista > escola > escola_trial > coordenador > diretor > pedagogico > professor > marketing

Mecanismo de verificação:

  1. requireRole(req, roles, recurso) — Middleware genérico que:

    • Extrai usuário do cookie JWT
    • Verifica se possui pelo menos um dos papéis requeridos
    • Em caso de falha: loga incidente de segurança via logAccessDenied() e retorna 403
  2. requireAdmin(req) — Atalho para requireRole(req, 'administrador', 'admin-area')

  3. logAccessDenied() — Registra automaticamente em logs_transacoes:

    json
    {
      "acao": "permissao.acesso_negado",
      "detalhes": {
        "tipo": "security",
        "recurso": "admin-escolas",
        "papel_usuario": "coordenador",
        "papel_requerido": "administrador",
        "todos_papeis": ["coordenador"],
        "url": "...",
        "method": "POST"
      }
    }
  4. Permissões dinâmicas por papelpermissions.ts define áreas acessíveis por papel (ex: coordenador pode ver calendario, olimpiadas_coord, mas não alunos ou configuracoes). Permissões obrigatórias (ex: painel_controle para coordenador) nunca podem ser removidas.

  5. Normalização lowercase — Todos os papéis são normalizados para lowercase em extractAuthenticatedUser() e verify-otp, prevenindo bypass por case-sensitivity.

O que NÃO está implementado

  1. Auditoria de mudanças de papel — Não há log específico quando um gestor altera permissões de um coordenador/diretor (embora operações de UPDATE genéricas são logadas).

  2. Segregação temporal — Não há conceito de papéis com expiração automática (ex: "coordenador temporário por 30 dias").

Riscos

RiscoCenárioImpacto
Escalação horizontalGestor de escola A cria coordenador com acesso a dados de escola BMitigado por RLS (escola_id no JWT)
Papel legadoUsuário com papel professor no banco mas sem tela implementadaMitigado por bloqueio em send-otp (PAPEIS_COM_TELA)

Criticidade: Baixo

O RBAC está bem implementado. RLS fornece defesa em profundidade.

Como testar manualmente (staging)

bash
# Tentar acessar endpoint admin com cookie de coordenador
curl -b "olp_auth=TOKEN_COORDENADOR" \
  -X POST https://[SUPABASE_URL]/functions/v1/admin-escolas \
  -H "Content-Type: application/json" \
  -H "Origin: https://olp.digital" \
  -d '{"action":"list"}'
# Esperado: 403 "Acesso negado"
# Log criado: permissao.acesso_negado

Como testar via Playwright

typescript
test('coordenador não acessa menu admin', async ({ page }) => {
  // Login como coordenador
  // ... fluxo de login
  
  // Verificar que não há link para admin
  await expect(page.locator('[data-testid="admin-menu"]')).not.toBeVisible();
  
  // Tentar navegar diretamente
  await page.goto('/admin');
  // Deve redirecionar para dashboard do coordenador
  await expect(page).toHaveURL(/coordenacao/);
});

Como validar via logs

sql
-- Incidentes de acesso negado
SELECT 
  criado_em, nome_usuario, ip, cidade,
  detalhes->>'recurso' as recurso,
  detalhes->>'papel_usuario' as papel_usuario,
  detalhes->>'papel_requerido' as papel_requerido
FROM logs_transacoes
WHERE acao = 'permissao.acesso_negado'
ORDER BY criado_em DESC
LIMIT 50;

Monitoramento e alertas

  • ✅ Cada acesso negado é logado automaticamente via logAccessDenied()
  • ✅ Admin pode visualizar logs de segurança no dashboard
  • ❌ Não há alerta automático para múltiplos acessos negados do mesmo usuário/IP

Reação automática

  • ✅ Retorna HTTP 403 imediatamente
  • ✅ Loga incidente com IP, URL, método, papéis do usuário
  • ❌ Não há bloqueio progressivo para tentativas repetidas de escalação

Evidência técnica

ArquivoLinha(s)Descrição
_shared/auth-helpers.ts:160-177requireRole()Middleware genérico com logging automático
_shared/auth-helpers.ts:101-135logAccessDenied()Registro de incidentes de segurança
_shared/auth-helpers.ts:90-93hasRole()Verificação case-insensitive de papel
_shared/permissions.ts:49-56PERMISSIONS_BY_ROLEMapeamento papel → permissões
_shared/permissions.ts:111-114MANDATORY_PERMISSIONS_BY_ROLEPermissões irremovíveis por papel

3. Isolamento Multi-Tenant

Status: ✅ Implementado

O que está implementado

  1. RLS (Row Level Security) ativo em todas as 57 tabelas do banco (contagem da data da auditoria, 2026-02-28 — tabelas novas como portal_rate_metrics, portal_alert_cooldown, senha_historico, canary_groups, feature_flags foram adicionadas depois). Verificação via supabase--linter confirma zero tabelas sem RLS habilitado.

  2. escola_id no JWT — Claim escola_id é incluída no token JWT tanto do sistema (jwt.ts:16) quanto do portal (jwt-portal.ts:28). Policies RLS verificam:

    sql
    auth.jwt()->>'escola_id' = escola_id::text
  3. SECURITY DEFINER functions — Para evitar recursão infinita de RLS, funções como aluno_pertence_escola() e responsavel_vinculado_aluno() usam SECURITY DEFINER com search_path = 'public'.

  4. Tabelas sem policies (por design, contagem da data da auditoria — re-auditar periodicamente):

    • cadastro_tokens — acesso exclusivo via service_role (criação de escolas)
    • sms_log — acesso exclusivo via service_role (legado Twilio, descontinuado)
    • twilio_billing_log — acesso exclusivo via service_role (legado Twilio, descontinuado)
    • Tabelas adicionadas após auditoria (token_blacklist, portal_rate_metrics, portal_alert_cooldown, senha_historico) também sem policies (acesso via service_role)
  5. Multi-escola — Usuários podem ter papéis em múltiplas escolas. O JWT carrega roles[] com escola_id por papel. Bloqueios são verificados por escola individualmente em verify-otp/index.ts:380-469.

O que NÃO está implementado

  1. Validação explícita de ownership em Edge Functions — Algumas Edge Functions confiam exclusivamente no RLS para isolamento, sem validação adicional no código da função. Se uma policy for mal-configurada, dados podem vazar.

  2. Teste automatizado de isolamento — Não há teste E2E que verifica se escola A não consegue ver dados de escola B. ✅ Implementado em tests/security/idor-cross-escola.test.ts (pós-auditoria).

Riscos

RiscoCenárioImpacto
Policy RLS defeituosaUma nova tabela é criada sem escola_id check no RLSAlto
Service role leakEdge Function usa createSupabaseSystem() indevidamente para operações de usuárioAlto

Criticidade: Médio

O RLS é robusto mas depende de configuração correta em cada nova tabela.

Como testar manualmente (staging)

bash
# Login como gestor da escola A
# Tentar acessar dados da escola B via API direta
curl -b "olp_auth=TOKEN_ESCOLA_A" \
  -X POST https://[SUPABASE_URL]/functions/v1/gestao-alunos \
  -H "Content-Type: application/json" \
  -H "Origin: https://olp.digital" \
  -d '{"action":"list","params":{"escola_id":"UUID_ESCOLA_B"}}'
# Esperado: array vazio ou erro (RLS filtra por escola_id do JWT)

Como validar via logs

sql
-- Verificar se há acessos cross-escola
SELECT 
  criado_em, nome_usuario, acao, 
  detalhes->>'escola_id' as escola_target,
  ip
FROM logs_transacoes
WHERE acao = 'permissao.acesso_negado'
  AND detalhes->>'tipo' = 'security'
ORDER BY criado_em DESC;

Monitoramento e alertas

  • ✅ RLS aplicado automaticamente pelo Postgres
  • ❌ Não há monitoramento específico de tentativas cross-tenant
  • ❌ Não há teste automatizado de isolamento em CI/CD

Reação automática

  • ✅ RLS bloqueia silenciosamente (retorna dataset vazio)
  • requireRole() verifica papel antes de processar ação

Evidência técnica

ArquivoDescrição
_shared/supabase-client.ts:32-60createSupabaseClient(req) — extrai JWT do cookie para RLS
_shared/supabase-client.ts:104-118createSupabaseSystem() — documentação explícita de uso restrito
DB: aluno_pertence_escola()SECURITY DEFINER function para RLS sem recursão
DB: responsavel_vinculado_aluno()SECURITY DEFINER function para RLS do portal

4. IDOR e Enumeração de UUID

Status: ⚠️ Parcial

O que está implementado

  1. UUIDs v4 — Todas as PKs usam gen_random_uuid(), que gera UUIDs criptograficamente aleatórios (122 bits de entropia). Não são sequenciais ou previsíveis.

  2. RLS como defesa primária — Mesmo que um atacante conheça um UUID válido de outra escola, o RLS bloqueia acesso.

  3. Mensagens genéricas — Endpoints de login não revelam se um CPF/INEP/CNPJ existe no sistema.

O que NÃO está implementado

  1. Validação explícita de ownership — Nem todas as Edge Functions validam no código se o id passado pertence à escola do usuário. Dependem exclusivamente do RLS.

  2. Rate limiting em endpoints de consulta — Endpoints como gestao-alunos com action list não têm rate limiting específico, permitindo enumeração massiva dentro do escopo da própria escola.

Riscos

RiscoCenárioImpacto
Enumeração intra-tenantCoordenador malicioso enumera todos os alunos da própria escolaBaixo (acesso legítimo)
IDOR se RLS falharPolicy mal configurada em nova tabela permite acesso cross-escola por UUIDAlto

Criticidade: Médio

UUIDs v4 + RLS fornecem defesa robusta. Risco real depende de qualidade das policies RLS.

Como testar manualmente (staging)

bash
# Tentar acessar aluno de outra escola por UUID
curl -b "olp_auth=TOKEN_ESCOLA_A" \
  -X POST https://[SUPABASE_URL]/functions/v1/gestao-alunos \
  -H "Content-Type: application/json" \
  -d '{"action":"get","params":{"id":"UUID_ALUNO_ESCOLA_B"}}'
# Esperado: aluno não encontrado (RLS filtra)

Monitoramento e alertas

  • ❌ Não há detecção de tentativas de IDOR
  • ❌ Não há rate limiting em consultas autenticadas

Evidência técnica

  • Todas as tabelas usam id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY
  • RLS policies verificam escola_id via auth.jwt()->>'escola_id'

5. Rate Limiting

Status: ✅ Implementado (atualizado Fase A.1 — 2026-03-07)

O que está implementado

Sistema Admin (send-otp):

TipoLimiteJanelaEvidência
Por usuário3 OTPs15 minutossend-otp/index.ts:18-19, 334-354
Por IP10 OTPs1 horasend-otp/index.ts:19, 357-377

Portal Público (portal-escola) — Fase A.1 (NAT de alta densidade):

Estratégia dual: escola_id como chave primária (controle de burst), IP como secundário (anti-bot).

TipoLimiteJanelaChave
Lookup por IP5001 minIP
Lookup por escola20001 minescola_id
Séries por IP5001 minIP
Login A por escola6001 minescola_id
Login A por IP8001 minIP
OTP por escola10015 minescola_id
OTP por IP5015 minIP
Check slug1001 minIP
Cadastro3030 minIP
Vínculo3060 minIP

Alertas push (ntfy.sh):

  • ✅ Rate limit OTP por escola atingido → alerta high com tags warning, money_with_wings
  • ✅ Lockout severo (10+ falhas) → alerta high com tags rotating_light, lock

Validação de CPF backend:

  • isValidCPF() aplicado em todos os endpoints que recebem CPF (6 chamadas em portal-escola, 1 em send-otp, 2 em gestao-responsaveis)

O que NÃO está implementado

  1. Rate limiting em endpoints autenticados — Nenhuma Edge Function autenticada (admin-escolas, gestao-alunos, etc.) possui rate limiting.

  2. Rate limiting no Worker Cloudflare — O gateway não implementa rate limiting próprio.

Criticidade: Baixo (anteriormente Médio)

Reduzido de Médio para Baixo após implementação da Fase A.1 com dual check (IP + escola_id), alertas push ntfy e validação CPF backend.

Evidência técnica

ArquivoDescrição
portal-escola/index.ts:33-51Constantes de rate limit Fase A.1
portal-escola/index.ts:87-110checkRateLimit() com keyField configurável
portal-escola/index.tsAlertas ntfy em rate limit OTP escola + lockout severo
send-otp/index.ts:18-19Constantes de rate limit do sistema
_shared/cpf-validator.tsValidação matemática de CPF (dígitos verificadores)

6. Lockout Progressivo

Status: ✅ Implementado (Portal)

O que está implementado

Portal público implementa lockout progressivo por identificador+IP:

Tentativas falhas (24h)LockoutEvidência
31 minutoportal-escola/index.ts:30
65 minutosportal-escola/index.ts:31
1030 minutosportal-escola/index.ts:32
typescript
const LOCKOUT_THRESHOLDS = [
  { attempts: 3, lockoutMinutes: 1 },
  { attempts: 6, lockoutMinutes: 5 },
  { attempts: 10, lockoutMinutes: 30 },
];

Implementação em checkLockout() (portal-escola/index.ts:79-119): consulta tentativas falhas nas últimas 24h por identificador OU IP, aplica o threshold mais alto atingido.

O que NÃO está implementado

  1. Lockout no sistema adminverify-otp tem proteção de brute force (5 tentativas = lockout 15min) mas não tem lockout progressivo. Após os 15 minutos, o usuário pode tentar novamente sem penalidade crescente.

  2. Lockout permanente — Não há mecanismo de ban permanente após X tentativas.

Criticidade: Baixo

Lockout progressivo está presente no endpoint mais exposto (portal público). O sistema admin tem proteção básica suficiente.

Evidência técnica

ArquivoLinha(s)Descrição
portal-escola/index.ts:29-33LOCKOUT_THRESHOLDS3 níveis de lockout
portal-escola/index.ts:79-119checkLockout()Lógica de lockout progressivo
verify-otp/index.ts:46-47ConstantesMAX_FAILED_ATTEMPTS = 5, LOCKOUT_MINUTES = 15

7. Proteção contra Brute Force

Status: ✅ Implementado

O que está implementado

verify-otp (sistema admin):

  • Máximo de 5 tentativas falhas em 15 minutos = lockout de 15 minutos
  • OTP invalidado após 3 erros no mesmo código (verify-otp/index.ts:282-288)
  • Tentativas falhas contabilizadas por usuario_id, não por IP
  • Incidente de bloqueio logado como auth.bloqueio_ip com detalhes de segurança
typescript
// Invalidar OTP se atingiu 3 tentativas falhas neste código específico
if (newAttempts >= 3) {
  await supabase
    .from('login_otps')
    .update({ usado_em: new Date().toISOString() })
    .eq('id', latestOtp.id);
  console.warn(`🔒 OTP ${latestOtp.id} invalidado após 3 tentativas falhas`);
}

portal-escola (portal público):

  • Lockout progressivo (ver seção 6)
  • Cada tentativa registrada em portal_login_tentativas
  • Mensagem genérica em caso de falha

O que NÃO está implementado

  1. CAPTCHA — Nenhum endpoint usa CAPTCHA ou desafio similar após múltiplas falhas.
  2. Notificação ao usuário — O titular da conta não é notificado sobre tentativas falhas contra sua credencial.

Criticidade: Baixo

Proteção adequada para o perfil de risco da aplicação.

Evidência técnica

ArquivoLinha(s)Descrição
verify-otp/index.ts:46-47ConstantesMAX_FAILED_ATTEMPTS = 5, LOCKOUT_MINUTES = 15
verify-otp/index.ts:188-236Brute force checkSoma tentativas falhas em OTPs recentes
verify-otp/index.ts:259-298OTP invalidoIncrementa contador + invalida OTP após 3 erros

8. CORS

Status: ✅ Implementado

O que está implementado

CORS configurado em _shared/cors-helpers.ts com:

  1. Origin exata — Nunca usa *. Retorna a origin exata da requisição se permitida.
  2. Whitelist estáticalocalhost:5173, localhost:8080, preview Lovable, olp.digital
  3. Whitelist dinâmica — Qualquer subdomínio de .lovableproject.com, .lovable.app e .olp.digital
  4. CredentialsAccess-Control-Allow-Credentials: true (obrigatório para cookies)
  5. Preflight cacheAccess-Control-Max-Age: 86400 (24h)
  6. Headers permitidosauthorization, x-client-info, apikey, content-type, cookie, accept
  7. Fallback — Se origin não reconhecida, usa http://localhost:5173 (desenvolvimento)
typescript
export function isOriginAllowed(origin: string | null): boolean {
  if (!origin) return false;
  if (origin.endsWith('.lovableproject.com') || origin.endsWith('.lovable.app')) return true;
  if (origin === 'https://olp.digital' || origin.endsWith('.olp.digital')) return true;
  return ALLOWED_ORIGINS.includes(origin);
}
  1. Todas as respostas incluem CORS — Padrão seguido em todas as Edge Functions (sucesso E erro).

O que NÃO está implementado

  1. Validação de Referer — Apenas Origin é verificada. Referer não é validado como defesa adicional.

Criticidade: Baixo

CORS está corretamente implementado para o cenário de cookies HttpOnly + cross-origin.

Como testar manualmente (staging)

bash
# Testar CORS com origin válida
curl -v -X OPTIONS https://[SUPABASE_URL]/functions/v1/me \
  -H "Origin: https://olp.digital" \
  -H "Access-Control-Request-Method: POST"
# Esperado: 204 com Access-Control-Allow-Origin: https://olp.digital

# Testar CORS com origin inválida
curl -v -X OPTIONS https://[SUPABASE_URL]/functions/v1/me \
  -H "Origin: https://evil.com" \
  -H "Access-Control-Request-Method: POST"
# Esperado: Access-Control-Allow-Origin: http://localhost:5173 (fallback)
# Navegador bloqueará a requisição cross-origin

Evidência técnica

ArquivoLinha(s)Descrição
_shared/cors-helpers.ts:11-19ALLOWED_ORIGINSLista estática de origins
_shared/cors-helpers.ts:24-38isOriginAllowed()Validação com wildcard para subdomínios
_shared/cors-helpers.ts:43-65getCorsHeaders()Gera headers com origin exata
_shared/cors-helpers.ts:70-74handleCorsPrelight()Resposta 204 para OPTIONS

9. Headers de Segurança HTTP

Status: ✅ Implementado (atualizado 2026-03-01)

O que está implementado

  • Access-Control-* headers (via cors-helpers.ts)
  • Set-Cookie com flags de segurança (HttpOnly; Secure; SameSite=None)
  • Meta tags de segurança no index.html (implementado 2026-03-01):
    • Content-Security-Policy com política restritiva (self + backends explícitos)
    • X-Frame-Options: DENY
    • X-Content-Type-Options: nosniff
    • Referrer-Policy: strict-origin-when-cross-origin
    • Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
  • Headers HTTP reais no Cloudflare Worker (implementado 2026-03-01, verificado via curl -I):
    • Strict-Transport-Security: max-age=31536000; includeSubDomains
    • X-Frame-Options: DENY
    • X-Content-Type-Options: nosniff
    • Referrer-Policy: strict-origin-when-cross-origin
    • Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
    • Content-Security-Policy: default-src 'none'; frame-ancestors 'none' (CSP restritiva para API JSON)

O que NÃO está implementado

  1. HSTS Preload — O domínio não foi submetido ao hstspreload.org. O HSTS funciona normalmente (browser lembra por 1 ano após primeira visita), mas a primeira requisição de um usuário novo ainda passa por HTTP antes do redirect. Submissão é opcional e praticamente irreversível (3-6 meses para remover).

CSP Detalhada

Frontend (index.html — meta tag):

default-src 'self';
script-src 'self' 'unsafe-inline';     ← Necessário para Vite (builds inline)
style-src 'self' 'unsafe-inline';      ← Necessário para Tailwind/shadcn
img-src 'self' https: data: blob:;     ← Storage externo + data URIs
font-src 'self' data:;
connect-src 'self' https://*.supabase.co https://gateway.olp.digital https://olp.digital https://*.olp.digital https://ai.gateway.lovable.dev;
frame-ancestors 'none';                ← Anti-clickjacking

API (Worker — HTTP header):

default-src 'none'; frame-ancestors 'none'

Adequada porque o Worker só retorna JSON — nenhum recurso deve ser carregado.

Riscos Residuais

RiscoStatusNotas
Clickjacking✅ Mitigadoframe-ancestors 'none' + X-Frame-Options: DENY em meta tag e Worker
XSS via terceiros✅ MitigadoCSP restringe scripts a 'self'
MIME sniffing✅ MitigadoX-Content-Type-Options: nosniff em meta tag e Worker
Downgrade HTTPS✅ MitigadoHSTS ativo no Worker (max-age=31536000; includeSubDomains). Janela mínima na primeira visita (mitigada por redirect 301 do Cloudflare). Preload opcional.

Criticidade: Baixo (anteriormente Médio → Alto)

Reduzido de Médio para Baixo após implementação dos headers HTTP no Worker. Todos os 6 headers de segurança estão ativos tanto no frontend (meta tags) quanto na API (Worker headers). Único ponto opcional: submissão no hstspreload.org.

Evidência técnica

ArquivoDescrição
index.htmlMeta tags de segurança (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
Cloudflare Worker (externo)Objeto SECURITY_HEADERS com 6 headers injetados em todas as respostas
docs/architecture/CLOUDFLARE_WORKER_GATEWAY.md seção 9Documentação completa dos headers implementados
_shared/cors-helpers.tsCORS headers (inalterado)

10. Proteção contra Replay de Token

Status: ✅ Implementado (2026-03-01)

O que está implementado

  1. Expiração temporal do JWT — 8h admin, 2h portal
  2. Cookie HttpOnly — Previne captura via XSS
  3. jti (JWT ID) — Cada token recebe identificador único via crypto.randomUUID()
  4. Tabela token_blacklist — Tokens revogados no logout são inseridos com TTL de 8h
  5. Verificação de blacklistverifyAuthToken() consulta token_blacklist antes de aceitar token

O que NÃO está implementado

  1. Token binding — Token não está vinculado a fingerprint do dispositivo, IP ou User-Agent.
  2. Nonce/counter — Não há mecanismo para detectar reuso sequencial.

Riscos

RiscoCenário de AtaqueImpacto
Replay de tokenAtacante captura token via rede (MITM) e o reutiliza antes do logout. Após logout, token é blacklisted.Baixo

Criticidade: Baixo

Mitigado por HTTPS obrigatório (Secure flag), expiração curta, e revogação via blacklist no logout.

Evidência técnica

ArquivoDescrição
_shared/jwt.tssignAuthToken() inclui jti claim via crypto.randomUUID()
_shared/jwt.tsverifyAuthToken() consulta token_blacklist para validar jti
logout/index.tsInsere jti na token_blacklist com TTL de 8h

11. Logs Estruturados

Status: ✅ Implementado

O que está implementado

Sistema de logging centralizado via registrarLog() em _shared/logging-helper.ts:

  1. Geolocalização automática — Exclusivamente via Cloudflare Worker headers (X-Geo-City, X-Geo-Region, X-Geo-Country)
  2. IP extraction — Cadeia de headers: x-olp-client-ip > x-real-ip > cf-connecting-ip > x-forwarded-for
  3. Parâmetro req: req obrigatório — Todas as chamadas devem incluir o request para extração de geodados do Cloudflare Worker
  4. Diff automáticogerarAlteracoes() em diff-helper.ts para operações UPDATE

Formato padrão de ação: modulo.operacao (ex: login.success, escola.create, permissao.acesso_negado)

Campos da tabela logs_transacoes:

  • usuario_id, nome_usuario, papel_principal, acao
  • ip, cidade, estado, pais
  • detalhes (JSONB com tipo, entidade, entidadeId, resumo/alterações)

O que NÃO está implementado

  1. Logs de SELECT/READ — Trade-off consciente documentado. Não há registro de consultas.
  2. Log forwarding — Logs ficam apenas no Supabase. Não há integração com serviços de log externo (Datadog, Sentry, etc.).
  3. Retenção automática — Não há política de expiração/arquivamento de logs antigos em logs_transacoes.

Criticidade: Baixo

Logging está bem implementado para auditoria e debugging.

Como validar via logs

sql
-- Distribuição de ações por dia
SELECT 
  DATE(criado_em) as dia,
  acao,
  COUNT(*) as total
FROM logs_transacoes
WHERE criado_em >= NOW() - INTERVAL '7 days'
GROUP BY dia, acao
ORDER BY dia DESC, total DESC;

-- Verificar se logs têm geolocalização
SELECT 
  COUNT(*) FILTER (WHERE cidade IS NOT NULL) as com_geo,
  COUNT(*) FILTER (WHERE cidade IS NULL) as sem_geo
FROM logs_transacoes
WHERE criado_em >= NOW() - INTERVAL '7 days';

Evidência técnica

ArquivoLinha(s)Descrição
_shared/logging-helper.ts:37-58extractIP()Cadeia de headers para IP real
_shared/logging-helper.ts:64-70extractGeoFromHeaders()Geodados do Cloudflare Worker
_shared/logging-helper.ts:79-200obterLocalizacao()Priorização: CF > Cache > FreeIPAPI
_shared/logging-helper.ts:206-247registrarLog()Inserção com geoloc + service_role
_shared/diff-helper.ts:19-53gerarAlteracoes()Diff para operações UPDATE

12. Monitoramento e Alertas

Status: ✅ Implementado

O que está implementado

  1. CRON monitoring — Tabela cron_status com campos:

    • status: operacional | alerta | critico
    • tentativas_consecutivas: contador de falhas seguidas
    • sms_critico_enviado_em: cooldown de 2h para SMS
  2. Escalação automática (maintenance-cron/index.ts:188-312):

    • Falha 1-2: status alerta, retry em 15 minutos
    • Falha 3+: status critico, SMS para todos os admins (via enviarSMSCriticoAdmins())
    • Cooldown de 2h entre SMSs críticos
    • Alertas in-app (notificações) para todos os admins
  3. Dashboard de CRON — Componente admin-cron-monitor.tsx com visualização de status, última execução, erros.

  4. Logs de falha persistentes — Falhas de SMS crítico e cooldown são logadas em logs_transacoes:

    • manutencao.sms_critico_falha
    • manutencao.sms_critico_cooldown
  5. Alertas push (ntfy.sh) para portal — Implementado em Fase B (2026-03-07):

    • Lockout severo (≥10 falhas): alerta de prioridade alta via enviarAlertaPush()
    • Rate limit de OTP por escola atingido (100/15min): alerta para detectar abuso de custo SMS

O que NÃO está implementado

  1. Monitoramento de latência de Edge Functions — Não há tracking de P95/P99 de tempo de resposta.
  2. Health check externo — Não há serviço externo (UptimeRobot, Pingdom) verificando disponibilidade.
  3. Alertas de segurança adicionais — Não há alerta automático para:
    • Pico de permissao.acesso_negado
    • Login de geolocalização atípica
  4. Dashboard de segurança — Admin vê logs genéricos mas sem dashboard focado em incidentes de segurança.

Criticidade: Baixo-Médio

Monitoramento operacional está bom. Alertas push de segurança no portal implementados. Faltam alertas em endpoints autenticados.

Evidência técnica

ArquivoDescrição
maintenance-cron/index.ts:86-143enviarSMSCriticoAdmins()
maintenance-cron/index.ts:148-183criarAlertaAdmins()
maintenance-cron/index.ts:188-312escalarFalhaCron() — lógica completa de escalação

13. Segurança de Dependências (Supply Chain)

Status: ✅ Implementado (2026-03-01)

O que está implementado

  1. Frontend (npm): Dependências fixadas em package.json com bun.lockb
  2. Backend (Deno): Imports pinados por versão (ex: https://deno.land/std@0.168.0/http/server.ts, npm:@supabase/supabase-js@2)
  3. Dependabot configurado.github/dependabot.yml com atualizações automáticas de segurança para npm
  4. CI audit.github/workflows/audit.yml executa npm audit --audit-level=high em cada PR
  5. Migração xlsx → ExcelJS — Pacote xlsx (CVE-2023-30533) removido e substituído por exceljs (2026-03-01)

O que NÃO está implementado

  1. Hash de integridade — Deno imports não usam --lock ou hash de integridade
  2. SBOM — Sem Software Bill of Materials gerado
  3. Deno audit — Sem verificação automatizada de imports Deno

Criticidade: Baixo

Dependências fixadas, Dependabot ativo, CI audit configurado, e vulnerabilidade crítica (xlsx) migrada.

Evidência técnica

ArquivoDescrição
.github/dependabot.ymlConfiguração Dependabot para npm
.github/workflows/audit.ymlCI pipeline com npm audit
package.jsonExcelJS substituiu xlsx

14. Proteção contra Scraping e DoS

Status: ⚠️ Parcial

O que está implementado

  1. Rate limiting por IP — Em endpoints públicos (send-otp, portal-escola). Ver seção 5.

  2. Cloudflare como proxy — Supabase Edge Functions rodam atrás da infra Cloudflare (proteção DDoS básica de camada 3/4).

  3. Gateway Cloudflare Worker — Em produção, requests passam pelo Worker OLP que herda proteções de rede do Cloudflare.

  4. Rate limiting NAT-aware (Fase A.1) — Dual check por IP + escola_id com janelas curtas de 1 minuto, protegendo endpoints públicos do portal contra abuso em ambientes de alta densidade (escolas com NAT compartilhado). Ver seção 5 e docs/security/RATE_LIMITS.md.

O que NÃO está implementado

  1. WAF (Web Application Firewall) — Não há WAF configurado no Cloudflare
  2. CAPTCHA — Nenhum endpoint usa CAPTCHA (planejado para Fase E com Cloudflare Turnstile)
  3. Device fingerprinting — Não há identificação de dispositivo além do IP
  4. Bot detection — Não há detecção de comportamento automatizado
  5. Request body size limit — Não há validação de tamanho do body nas Edge Functions

Riscos

RiscoCenárioImpacto
Scraping de dados públicosBot enumera slugs de escolas via lookup_escolaBaixo (dados não sensíveis, rate limited 2000/min)
DoS em Edge FunctionRequests com payloads grandes ou queries complexasMédio
Proxy rotation bypassAtacante usa rede de proxies para bypass de rate limit por IPMédio (mitigado por dual check escola_id)

Criticidade: Baixo-Médio

Proteção robusta via rate limiting NAT-aware, lockout progressivo, e Cloudflare. CAPTCHA planejado (Fase E).

Evidência técnica

  • Rate limiting: ver seção 5
  • Cloudflare: Worker gateway em docs/architecture/CLOUDFLARE_WORKER_GATEWAY.md
  • Ausência de WAF/CAPTCHA: nenhum arquivo de configuração encontrado

15. Segurança de Secrets e Variáveis de Ambiente

Status: ✅ Implementado

O que está implementado

Secrets configurados no Supabase (Edge Functions):

SecretPropósitoSensibilidade
OLP_JWT_SECRETAssinatura JWT do sistema e portal (unificado — PostgREST exige secret único para RLS)🔴 Crítico
TWILIO_ACCOUNT_SIDConta Twilio para SMS🔴 Crítico
TWILIO_AUTH_TOKENAutenticação Twilio🔴 Crítico
TWILIO_FROM_NUMBERNúmero de origem SMS🟢 Baixo

| CRON_SECRET | Autenticação de CRON jobs | 🟡 Médio | | MP_ACCESS_TOKEN | MercadoPago access token | 🔴 Crítico | | SUPABASE_SERVICE_ROLE_KEY | Bypass de RLS (uso restrito) | 🔴 Crítico |

Medidas implementadas:

  1. Nenhum secret no código-fonte — Todos os secrets são acessados via Deno.env.get()
  2. Frontend não tem acesso a secrets privados — Apenas VITE_SUPABASE_URL e VITE_SUPABASE_PUBLISHABLE_KEY (chaves públicas por design)
  3. Service role key restrito — Documentação explícita no código (supabase-client.ts:93-101) sobre uso exclusivo para logging e operações de sistema

O que NÃO está implementado

  1. Rotação de secrets — Sem política ou mecanismo de rotação automática
  2. Vault/HSM — Secrets armazenados diretamente no Supabase (sem Vault externo)
  3. Audit de acesso a secrets — Não há log de qual Edge Function acessou qual secret
  4. Alerta de secret leak — Não há monitoramento de exposição de secrets em logs ou repositório

Riscos

RiscoCenárioImpacto

| Secret leak em logs | console.log() acidentalmente loga valor de token/secret | Alto | | Rotação impossível | Troca de OLP_JWT_SECRET invalida todas as sessões ativas | Médio |

Criticidade: Médio

Secrets estão corretamente segregados. Risco principal é ausência de rotação e presença do bypass code em produção.

Como testar manualmente (staging)

bash
# Verificar que secrets não vazam em respostas
curl -s -X POST https://[SUPABASE_URL]/functions/v1/me \
  -H "Content-Type: application/json" \
  -H "Origin: https://olp.digital" | jq .
# Nenhum campo deve conter valores de secrets

Evidência técnica

ArquivoDescrição
_shared/supabase-client.ts:93-101Comentários de restrição de createSupabaseSystem()
_shared/jwt.ts:34-37Validação de OLP_JWT_SECRET presente
_shared/twilio-sms.ts:73-77Validação de credenciais Twilio
.envApenas variáveis VITE_* (chaves públicas)

Matriz de Risco Consolidada

#DomínioStatusCriticidadePrioridade de Remediação
1Autenticação JWT✅ Impl.Baixo-
2Autorização RBAC✅ Impl.Baixo-
3Multi-Tenant✅ Impl.MédioP3 (testes E2E)
4IDOR/UUID⚠️ ParcialMédioP3
5Rate Limiting✅ Impl.BaixoP3 (endpoints auth)
6Lockout Progressivo✅ Impl.Baixo-
7Brute Force✅ Impl.Baixo-
8CORS✅ Impl.Baixo-
9Headers HTTP✅ Impl.Baixo-
10Replay de Token✅ Impl.Baixo-
11Logs Estruturados✅ Impl.Baixo-
12Monitoramento✅ Impl.Baixo-MédioP3 (alertas auth)
13Supply Chain✅ Impl.Baixo-
14Scraping/DoS⚠️ ParcialBaixo-MédioP3 (CAPTCHA Fase E)
15Secrets✅ Impl.MédioP3 (rotação)

Plano de Ação Recomendado

P1 — Imediato ✅ CONCLUÍDO

  • [x] Adicionar headers de segurança em index.html — Meta tags CSP, X-Frame-Options, etc.
  • [x] Configurar headers de segurança no Cloudflare Worker — HSTS + 5 headers HTTP

P2 — Curto prazo ✅ CONCLUÍDO

  • [x] Implementar jti no JWT e blacklist de tokenstoken_blacklist com TTL 8h (2026-03-01)
  • [x] Configurar Dependabot no repositório.github/dependabot.yml (2026-03-01)
  • [x] Adicionar npm audit no pipeline de CI.github/workflows/audit.yml (2026-03-01)
  • [x] Migrar xlsx (CVE-2023-30533) para ExcelJS — (2026-03-01)

P3 — Médio prazo (em andamento)

  • [x] Implementar alertas push no portal (rate limit + lockout severo) — ntfy.sh (Fase B, 2026-03-07)
  • [x] Validação de CPF/CNPJ em todos os endpointsisValidCPF() (Fase C, 2026-03-07)
  • [x] Remover console.logs com dados sensíveis — Fase A (2026-03-07)
  • [x] Rate limiting NAT-aware no portal — Dual check IP + escola_id (Fase A.1, 2026-03-07)
  • [x] Verificar/desabilitar dev-bypass-login em produção — Removido completamente (Mar/2026)
  • [ ] Adicionar rate limiting em endpoints autenticados
  • [ ] Criar testes E2E de isolamento multi-tenant
  • [ ] Implementar política de rotação de secrets
  • [ ] Adicionar CAPTCHA após múltiplas falhas de login (Fase E — Cloudflare Turnstile planejado)
  • [ ] Health check externo
  • [ ] Dashboard de segurança dedicado

Apêndice A: Checklist de Verificação Rápida

[✅] JWT customizado com cookies HttpOnly
[✅] Separação admin/portal com claims distintas
[✅] Fallback Authorization header REMOVIDO
[✅] RBAC com requireRole() em todas as Edge Functions
[✅] logAccessDenied() automático
[✅] RLS ativo em 100% das tabelas (57)
[✅] Rate limiting em endpoints públicos (NAT-aware dual check)
[✅] Lockout progressivo no portal
[✅] Brute force protection no verify-otp
[✅] CORS com origin exata (nunca *)
[✅] OTP hasheado com SHA-256
[✅] OTP com expiração de 5 minutos
[✅] Mensagens genéricas (não revela existência de usuário)
[✅] Logs estruturados com geolocalização
[✅] Monitoramento CRON com escalação SMS
[✅] Cookies de logout em múltiplos domínios
[✅] Headers de segurança HTTP (CSP, X-Frame-Options, HSTS) — meta tags + Worker
[✅] Revogação server-side de JWT (jti + token_blacklist)
[✅] Anti-replay (jti)
[✅] Audit automatizado de dependências (Dependabot + CI audit)
[✅] Alertas push portal (ntfy.sh — lockout severo + rate limit OTP)
[✅] Validação CPF/CNPJ em endpoints críticos
[✅] Console.logs sensíveis removidos
[❌] CAPTCHA (planejado Fase E — Cloudflare Turnstile)
[❌] WAF
[❌] Health check externo
[❌] Rotação de secrets
[❌] Rate limiting em endpoints autenticados

Relatório gerado automaticamente via análise de código-fonte. Revisão manual recomendada para validação de políticas RLS individuais e configurações de infraestrutura não acessíveis via código.