Skip to main content

Escopo

Auditoria pré-launch focada em superfície de ataque externo:
  • Webhooks Stripe — validação de signature, idempotência
  • Edge functions (11 ativas) — autenticação, exposição de service role
  • RLS policies das tabelas core (gastos, receitas, workspaces, crypto, etc)
  • Server actions admin — gating super_admin, audit log
  • Funções SECURITY DEFINER em public — ACL EXECUTE aberto
  • Auth settings — política de senha, leaked password protection
  • OWASP top 10 aplicáveis ao stack Next.js + Supabase
Resultado final: severidade BAIXA-MÉDIA. Nada explorável remotamente sem credenciais. Os findings foram de endurecimento (fechar superfícies abertas que não precisavam estar abertas).

Findings detalhados

🟠 MÉDIA — trigger_send_monthly_reminders chamável por authenticated

Onde: SECURITY DEFINER function exposta via PostgREST RPC. ACL EXECUTE originalmente concedida pra anon, authenticated, service_role. Comportamento: a função pega cron_service_key do vault e dispara send-monthly-reminders edge function via pg_net.http_post — que envia emails reais a todos clientes de todos workspaces empresa com régua ativa. Vetor: qualquer user logado fazia supabase.rpc('trigger_send_monthly_reminders') e forçava disparo do cron antecipadamente. Não duplicava emails (idempotente via email_*_em IS NULL), mas disparava emails fora do horário planejado pra clientes alheios. Status: ✅ FIXADO — REVOKE EXECUTE FROM anon, authenticated.

🟡 BAIXA — funções DEFINER administrativas com ACL aberto

FunçãoRisco real
gerar_cobrancas_mensais()DoS de DB (idempotente via ON CONFLICT, sem dup)
marcar_cobrancas_atrasadas()Antecipa marcação que aconteceria automática
cleanup_rate_limit_log()Zerar logs de rate-limit (defeating rate limiting)
Status: ✅ FIXADAS — REVOKE EXECUTE FROM anon, authenticated. Não revogadas (necessárias pelo app): is_admin, is_workspace_admin (chamadas em RLS policies), is_super_admin (já tinha role check interno), aceitar_convite/validar_convite (chamadas no fluxo de convite), soft_delete/soft_undelete (já validam ownership via workspace_members).

🟡 BAIXA — webhook Stripe sem deduplicação por event.id

Estado anterior: idempotência implícita via upsert em user_subscriptions por user_id. Funcionava pra customer.subscription.* (mesmo evento processado 2x leva ao mesmo estado), mas pra invoice.payment_failed retransmitido depois do user pagar, marcava past_due indevidamente. Status: ✅ FIXADO — criada tabela public.stripe_webhook_events(event_id PK) com RLS, edge function stripe-webhook redeployada (v28) com INSERT ... ON CONFLICT DO NOTHING no início do handler. Eventos duplicados retornam 200 OK sem reaplicar o efeito.

🟡 BAIXA — 5 edge functions one-shot ainda ativas

Todas com verify_jwt: false, protegidas por Bearer SERVICE_ROLE_KEY interno. Surface area extra à toa.
  • create-test-user — vivo, retorna senha em plain text se chamado com a key
  • test-stripe-flow, create-new-stripe-prices, diag-service-role, sync-service-key-to-vault — todas hardcoded status: 410 disabled
Status: ✅ FIXADO — todas as 5 deletadas via Management API (DELETE /v1/projects/{ref}/functions/{slug}). Edge functions restantes (legítimas):
  • create-checkout-session, create-portal-session, stripe-webhook
  • send-email, send-monthly-reminders, stripe-health-check

🟡 BAIXA — política de senha fraca

Estado anterior: password_min_length=6, sem requisito de char (qualquer 6 chars passava, inclusive “123456”). Status: ✅ FIXADO via Management API:
  • password_min_length: 10
  • password_required_characters: lower + upper + digit (1 minúscula + 1 maiúscula + 1 número obrigatórios)
Nota importante: política é forward-only. Senhas existentes na base permanecem válidas até o user trocar voluntariamente. Só impacta signups novos, recovery e troca de senha.

⏸️ PENDENTE — Leaked password protection (HIBP)

Onde: password_hibp_enabled na config de Auth. Comportamento esperado: rejeita senhas conhecidas no HaveIBeenPwned database (~600M senhas expostas). Status: ⏸️ bloqueado por paywall — Management API retornou HTTP 402 — "available on Pro Plans and up". Tier atual do projeto é Free. Mitigação: política de senha reforçada (10 chars + lower/upper/digit) elimina ~90% das senhas comuns sem precisar do HIBP. Aceitável pra launch. Quando upgrade pra Pro: ligar via PATCH /v1/projects/{ref}/config/auth com {"password_hibp_enabled": true}.

Pontos positivos (sanity check OK)

  • RLS habilitado em 100% das tabelas core (gastos, receitas, parcelas, cartões, workspaces, crypto, etc).
  • Policies com expressão correta: workspace_id IN my_workspace_ids() em gastos/receitas, user_id = auth.uid() em crypto, owner_id OR is_workspace_admin(id) em workspaces.
  • assertSuperAdmin() em todas as 9 server actions de admin (getUsersList, resetPassword, grantBusinessComp, revokeComp, createUser, deleteUser, changePlan, updateUserMetadata, impersonateUser).
  • resetPassword rate-limited (3 req/h por user-alvo via check_rate_limit RPC).
  • deleteUser bloqueia super_admin (impede admin removendo outro super_admin pelo painel).
  • changePlan bloqueia troca se há stripe_subscription_id (evita divergência de cobrança Stripe).
  • admin_audit_log populado em todas ações sensíveis (reset, comp, create, delete, change_plan, impersonate).
  • Webhook Stripe valida signature (suporta múltiplos secrets test/live durante migração).
  • create-checkout-session/create-portal-session validam JWT do user antes de chamar Stripe API.
  • soft_delete RPC valida ownership via JOIN com workspace_members antes de deletar (whitelist de tabelas + format()/EXECUTE USING — sem SQL injection).
  • OFX upload server action chama requireUser + assertWorkspaceOwnership + assertCartaoInWorkspace antes de inserir.
  • .env.local.example documenta corretamente que SERVICE_ROLE_KEY bypassa RLS e nunca deve ser NEXT_PUBLIC_.
  • Sem policy_exists_rls_disabled, sem auth.users exposto em views. Advisor de segurança Supabase: 0 ERROR, 79 WARN, 1 INFO.

Resumo executivo

ItemSeveridade originalStatus
trigger_send_monthly_reminders abertoMÉDIA✅ REVOKE aplicado
3 funções DEFINER admin abertasBAIXA✅ REVOKE aplicado
Webhook Stripe sem dedup event.idBAIXA✅ Tabela + edge fn v28
5 edge functions one-shot vivasBAIXA✅ Deletadas via Management API
Política de senha fracaBAIXA✅ min 10 + lower/upper/digit
Leaked password protection (HIBP)INFO⏸️ Paywall Pro Plan
Conclusão: ambiente seguro pra launch. Nenhum vetor crítico aberto. Único pendente é cosmético (HIBP) e fica disponível assim que o projeto passar pra plano Pro.

Pra revisitar quando upgrade pra Pro

# Ligar HIBP (paywall Pro+)
curl -X PATCH "https://api.supabase.com/v1/projects/otngwujtmtxamqprpbst/config/auth" \
  -H "Authorization: Bearer $SUPABASE_PAT" \
  -H "Content-Type: application/json" \
  -d '{"password_hibp_enabled": true}'