Supabase com RLS desligada: qualquer pessoa lê o banco inteiro

Você subiu o app. O login funciona, cada usuário vê os dados dele, a tela está certa. E está mesmo — no navegador.

O problema é que o navegador não é o único jeito de falar com o seu banco.

A chave que aparece no F12 não é um vazamento

Abra o seu app, aperte F12, procure por supabaseKey. Ela está lá, no meio do JavaScript. Isso assusta na primeira vez, mas é assim que tem que ser: a chave anônima (anon key) do Supabase é publicável por design, do mesmo jeito que a chave publicável do Stripe. Ela não é uma senha. Ela só diz de qual projeto a requisição veio.

Quem decide o que essa chave pode ler não é ela — é a Row Level Security.

E a RLS vem desligada quando você cria uma tabela.

O que "desligada" significa na prática

Com a RLS desligada, a tabela responde a qualquer requisição que traga a chave anônima. Não interessa quem está logado, nem se alguém está logado.

O seu app filtra por usuário porque o seu código pede assim:

const { data } = await supabase
  .from('mensagens')
  .select('*')
  .eq('user_id', user.id)   // <- o filtro está aqui, no cliente

Esse .eq() é uma conveniência da sua tela. Não é uma regra do banco. Quem falar com o Supabase direto simplesmente não escreve essa linha:

curl "https://SEU-PROJETO.supabase.co/rest/v1/mensagens?select=*" \
  -H "apikey: SUA_ANON_KEY" \
  -H "Authorization: Bearer SUA_ANON_KEY"

Se voltar a lista de mensagens de todo mundo, a RLS está desligada nessa tabela. E as duas coisas que esse comando precisa — a URL do projeto e a chave anônima — estão as duas no JavaScript da sua página.

Por que isso é tão comum em app feito com IA

Não é descuido de quem usa Lovable, Bolt, Replit, Base44, Create.xyz ou qualquer outra plataforma que monta o app inteiro com Supabase de backend — e também não é exclusivo de quem usa essas plataformas prontas: o mesmo acontece com Cursor, Windsurf ou qualquer assistente de código gerando o schema direto. É a ordem em que as coisas acontecem.

O assistente gera o schema, gera as telas, e tudo funciona na primeira execução. RLS ligada quebraria essa primeira execução: com RLS e sem política nenhuma, a tabela responde vazio, o app parece bugado e a pessoa pede para o assistente "consertar". O caminho de menor atrito é a tabela aberta.

Some a isso o fato de que o app parece certo — cada usuário vê só o que é dele, na tela — e você tem um problema que não se manifesta em nenhum teste manual. Só aparece quando alguém pergunta ao banco sem passar pela sua tela.

Como verificar as suas tabelas

No painel: Table Editor → cada tabela mostra um aviso de "RLS disabled" quando está aberta. É a checagem mais rápida, e o lugar onde você vê todas de uma vez.

Por fora: o curl acima, uma tabela por vez. É mais chato, mas é o que prova o comportamento real — inclusive nos casos em que a RLS está ligada e a política é permissiva demais, que o aviso do painel não pega.

Ligar a RLS não é o fim

Este é o erro seguinte, e ele é mais silencioso.

Com a RLS ligada e nenhuma política, a tabela nega tudo. O app quebra. A saída que aparece em muito tutorial é esta:

-- não faça isso
create policy "acesso" on mensagens
  for all using (true);

Isso liga a RLS e devolve exatamente o comportamento de antes: using (true) é verdadeiro para qualquer requisição, autenticada ou não. O painel para de reclamar e o banco continua aberto.

A política precisa amarrar a linha ao dono dela:

alter table mensagens enable row level security;

create policy "dono lê as próprias mensagens" on mensagens
  for select using (auth.uid() = user_id);

create policy "dono escreve as próprias mensagens" on mensagens
  for insert with check (auth.uid() = user_id);

Duas coisas que se esquece aqui:

  • for select não cobre escrita. Uma política de leitura bem feita convive com um insert liberado, e aí qualquer um grava na sua tabela. Cada operação precisa da sua.
  • using filtra o que já existe; with check valida o que está entrando. Sem o with check no insert, dá para gravar uma linha com o user_id de outra pessoa.

A chave que realmente não pode aparecer

Existe uma segunda chave no painel do Supabase: a service_role. Essa ignora a RLS inteira, por design — ela existe para o seu backend.

Se ela estiver no código do front, nada do que está acima importa: as políticas não se aplicam a quem a usa. Procure por ela no seu bundle. Se encontrar, rotacione a chave antes de qualquer outra coisa — remover do código não invalida a que já foi publicada.

O resumo

  • A anon key no F12 é normal. A service_role no F12 é incidente.
  • RLS vem desligada; o app funcionar não é evidência de que ela está ligada.
  • using (true) cala o aviso sem resolver o problema.
  • Cada operação (select, insert, update, delete) precisa da sua política.
← Todos os posts