Todos os posts
Incidente6 min de leitura

7.403 apps do Lovable vazaram dados de usuários. A sua tabela do Supabase está entre elas?

Translucent database cylinder with an open glowing drawer spilling envelopes and ID cards into the dark

Nesta semana, a Crissy Field, uma empresa de segurança de Hamburgo, publicou junto com o jornal econômico alemão Handelsblatt uma pesquisa sobre apps criados com o Lovable. Eles extraíram 159.041 projetos do Lovable do arquivo do Common Crawl, cobrindo apps criados entre dezembro de 2025 e setembro de 2026, e fizeram uma pergunta simples: o que o banco de dados entrega para um completo desconhecido que não está logado?

A resposta não foi boa. Segundo a reportagem da Startup Fortune, 34.545 desses projetos usavam o Supabase como backend, e 24.664 deles retornavam registros do banco sem nenhuma autenticação. Em 7.403 sites, esses registros incluíam endereços de e-mail, espalhados por 10.560 tabelas. A RuntimeWire relata que os dados expostos incluíam números de passaporte, informações de pagamento e mais de 200.000 mensagens de chat. Um prestador de serviços tinha mais de 1.000 registros de passaporte legíveis; um fórum de lifestyle vazou 80.000 perfis de usuários junto com essas mensagens de chat.

Não houve nenhum zero-day envolvido, e nada foi "hackeado" no sentido dramático. Os pesquisadores usaram requisições somente leitura com a própria chave pública de cada site. Os dados simplesmente estavam lá para quem pedisse.

Se você colocou algo no ar com Supabase, principalmente usando um builder de IA, este post mostra como verificar seu próprio app em poucos minutos e corrigir do jeito certo. Não é só um problema do Lovable: Bolt, Cursor, v0 e apps escritos à mão cometem o mesmo erro.

O que deu errado de fato

O Supabase expõe suas tabelas do Postgres por meio de uma API REST gerada automaticamente. Seu frontend conversa com essa API usando a anon key (também chamada de publishable key). Essa chave foi feita para ser pública. Ela vai no seu bundle de JavaScript, e qualquer pessoa pode lê-la no DevTools.

Esse design funciona bem, desde que toda tabela tenha Row Level Security (RLS) ativado com políticas sensatas. O RLS é o que decide quais linhas cada requisição pode ver. Como observa o TheCyberSecGuru, citando a própria documentação do Supabase: "Sem RLS, qualquer pessoa com a sua anon key pode acessar todos os dados."

Então a cadeia de falhas é esta:

  • Uma ferramenta de IA (ou um humano cansado) gera create table profiles (...).
  • Ninguém adiciona enable row level security.
  • O app funciona perfeitamente nos testes, porque o bug é justamente ser permissivo demais.
  • Qualquer pessoa que copie a anon key do seu bundle pode rodar select * nessa tabela.

Se quiser entender como o RLS funciona, nosso guia sobre Row Level Security no Supabase cobre o básico. A seguir, o foco é testar e corrigir.

Row Level Security no Supabase: o guia completo

Como verificar seu app (5 minutos)

1. Olhe de fora, como os pesquisadores fizeram. Abra seu site em produção, abra o DevTools e procure supabase.co no JavaScript carregado. Você vai encontrar a URL do seu projeto e a anon key logo ao lado. Depois, em um terminal, peça uma linha de uma tabela que você considera privada:

curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=1" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

O que a resposta diz:

  • Um array vazio [] ou um erro de permissão: ótimo, usuários anônimos não conseguem ler essa tabela.
  • Linhas reais: você tem o mesmo problema daqueles 7.403 sites.

Repita para cada tabela que guarda dados de usuários: profiles, users, messages, orders, bookings, leads, waitlist e assim por diante. Tabelas de lista de espera e de formulário de contato são fáceis de esquecer e quase sempre contêm e-mails.

Já que você está no bundle, confira também se a chave encontrada é a anon key e não a service_role key. O service role ignora o RLS por completo. Se ela está no seu frontend, considere-a vazada e faça a rotação agora: tudo o que está no seu bundle pode ser lido por qualquer visitante e por qualquer script de terceiros da página, como os usuários da Polymarket descobriram, e bots já estão caçando chaves vazadas. Nosso post sobre como rotacionar os segredos do seu app sem downtime explica a rotação, e API keys expostas em JavaScript no lado do cliente mostra o que mais costuma parar nos bundles.

API keys no código frontend: o que é seguro publicar

2. Verifique por dentro. No editor SQL do Supabase, liste todas as tabelas do schema public e se o RLS está ativado:

select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Qualquer linha com rowsecurity = false pode ser lida (e possivelmente gravada) com a anon key. O Security Advisor no painel do Supabase também aponta esses casos. Leia os avisos em vez de ignorá-los.

