← Voltar ao blog
SegurançaScanaris Team · 9 min de leitura · October 03, 2026

Lovable e a vulnerabilidade de RLS (CVE-2025-48757): porque é que a tua aplicação pode estar a expor dados

CVE-2025-48757 é uma vulnerabilidade do Lovable classificada como crítica (CVSS 9.3) que expôs mais de 170 aplicações. O problema: qualquer pessoa, sem iniciar sessão, conseguia ler, e em muitos casos modificar, dados na tua base de dados: nomes, emails, chaves de API de terceiros, até o estado dos pagamentos. A causa de fundo não é um bug escondido do Lovable, mas um padrão comum a quase todas as apps "vibe-coded": confiar toda a segurança da base de dados às políticas de RLS do Supabase… e deixá-las incompletas. Este guia explica o que aconteceu exatamente, como confirmar em cinco minutos se a tua app tem o mesmo buraco, como o fechar com SQL concreto e o que fazer se já expuseste dados.

O que aconteceu realmente

O Lovable constrói apps em que o browser fala diretamente com a base de dados (Supabase) através de chamadas REST, usando a chave pública anon. É um desenho sólido desde que todas as tabelas tenham políticas de Row-Level Security (RLS) a decidir que linhas cada utilizador pode ler ou tocar. A vulnerabilidade apareceu porque muitos projetos foram publicados com tabelas sem RLS ou com RLS insuficiente. Um atacante só tinha de editar o pedido REST, mudar um filtro no URL, para pedir "todas as linhas" de uma tabela que devia estar protegida.

Para perceber porque basta editar um URL, tens de saber como o Supabase funciona por baixo. À frente da tua base de dados Postgres há uma camada chamada PostgREST que transforma cada tabela num endpoint HTTP: GET /rest/v1/customers?select=* devolve linhas da tabela customers. A chave anon não é um segredo: vai no JavaScript da tua app e qualquer pessoa a pode ver. A única coisa que decide que linhas saem desse endpoint são as políticas de RLS da tabela. Sem elas, a resposta é a tabela inteira.

Foi reportada em março de 2025 por Matt Palmer e Kody Low, e de forma independente por Danial Asaria, com divulgação pública a 29 de maio de 2025 (análise técnica, registo do CVE). Qualquer projeto Lovable com uma base de dados criada até 15 de abril de 2025, inclusive, pode estar afetado.

Cronologia em resumo

  • Março de 2025: primeiros relatórios privados ao Lovable; encontram-se apps publicadas com tabelas legíveis sem sessão.
  • 15 de abril de 2025: a data-limite que o Lovable dá para os projetos afetados; as apps geradas depois incluem alterações na forma como as tabelas são criadas.
  • 29 de maio de 2025: divulgação pública e atribuição do CVE com pontuação crítica.
  • Depois: o Lovable acrescenta avisos de segurança no editor, mas as políticas continuam a ser responsabilidade de quem publica a app.

Porque te afeta mesmo que não uses o Lovable

Isto não é um problema exclusivo do Lovable. Qualquer app que fale com o Supabase diretamente a partir do browser, quer a tenhas feito com Bolt, v0, Cursor, Replit ou à mão, depende de o RLS estar correto em todas as tabelas. A app parece perfeita, o login funciona, mas a segurança a sério vive na base de dados, não na interface. Esconder um botão no frontend não protege nada: o pedido REST continua lá para quem souber onde procurar.

O mesmo vale para o Firebase com regras em "modo de teste", para o PocketBase sem regras nas coleções, ou para qualquer backend-as-a-service que o browser chame diretamente. O padrão é sempre o mesmo: a IA que gerou a tua app sabe pô-la a funcionar, mas raramente escreve as regras que a impedem de funcionar para quem não devia.

Como saber se a tua app é vulnerável (5 minutos)

Teste 1: o pedido anónimo

  • Abre as DevTools (F12) → separador Network enquanto usas a tua app e repara nas chamadas a *.supabase.co/rest/v1/. Copia um desses URLs por inteiro (a apikey vai num header; no URL vês a tabela e o filtro).
  • Abre-o numa janela anónima, sem sessão. Se devolver dados, qualquer visitante também os recebe.
  • Tenta pedir a mais: se uma chamada devolve os teus dados, tira o filtro do URL (a parte ?user_id=eq.…). Se entregar linhas de outros utilizadores, o teu RLS não está a funcionar.

Teste 2: o painel do Supabase

Abre o teu projeto → Table Editor. Cada tabela sem RLS tem uma etiqueta vermelha "RLS disabled". Depois vai a Authentication → Policies: uma tabela com RLS ativo mas sem política nega tudo (bom para os dados, mas a tua app deixa de funcionar), e uma com uma política using (true) está tão aberta como se o RLS estivesse desligado. O painel tem ainda um Security Advisor (em Advisors) que lista exatamente estas tabelas.

Teste 3: uma consulta SQL

No SQL Editor do painel, esta consulta lista as tabelas do esquema public (o que a API expõe) que não têm RLS ativo:

select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind = 'r'
  and not c.relrowsecurity;

Se devolver alguma linha com dados reais, essa tabela está aberta através da API neste preciso momento.

Como corrigir, passo a passo

1. Ativa o RLS em todas as tabelas

Não só nas "importantes". A regra é negar por defeito: sem uma política que o permita, ninguém entra. Para cada tabela do esquema public:

alter table public.customers enable row level security;

Assim que o fizeres, a tua app deixa de ver dados até escreveres políticas. É o esperado: a porta está agora fechada e vais abrir só o necessário.

2. Escreve políticas explícitas por operação

