Em resumo
Uma app Lovable é tão segura como a sua base de dados Supabase, e “o RLS está ligado” prova muito pouco. Teste com dois utilizadores de duas empresas, mantenha as funções fora dos metadados do utilizador, mantenha a chave secreta fora do browser e verifique à parte os grants, as vistas, o storage, as funções e os limites de pedidos, porque cada um contorna as suas políticas à sua maneira. Abaixo está a lista que executo, o prompt que pode colar no Lovable ou no Claude e oito verificações que acrescentei depois de a app de um cliente me mostrar o que as primeiras doze não apanham.
Sou o Denys, engenheiro full-stack e de IA. Este mês corrigi a app de um cliente feita com Lovable. Tudo corria em Supabase, como de costume, e havia tantas correções de segurança que a app parecia feita delas. Esta é a lista que passo agora em todos os projetos Lovable ou Supabase antes de dizer que os dados dos clientes estão protegidos.
O Lovable é seguro? As falhas de segurança até agora
A plataforma constrói e aloja a sua app, mas a app ser segura depende sobretudo da base de dados Supabase que está por trás, porque a app lê e escreve nessa base de dados diretamente a partir do browser. O registo público mostra os dois tipos de problema:
| Quando | O que aconteceu | Fonte |
|---|---|---|
| Março de 2025 | CVE-2025-48757: uma análise de 1645 apps de demonstração do Lovable encontrou 303 endpoints em 170 delas que serviam dados a qualquer pessoa com a chave pública, porque o RLS faltava ou era demasiado permissivo | Divulgação de Matt Palmer, maio de 2025 |
| Fevereiro de 2026 | Uma app alojada no Lovable expôs 18 697 registos de utilizadores; uma verificação de acesso nas suas funções deixava entrar visitantes anónimos e bloqueava os utilizadores com sessão iniciada. O Lovable disse que parte do código não foi gerada pelo Lovable | The Register, 27 de fevereiro de 2026 |
| 3 de fevereiro – 20 de abril de 2026 | Uma regressão da plataforma permitiu que outros utilizadores do Lovable lessem o histórico de chat e o código-fonte de projetos públicos | Publicação do Lovable sobre o incidente, 22 de abril de 2026 |
Os dois primeiros são falhas ao nível da app, do tipo que as verificações abaixo apanham. A terceira foi do próprio Lovable e o Lovable corrigiu-a; a sua parte é manter as chaves fora dos chats e do código (verificação 3) e rodar as que lá estiveram. O Lovable executa uma análise de segurança antes de publicar, mas agir sobre ela cabe a si, e quando o CVE foi publicado essa análise verificava se existia uma política, não se estava correta. Por isso, “o Lovable é seguro?” resume-se a saber se a sua base de dados passa as verificações abaixo.
Porque é que “o RLS está ligado” não chega numa app Lovable?
Uma app Lovable fala com a base de dados diretamente a partir do browser. Não há um backend seu pelo meio: a página chama a Data API do Supabase com uma chave pública e o Postgres decide o que devolve (que schemas essa API serve é uma definição à parte, tratada em Supabase exposed schemas). Por isso, todas as regras que protegem os dados dos clientes têm de viver na base de dados, e a base de dados tem várias portas.
O row level security é uma delas. Os grants decidem se uma função pode sequer tocar numa tabela, as políticas decidem que linhas, e três coisas ignoram totalmente as políticas: a chave secreta, as vistas criadas sem security_invoker e as funções SECURITY DEFINER. Isto não é hipotético. O próprio guia de segurança do Lovable pede-lhe que reveja as políticas de RLS que ele configura, e o CVE-2025-48757, acima, é o que acontece quando ninguém o faz.
O prompt: cole-o no Lovable ou no Claude
Pode colá-lo tal como está. Depois verifique o resultado por si: a IA adora dizer “feito” quando não está, e um prompt que faz um erro desaparecer pode consegui-lo alargando o acesso.
Audita este projeto em segurança, corrige o que encontrares e diz-me o que mudaste:
1. Testa com dois utilizadores de duas empresas. O utilizador A tenta ler, editar e apagar registos de B por id, pela API, pela pesquisa e pelas exportações. Tudo tem de falhar.
2. RLS em todas as tabelas, as políticas verificam o dono da linha ou a empresa. "Sessão iniciada" não chega.
3. A service role key nunca no frontend. As edge functions que a usam verificam o utilizador e filtram por empresa elas próprias, a chave ignora o RLS.
4. Funções, plano e id da empresa nunca vêm dos metadados do utilizador, os utilizadores podem editá-los. Guarda-os numa tabela em que só o servidor escreve.
5. Quando um pedido envia dois ids (projeto + tarefa), verifica que a tarefa pertence a esse projeto.
6. Revoga todos os privilégios de tabela a anon e authenticated, devolve só os que as políticas precisam. O RLS não impede o TRUNCATE. Cada migração que cria uma tabela inclui o seu GRANT: a partir de 30 de outubro de 2026 o Supabase deixa de conceder grants às tabelas novas automaticamente, e a app recebe o erro 42501.
7. Todas as vistas com security_invoker = true, caso contrário ignoram o RLS.
8. Buckets de storage: os utilizadores leem e escrevem só os seus próprios ficheiros, sem buckets públicos para dados privados.
9. Funções SECURITY DEFINER: NULL tem de significar negar, não permitir.
10. Limites de pedidos no login, no registo, na recuperação de palavra-passe e no OTP, guardados na base de dados, não em memória.
11. Valida toda a entrada no servidor.
12. Executa o security advisor do Supabase e corrige todos os avisos.
As 12 verificações e como provar cada uma
1. Dois utilizadores em duas empresas
Crie o utilizador A na empresa 1 e o utilizador B na empresa 2, cada um com alguns registos. Entre como A e tente ler, editar e apagar os registos de B por id, pela API, pela pesquisa e por todas as exportações. Cada tentativa tem de devolver nada ou um erro. É o único teste que apanha a maior parte do que se segue, porque testa o que o atacante faz, não o que você escreveu.
Acrescente um terceiro utilizador que pertença às duas empresas. Uma política do género “quem chama é membro da empresa da linha” deixa esse utilizador ler as linhas da empresa 2 enquanto trabalha na empresa 1. Noutra app Supabase, a minha sonda com um utilizador assim devolveu o documento da outra empresa até a consulta passar a filtrar também pela empresa do pedido. Escrevi isso em um utilizador em duas orgs que o RLS não isolou.
2. RLS em todas as tabelas, limitado ao dono ou à empresa
auth.uid() is not null significa “qualquer utilizador com sessão iniciada”, o que, numa página de registo pública, significa qualquer pessoa. Cada política compara o dono ou a empresa da linha com quem chama. Liste as tabelas que ainda têm o RLS desligado:
select c.relname
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;
3. A chave secreta nunca no frontend
A service role key, e a mais recente chave sb_secret_, contornam todas as políticas. Procure as duas no JavaScript compilado e rode qualquer chave que alguma vez tenha sido publicada. As edge functions que a usam têm de verificar quem é o utilizador e filtrar por empresa elas próprias, porque com essa chave mais nada o fará.
4. Funções, plano e id da empresa nunca vêm dos metadados do utilizador
raw_user_meta_data é o que o cliente enviou ao signUp, e os utilizadores podem alterá-lo depois. Noutra app, o trigger de novo utilizador lia dali a função do convite, por isso qualquer pessoa que se registasse podia passar a função de dono e ficar Owner. Guarde funções, planos e ids de empresa numa tabela em que só o servidor escreve, ou em app_metadata. A história completa: uma função tirada dos metadados do registo.
5. Dois ids num pedido: verifique que pertencem um ao outro
Quando um pedido leva um id de projeto e um id de tarefa, autorizar o projeto não chega. Um utilizador com direitos no projeto A envia A mais o id de uma secção do projeto B, e a linha vai parar a B. O RLS não apanha isto, porque o insert grava o id de projeto do próprio atacante, que é exatamente o que a política verifica. Verifique que o filho pertence ao pai antes de escrever.
6. Grants: revogar e devolver só o necessário
Grants e políticas são duas camadas separadas. Acrescentar políticas não retira um grant, e é por isso que o guia de RLS do Supabase manda revogar e voltar a conceder de forma explícita. Noutro projeto, a imagem supabase/postgres 17.6.1 tinha dado a anon e authenticated TRUNCATE, REFERENCES, TRIGGER e MAINTAIN em todas as tabelas de public, e nenhuma política se aplica ao TRUNCATE. A Data API não tem um endpoint de TRUNCATE, por isso não é um apagamento com um só pedido, mas nenhuma função de cliente precisa dele, e qualquer função que execute SQL com essa função herda-o.
revoke all on public.orders from anon, authenticated;
grant select, insert, update on public.orders to authenticated;
select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
order by table_name;
Uma data muda esta verificação. O Supabase deixa de conceder grants às tabelas novas automaticamente: nos projetos novos desde 30 de maio de 2026, e em todos os projetos existentes a partir de 30 de outubro de 2026, incluindo os que o Lovable criou. Depois disso, uma tabela que o Lovable ou uma migração crie sem um GRANT explícito fica invisível para a app: o pedido falha com 42501 e a funcionalidade que usa a tabela deixa de funcionar. Por isso, cada migração que cria uma tabela passa a levar os seus grants, a par de enable row level security e das políticas.
Duas coisas mantêm-se. As tabelas existentes conservam os grants que têm, por isso revogar o que não precisa continua a ser trabalho seu. E a alteração retira apenas select, insert, update e delete: no projeto Lovable, com os novos valores por omissão, as tabelas novas continuavam a vir com TRUNCATE, REFERENCES, TRIGGER e MAINTAIN para anon e authenticated. Revogar tudo primeiro e conceder depois continua a ser a ordem segura.
7. Todas as vistas com security_invoker = true
Uma vista corre com os direitos do seu dono, e no Supabase o dono costuma ser o postgres, por isso uma vista sobre uma tabela protegida entrega todas as linhas que as políticas retêm. No Postgres 15 e posteriores:
alter view public.customer_summary set (security_invoker = true);
8. Storage: só os ficheiros próprios, nenhum bucket público para dados privados
Um bucket público serve qualquer ficheiro a quem tiver o URL. Os ficheiros privados vão para um bucket privado com políticas em storage.objects que verifiquem o dono ou a empresa, normalmente a partir da primeira pasta do caminho. Depois de mover um projeto, conte as políticas de storage ativas: numa app, todos os uploads falharam depois de uma mudança porque a migração das políticas nunca correu e a tabela tinha o RLS ligado com zero políticas.
9. Funções SECURITY DEFINER: NULL significa negar
Em PL/pgSQL, if not (has_permission or assignee_id = auth.uid()) não lança exceção quando assignee_id é null: a condição inteira é null e o if salta-a. A mesma expressão é segura dentro de uma política, que trata null como negar. Numa app isso deixou uma função com todas as permissões desligadas agir sobre as 3683 tarefas, porque nenhuma tinha responsável. Envolva a condição em coalesce(…, false). Detalhes: um NULL num IF NOT de PL/pgSQL saltou a regra.
10. Limites de pedidos guardados na base de dados
Limite o login, o registo, a recuperação de palavra-passe e o OTP, e guarde o contador no Postgres. Na app Lovable, os limites viviam numa variável dentro da edge function. Testei o projeto alojado a 17 de outubro de 2026: cada pedido corria num isolate novo, por isso o contador em memória valia sempre 1 e o limite nunca podia disparar. O supabase functions serve local reutiliza o isolate, e foi por isso que o mesmo código passou em todos os testes locais.
A chave importa tanto como o armazenamento. Na plataforma alojada, o último salto de X-Forwarded-For é o próprio proxy do Supabase, por isso usá-lo como chave põe todos os chamadores no mesmo balde, e o x-client-ip é definido pelo cliente. Na minha sonda, o cf-connecting-ip trazia o cliente real. A correção foi uma função SECURITY DEFINER claim_rate_limit(bucket, key, max, window) que só o service role pode executar, serializada com um advisory lock, que guarda um hash do IP ou do id do utilizador em vez do valor em bruto.
11. Valide toda a entrada no servidor
Seja o que for que o formulário verifique, a API aceita tudo o que o cliente enviar. Comprimentos, formatos, enums e propriedade são verificados na base de dados (constraints, triggers) ou na edge function, nunca só no React.
12. O Security Advisor do Supabase, todos os avisos
Abra Advisors, depois Security Advisor, e corrija todas as deteções. Assinala tabelas com o RLS desligado, vistas que contornam o RLS, políticas que leem metadados do utilizador, políticas permissivas using (true) e funções com um search_path mutável. Não vê as verificações 1, 5, 9 nem 10, porque essas são sobre lógica, não sobre configuração.
O que me ensinou a app Lovable que as 12 não apanham?
Isto saiu da app do cliente e das suas revisões. Nada disto aparece num scanner.
- Campos que o cliente não deve definir. O cliente enviou
status: 'approved'por conta própria, e isso saltou a moderação. Uma política UPDATE cujowith checké mais permissivo do que o seuusingtambém deixa um utilizador mover uma linha para outro dono. Uma política não consegue calcular um valor, por isso um triggerBEFORE INSERT OR UPDATErecalculastatus, função, preço e empresa a partir dos dados do próprio servidor e ignora o que o cliente enviou. - Todos os caminhos de escrita, não só o insert principal. Depois de o insert principal ser corrigido, o mesmo desvio continuava nos anexos, nos comentários, na restauração de um item apagado com soft delete e nos contadores. Liste todos os escritores de uma coluna protegida antes de corrigir o primeiro.
- Listas de permitidos, não de proibidos. Os registos falsificados custaram cinco rondas de revisão, porque cada correção bloqueava uma combinação e deixava uma irmã aberta. Um trigger que permite só as combinações listadas de tipo e estado fechou a classe inteira de uma vez.
- As funções também são uma API. Toda a função em
publicpode ser chamada por/rpcpor qualquer função com EXECUTE, e o RLS não se aplica às funções. Dê a cada funçãoSECURITY DEFINERset search_path = '', revogue o EXECUTE apublic,anoneauthenticated, e conceda-o apenas à função que dele precisa. - Revogar o EXECUTE pode partir páginas públicas. O Postgres verifica o EXECUTE em todas as funções usadas por todas as políticas que se aplicam à função, mesmo num ramo que nunca correria. Quando o
anonperdeu o EXECUTE emis_admin(), todas as leituras anónimas falharam com 42501. As políticas só de admin vãoto authenticated, e as leituras públicas têm a sua própria política sem o ramo de admin. - Registo com a confirmação de email ligada. Com a confirmação ligada, o
signUpnão devolve sessão, por isso qualquer escrita de perfil logo a seguir corre comoanone não escreve nada, em silêncio. Os projetos Lovable costumam ter a confirmação automática ligada, o que esconde o bug. Passe os campos emsignUp({ options: { data } }), deixe o trigger de novo utilizador validá-los e escrevê-los, e trate esses metadados como não fiáveis (verificação 4). - Testes que passam pela razão errada. A stack local e o projeto alojado tinham privilégios por omissão diferentes, por isso “o anon não consegue inserir” passava em local porque faltava o grant, não porque o RLS funcionasse. Um teste negativo só significa algo quando o grant positivo existe. Compare o
pg_default_aclem local e em produção. - A base de dados ativa, não os ficheiros de migração. Depois de uma migração ou de uma mudança de projeto, compare o que está realmente ativo:
pg_policies,pg_get_functiondefde cada guarda e os grants acima. Noutra app, uma migração nova copiou o corpo de uma função antiga e desfez, em silêncio, uma correção posterior.
Uma app Lovable cumpre o RGPD?
O Lovable não torna uma app conforme ou não conforme; quem decide é o que a app faz com os dados pessoais, e uma revisão de segurança não responde a isso. Estas são as seis falhas que verifico numa app Lovable Cloud, cada uma com a correção:
- Liste cada subcontratante. O Lovable Cloud corre a base de dados, a autenticação, o armazenamento e as funções no Supabase, o Lovable aloja o site e pode enviar os emails de início de sessão, e o Lovable AI Gateway passa texto a um fornecedor de modelos. Acrescente cada serviço de email, IA ou scraping que as suas funções chamam. Cada um que recebe dados pessoais precisa de um contrato de subcontratação (DPA), e um fora da UE precisa ainda de um mecanismo de transferência. A região de alojamento não aparece no código, por isso pergunte por ela.
- Apague um utilizador que tenha conteúdo. As chaves estrangeiras do que esse utilizador escreveu podem bloquear a eliminação, e então um pedido de apagamento não pode ser concluído. Ponha essas chaves em cascade ou set null, acrescente um trigger de eliminação que remova também os registos de email e os tokens de sessão do utilizador, e mantenha um teste de base de dados que apague um utilizador assim.
- Defina um prazo de conservação. Registos de email, contagem de visualizações, filas de emails falhados e linhas com eliminação lógica costumam guardar dados pessoais sem data de fim. Uma tarefa agendada que apague de vez essas linhas após um prazo fixo, com os seus ficheiros, resolve a maior parte.
- Rastreio de emails só com consentimento. Registar aberturas e cliques por destinatário exige consentimento pelas regras de ePrivacy: um indicador de consentimento, desligado por omissão, com a data e hora definidas pelo servidor, não pelo cliente.
- Tire os endereços dos logs das funções. Os logs das edge functions não são limpos, por isso um
console.logcom o endereço de um destinatário deixa-o lá. Registe ids e contagens. - Ligue a confirmação de email. Com a autoconfirmação, qualquer pessoa pode registar-se com um endereço que não é seu e pôr na app o endereço de uma pessoa real. O ponto 6 da lista acima trata do lado do registo.
Para verificar a sua app: procure chaves de API nas funções para listar os serviços, apague um utilizador de teste com conteúdo, procure @ nos logs das funções e peça a cada fornecedor o contrato de subcontratação. Isto não é aconselhamento jurídico.
Como evitar que volte?
Todas as verificações acima são uma consulta ou um teste, por isso guarde-as no repositório e execute-as antes de cada deploy. Os testes pgTAP do Supabase cobrem grants, políticas e funções; o teste das duas empresas pertence ao lado deles. Se a sua app tem funções e um ecrã de permissões, também não chega um teste por política: eu provo cada caixa de permissão contra uma cópia de produção, como descrevo em como testar o Supabase RLS, onde 30 de 87 caixas se revelaram inúteis. A Auditoria de segurança Supabase gratuita executa 46 regras do OWASP Top 10:2025 sobre as funções, grants, políticas e definições de auth de um projeto, por isso use-a para a primeira passagem; o teste das duas empresas e tudo o que diz respeito à sua interface continuam manuais.
O que esta lista não cobre: pagamentos e webhooks, segredos no histórico do Git, cabeçalhos de segurança no host, cópias de segurança. Importam, mas são outra lista.
Se herdou uma app Lovable ou Supabase e não tem a certeza de quais destas verificações ela passa, esta auditoria é onde começa um resgate de projeto. O caso do registo de horas para a empresa de auditoria mostra como são as verificações de funções numa app em produção.
Encontrou uma falha que esta lista não cobre?
Gostava de a ouvir. Troco impressões com gosto sobre como proteger apps Lovable e Supabase.
Fale comigo