3. Procure por "RLS ativado, mas aberto mesmo assim". Ter o RLS ativado não é a linha de chegada. Liste suas políticas:

select tablename, policyname, roles, cmd, qual
from pg_policies
where schemaname = 'public';

Fique atento a qual = true em uma política select que se aplica a anon ou public. É uma política que diz "todo mundo pode ler tudo", o que equivale a não ter RLS. Ferramentas de IA às vezes geram exatamente isso para fazer um erro sumir.

Como corrigir

Ative o RLS em todas as tabelas:

alter table public.profiles enable row level security;
alter table public.messages enable row level security;
-- repeat for every table in public

Com o RLS ativado e sem políticas, a tabela fica bloqueada: a anon key não recebe nada. É um padrão seguro. Agora adicione só o acesso de que você realmente precisa.

Escreva políticas restritas. Uma configuração típica de "cada usuário só pode ver e editar a própria linha":

create policy "Read own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Update own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

Pontos de atenção:

  • Restrinja a authenticated, não a public, a menos que os dados sejam realmente públicos (posts de blog, listagens de produtos).
  • Separe leitura e escrita. Uma tabela pode ser segura para leitura pública e ainda assim precisar de inserts bem restritos.
  • Tabelas públicas também vazam colunas. Se uma tabela profiles é pública para exibir nomes, uma coluna de e-mail ou telefone na mesma tabela também é pública. Mova os campos privados para uma tabela separada ou exponha uma view só com as colunas seguras.
  • Views podem ignorar o RLS. No Postgres, views rodam por padrão com os privilégios do dono. Crie views com security_invoker = true para que o RLS de quem faz a chamada seja aplicado.
  • O storage é separado. Buckets públicos servem arquivos para qualquer pessoa com a URL. Documentos de identidade, faturas e avatares enviados por usuários privados devem ficar em buckets privados com políticas de storage.

Faça disso o padrão, não uma tarefa de limpeza. A amostra dos pesquisadores cobre dez meses de apps novos, o que significa que isso continua acontecendo a cada nova tabela. Torne difícil esquecer:

  • Adicione uma regra às instruções de projeto da sua ferramenta de IA: todo create table em public deve vir seguido de enable row level security e políticas explícitas.
  • Revise as migrations geradas em busca de create table sem o enable row level security correspondente antes de fazer o merge.
  • Refaça a verificação de fora depois de cada mudança de schema, não só no lançamento.

Se você usa Firebase em vez de Supabase, o mesmo tipo de erro existe lá na forma de regras de segurança abertas. Veja Firebase Security Rules: pare de deixar seu banco de dados aberto.

Firebase Security Rules: proteja seu banco de dados

Se você encontrou dados expostos

Corrigir a política impede leituras futuras, mas não desfaz as passadas. Seja honesto consigo mesmo sobre o que estava acessível e por quanto tempo. Verifique os logs da API do Supabase em busca de leituras em massa incomuns vindas de IPs desconhecidos, faça a rotação de qualquer segredo que estivesse guardado em tabelas legíveis e confira suas obrigações de notificação caso dados pessoais como e-mails, documentos ou dados de pagamento tenham sido expostos. No Brasil, isso provavelmente envolve as regras da LGPD sobre incidentes de segurança; na UE, as regras do GDPR sobre violações de dados.

Checklist rápido

Antes de dar por encerrado, garanta que:

  • Toda tabela em public tem rowsecurity = true.
  • Nenhuma política select usa using (true) em tabelas com dados privados.
  • As políticas são restritas a authenticated e vinculadas a auth.uid().
  • Colunas privadas não ficam em tabelas com leitura pública.
  • As views usam security_invoker = true.
  • Só a anon key está no seu frontend, nunca a service_role.
  • Arquivos privados ficam em buckets de storage privados.
  • Você testa de novo de fora depois de cada mudança de schema.

Verifique seu app de fora

Nosso checklist de segurança para apps feitos com IA cobre as outras coisas que vale a pena verificar antes do lançamento. O desconfortável nesta pesquisa é como o método foi comum: chave pública, requisição somente leitura, os dados voltam. Essa é exatamente a visão que um atacante tem, e é a visão que você deveria verificar com regularidade.

O AppSafe escaneia seu app em produção de fora. O scan completo verificado inclui verificações somente leitura de bancos de dados abertos do Supabase e do Firebase e procura API keys expostas nos seus bundles de JavaScript, e os scans agendados podem alertar você quando algo muda depois de um deploy. Quer ver como é um relatório? Dê uma olhada no relatório de exemplo.

Faça agora um scan gratuito do seu app e descubra o que um desconhecido consegue ver antes que outra pessoa descubra.

Rode esta checklist automaticamente

Um escaneamento grátis verifica todos esses problemas no seu app e te dá um prompt de correção com IA para cada achado.

Escanear meu app grátis