O padrão mais comum é "cada utilizador vê e edita só as suas linhas", comparando auth.uid() com a coluna que guarda o dono. Para uma tabela customers com uma coluna user_id:

create policy "customers: ler próprias"
  on public.customers for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "customers: inserir próprias"
  on public.customers for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

create policy "customers: atualizar próprias"
  on public.customers for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "customers: apagar próprias"
  on public.customers for delete
  to authenticated
  using ((select auth.uid()) = user_id);

Três detalhes fazem a diferença: to authenticated deixa de fora os visitantes anónimos; with check impede que alguém insira ou edite linhas em nome de outra pessoa (sem ele, um utilizador podia criar dados com o user_id de outro); e envolver auth.uid() num select faz com que o Postgres o avalie uma vez por consulta em vez de uma vez por linha, o que em tabelas grandes multiplica o desempenho. Acrescenta um índice em user_id se não tiveres.

3. Nunca uses a chave service_role no frontend

Salta o RLS por completo: com ela, todas as políticas que acabaste de escrever deixam de existir. Essa chave vive só no servidor (Edge Functions, o teu próprio backend, um worker). Se alguma vez esteve em código do cliente, trata-a como comprometida e roda-a a partir do painel. O Supabase oferece agora também chaves novas (sb_publishable_… para o browser e sb_secret_… para o servidor) que tornam a separação mais óbvia.

4. Volta a testar

Repete o teste anónimo: o pedido sem sessão tem de devolver uma lista vazia ou um erro 401. Depois inicia sessão com duas contas diferentes e confirma que nenhuma vê os dados da outra. Se tens testes automáticos, este é o caso que merece um.

Erros comuns ao corrigir

  • Políticas com using (true). Os assistentes de IA sugerem-nas muitas vezes "para funcionar". Deixam a tabela tão aberta como não ter RLS nenhum.
  • Esquecer o with check. Protege as leituras mas permite escritas em nome de outra pessoa.
  • Tabelas de "catálogo" com dados pessoais. Uma tabela pública de perfis pensada para mostrar nomes acaba a expor emails ou telefones. Expõe só as colunas necessárias através de uma vista.
  • Vistas que saltam o RLS. No Postgres uma vista corre com as permissões de quem a criou. Criada a partir do painel, pode devolver linhas que as políticas da tabela base proibiriam. Cria as vistas com security_invoker = true (Postgres 15 ou superior) ou revê-as uma a uma.
  • Storage sem políticas. Os buckets têm as suas próprias regras. Um bucket público serve qualquer ficheiro a quem tiver o URL; documentos privados vão para buckets privados com políticas por utilizador.
  • Corrigir só a produção. Se manténs um projeto de desenvolvimento com dados copiados, está igualmente exposto.

Se já expuseste dados

Primeiro fecha a porta (os passos acima). Depois roda todas as chaves que estavam em tabelas legíveis: chaves de API de terceiros, tokens, webhooks. Revê os logs da API no painel do Supabase para estimar o que foi lido e desde quando. Se os dados incluem informação pessoal de utilizadores europeus, o RGPD obriga-te a avaliar se tens de notificar a autoridade de proteção de dados em 72 horas e, nos casos de alto risco, os próprios utilizadores. Documenta o que encontraste e o que fizeste: vão pedir-to se alguém vier bater à porta.

Um prompt para corrigir com a tua IA

Se trabalhas com o Cursor, o Claude ou o próprio chat do Lovable, este prompt costuma dar um bom resultado: "Lista todas as tabelas do esquema public do meu projeto Supabase. Para cada uma, ativa o Row-Level Security e escreve políticas separadas para select, insert, update e delete restritas ao papel authenticated, de modo que cada utilizador só aceda às linhas cuja coluna user_id seja igual a (select auth.uid()). Usa with check no insert e no update. Diz-me que tabelas não têm coluna de dono para eu decidir essas à mão. Nunca uses using (true)."

Como o Scanaris o verifica

O Scanaris analisa o teu site só em leitura e deteta a impressão digital deste padrão: endpoints do Supabase acessíveis a partir do browser, CORS permissivo, chaves e tokens expostos no teu JavaScript, e dados que a app serve sem sessão. Se lhe deres uma connection string só de leitura, audita também tabela a tabela se o RLS está ativo e se existem políticas. Cola o teu URL e diz-te, em segundos e ordenado por gravidade, o que está à vista de todos, antes que outra pessoa o encontre.

Perguntas frequentes

O Lovable já corrigiu isto?

O Lovable mudou a forma como gera as tabelas e acrescentou avisos, mas uma app publicada antes de 15 de abril de 2025 fica como a deixaste: as políticas não se acrescentam sozinhas. E em qualquer projeto, novo ou antigo, cada tabela que crias depois precisa das suas.

Ativar o RLS parte a minha app?

Ativá-lo sem políticas, sim: tudo é negado. É o comportamento correto. Escreve as políticas do passo 2 e a app volta a funcionar, mas só para quem deve.

Posso proteger a tabela a partir do frontend?

Não. O frontend é código que corre no computador do visitante e pode ser alterado à vontade. A única coisa que não se consegue contornar são as regras impostas pela base de dados.

E se a minha app não tem utilizadores, só uma tabela de conteúdo público?

Nesse caso uma política de leitura pública é legítima: for select to anon, authenticated using (true). O que não pode existir é uma política de escrita aberta nem colunas com dados pessoais nessa tabela.

Como está o teu site?

Passa-lhe o Scanaris e verifica em 30 segundos, grátis.

Analisar o meu site

Verificações relacionadas

Guias de segurança por stack