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 DEFINERempublic— ACLEXECUTEaberto - Auth settings — política de senha, leaked password protection
- OWASP top 10 aplicáveis ao stack Next.js + Supabase
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ção | Risco 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) |
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 comverify_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 keytest-stripe-flow,create-new-stripe-prices,diag-service-role,sync-service-key-to-vault— todas hardcodedstatus: 410 disabled
DELETE /v1/projects/{ref}/functions/{slug}).
Edge functions restantes (legítimas):
create-checkout-session,create-portal-session,stripe-webhooksend-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: 10password_required_characters: lower + upper + digit(1 minúscula + 1 maiúscula + 1 número obrigatórios)
⏸️ 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). - ✅
resetPasswordrate-limited (3 req/h por user-alvo viacheck_rate_limitRPC). - ✅
deleteUserbloqueia super_admin (impede admin removendo outro super_admin pelo painel). - ✅
changePlanbloqueia troca se hástripe_subscription_id(evita divergência de cobrança Stripe). - ✅
admin_audit_logpopulado 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-sessionvalidam JWT do user antes de chamar Stripe API. - ✅
soft_deleteRPC valida ownership via JOIN comworkspace_membersantes de deletar (whitelist de tabelas +format()/EXECUTE USING— sem SQL injection). - ✅ OFX upload server action chama
requireUser+assertWorkspaceOwnership+assertCartaoInWorkspaceantes de inserir. - ✅
.env.local.exampledocumenta corretamente queSERVICE_ROLE_KEYbypassa RLS e nunca deve serNEXT_PUBLIC_. - ✅ Sem
policy_exists_rls_disabled, semauth.usersexposto em views. Advisor de segurança Supabase: 0 ERROR, 79 WARN, 1 INFO.
Resumo executivo
| Item | Severidade original | Status |
|---|---|---|
trigger_send_monthly_reminders aberto | MÉDIA | ✅ REVOKE aplicado |
| 3 funções DEFINER admin abertas | BAIXA | ✅ REVOKE aplicado |
Webhook Stripe sem dedup event.id | BAIXA | ✅ Tabela + edge fn v28 |
| 5 edge functions one-shot vivas | BAIXA | ✅ Deletadas via Management API |
| Política de senha fraca | BAIXA | ✅ min 10 + lower/upper/digit |
| Leaked password protection (HIBP) | INFO | ⏸️ Paywall Pro Plan |