Pular para o conteúdo

Noções de segurança

O Supabase Row Level Security está ligado. A tabela continua pública.

Ligar o Supabase Row Level Security não protege uma tabela. Quem protege são as políticas, e a que consertou o seu app pode deixar qualquer um entrar.

Vlad Tkachenko7 min de leitura

Em resumo

  • No Supabase, a chave e as regras são duas coisas diferentes. Ligado sem regra não deixa ninguém passar; ligado com a regra errada não segura ninguém.
  • A regra que faz um app quebrado voltar a funcionar costuma ser a que permite qualquer pedido, de qualquer pessoa.
  • Leia a política, não o botão. Uma única palavra decide se um estranho consegue ler a sua tabela.

Você ligou o Row Level Security porque alguma coisa mandou: o advisor dentro do Supabase, um checklist, o resultado de um scan, alguém num Discord. A chave está verde no seu painel agora. E mesmo assim estão te dizendo que a sua tabela continua legível por estranhos.

Aqui está a parte que guia após guia conta errado: a chave não protege nada. Ela decide que as suas regras vão ser conferidas. As regras são outra coisa, você tem que escrever, e a regra mais rápida de escrever (a que faz um app quebrado voltar a funcionar) deixa todo mundo entrar.

O Row Level Security está ligado no Supabase. Como a tabela continua pública?

Porque ligar e decidir quem entra são dois passos diferentes, e só o primeiro é uma chave.

Pense na chave como colocar alguém na porta. Acioná-la não decide quem passa. Decide que agora alguém confere uma lista. Suas políticas são essa lista. Uma lista vazia barra todo mundo; uma lista que diz "todos" não barra ninguém. As duas coisas são Row Level Security ligado, e o seu painel mostra o mesmo verde nos dois casos.

É por isso que a configuração sozinha responde muito pouco, e por isso o nosso scanner nunca pergunta ao Supabase se ela está ativa. Ele pergunta para a tabela. Manda o pedido que um estranho mandaria, com a chave pública que viaja dentro do seu app, e vê se volta uma resposta. Ele pede uma contagem em vez das linhas, então descobre que a porta abriu sem ler nada do que está atrás.

A política que consertou o seu app provavelmente é o problema

No momento em que você liga o Row Level Security, o seu app para de mostrar dados, e o que você fez logo depois para ele voltar a funcionar é justamente o que merece uma olhada.

Essa sequência é completamente normal, e é onde a coisa desanda. Com a configuração ligada e nenhuma política escrita, o Postgres (o banco de dados em que o Supabase roda) recusa todos os pedidos por padrão, então suas listas voltam vazias e suas telas ficam em branco. Alguma coisa precisa entrar na lista. Se você pediu para o Cursor ou o Lovable consertar, ou colou o primeiro trecho que fez o erro sumir, o que você tem agora provavelmente é assim:

CREATE POLICY "Enable read access for all users"
  ON public.profiles
  FOR SELECT
  USING (true);

USING (true) é a condição que uma linha precisa cumprir antes de o banco entregar. Toda linha cumpre true. Tem um segundo detalhe ali dentro que é fácil passar batido: sem a cláusula TO, uma política vale para public, e public cobre visitantes logados e estranhos completos do mesmo jeito.

Aí o app volta a funcionar, nada dá erro, e a tabela está exatamente tão legível quanto estava antes de você começar.

Isso conserta a leitura, e só a leitura. Se o seu app também salva nessa tabela, a próxima coisa que você encontra é new row violates row-level security policy, que é a mesma configuração recusando uma escrita, e nenhuma policy de leitura resolve.

Os quatro estados em que uma tabela pode estar

Dois deles são seguros e dois não, e a chave não te diz quais. "Chave publicável" abaixo é a que tem lugar no seu app: sb_publishable_… nos projetos novos do Supabase, anon nos antigos.

Row Level SecurityA políticaSeu appUm estranho com a sua chave publicável do Supabase
Desligadotanto faz, nenhuma é lidaFuncionaLê todas as linhas
Ligadonenhuma escritaQuebradoNão lê nada
LigadoUSING (true)FuncionaLê todas as linhas
LigadoUSING (auth.uid() = user_id)FuncionaLê só as dele
A marca embaixo de cada coluna não segue a chave de cima. Duas delas estão ligadas e uma dessas entrega tudo.

As duas colunas do meio são o par que pega as pessoas. Mesma configuração, mesmo selo verde, resultados opostos, e a diferença é uma palavra dentro de uma regra que quase ninguém abre.

Se você preferir não ler cada política por conta própria, o nosso scan gratuito faz ao seu banco no ar a mesma pergunta que um estranho faria e diz quais tabelas responderam. Leva uns 20 segundos e não precisa de conta: escaneie seu app.

"Authenticated" não é a mesma coisa que "seu"

Uma política que permite authenticated permite todo mundo que tem conta, o que (se o seu app tem um cadastro aberto) é todo mundo disposto a preencher.

Essa é a versão mais sutil do mesmo erro, e ela sobrevive a muita revisão porque parece caprichada. TO authenticated USING (true) se lê como uma restrição, e é uma: exclui quem nunca se cadastrou. O que ela não faz é impedir um cliente seu de ler as linhas de outro cliente, que costuma ser o que você queria dizer com "privado".

A regra que faz isso nomeia de quem é a linha:

CREATE POLICY "Users read their own rows"
  ON public.orders
  FOR SELECT
  TO authenticated
  USING (auth.uid() = user_id);

auth.uid() é quem está perguntando. user_id é a coluna na linha que diz de quem ela é. A linha volta quando os dois batem, e fica onde está quando não batem.

Como conferir suas próprias tabelas em dois minutos

Abra o Supabase, vá em Authentication → Policies e leia a expressão dentro de cada política em vez do selo do lado de cada tabela.

Três coisas para procurar:

  • Uma política cuja condição é true. Decida, tabela por tabela, se você ficaria bem com aqueles dados numa página aberta. Para uma lista de artigos publicados, sim. Para qualquer coisa com uma pessoa dentro, não.
  • Uma política sem cláusula TO. Ela vale para todo mundo, logado ou não, mesmo quando o resto da regra parece bem específico.
  • Uma tabela com a configuração ligada, nenhuma política, e um app que mesmo assim funciona. Essa combinação significa que alguma coisa está chegando aos seus dados por outro caminho, e a explicação de sempre é uma chave secreta (sb_secret_…, ou service_role num projeto antigo), que ignora todas as políticas que você escreveu. Quais chaves de API são seguras no frontend mostra como distinguir essa da segura.

A outra conferida a que as pessoas recorrem é abrir o app numa janela anônima sem entrar na conta. Vale fazer, mas saiba o que ela prova. O seu app decide o que desenhar. O seu banco decide o que entregar. São decisões diferentes, e um estranho que pula as suas telas recebe a segunda.

As telas do seu app não são a cerca. A mesma tabela pode mostrar duas linhas pelo seu app e entregar as seis para um pedido que nunca o abriu.

Como isso parece enquanto está acontecendo com você

Não parece nada. É esse o formato desse problema, e é por isso que ele fica parado por meses.

Nenhum erro aparece no seu builder. Nada fica mais lento, nenhuma tela quebra, nenhum e-mail chega. Seu app se comporta exatamente como no dia em que você lançou, porque do lado dele nada mudou: ele sempre teve permissão de ler aquelas linhas. O que mudou é que todo mundo também tem, usando a chave que viaja no código que o seu site manda para cada visitante.

Quando aparece, aparece de lado. Uma cliente pergunta como alguém soube de uma coisa que só o seu app sabia. Uma lista de endereços que você nunca publicou surge em algum lugar. E se a regra generosa cobre escrita além de leitura, FOR ALL em vez de FOR SELECT, aí qualquer pessoa também pode alterar e apagar linhas, que é a versão que as pessoas descobrem como uma tabela de repente vazia.

Regras também escorregam. Uma migração, uma mudança de schema, mais um conserto de madrugada que precisava que os dados carregassem: qualquer um desses pode afrouxar uma política sem avisar, e por isso aqui vale uma segunda olhada mais para frente, e não só uma agora. Ficar de olho nisso é parte do que o Reeve Care faz, embora um lembrete na sua agenda cumpra o mesmo papel.

O que fazer esta semana

O que fazer

  • Abra Authentication → Policies no Supabase e leia a condição de cada política, tabela por tabela. O selo na tabela não é a resposta.
  • Para cada tabela com pessoas dentro (usuários, perfis, pedidos, mensagens), confira se a regra nomeia o dono da linha em vez de permitir todo mundo.
  • Troque qualquer USING (true) nessas tabelas por uma regra que compare auth.uid() com a coluna do dono, e confirme depois que o seu app ainda carrega.
  • Acrescente a cláusula TO que você queria. Uma política sem ela vale para estranhos igual vale para visitantes logados.
  • Se uma tabela tem a configuração ligada, nenhuma política, e o seu app mostra os dados dela mesmo assim, descubra o que está passando por cima antes de mexer em qualquer outra coisa.

Comece pela tabela que mais te constrangeria se fosse uma página aberta, e resolva essa hoje. O checklist de segurança de 10 minutos cobre isso junto com o resto do que vale olhar num app recém-lançado, e o guia de segurança do Supabase passa pelo que mais costuma ficar aberto.

Perguntas frequentes

Liguei o Row Level Security e meu app parou de mostrar dados. Quebrei alguma coisa?

Não, isso é a configuração funcionando. Com o Row Level Security ligado e nenhuma política escrita, o Postgres (o banco de dados por baixo do Supabase) recusa todos os pedidos por padrão, inclusive os do seu próprio app. A solução é adicionar uma política que descreva quem deve ver quais linhas. O erro a evitar é adicionar uma que permita todo mundo, porque é essa que faz o app voltar a funcionar e deixa a tabela aberta.

USING (true) alguma vez é a política certa?

Sim, para dados que são realmente públicos. Uma tabela de posts publicados, um catálogo de produtos, uma lista de lugares num mapa: eles existem para serem lidos por qualquer pessoa, e uma política que permite isso está certa. Ela deixa de estar certa no momento em que a tabela tem pessoas dentro. Pergunte a si mesmo se você ficaria confortável publicando o conteúdo dessa tabela numa página aberta, e deixe a resposta decidir a política.

O Row Level Security me protege se a minha chave secreta vazou?

Não. Uma chave secreta do Supabase (sb_secret_ nos projetos novos, service_role nos antigos) ignora o Row Level Security por completo, e é para isso que ela serve. Todas as políticas que você escreveu são puladas, em todas as tabelas. Se essa chave está no seu frontend, suas políticas não estão fazendo nada por você, e girar a chave é o primeiro trabalho, antes de qualquer mexida nas regras.

Preciso de Row Level Security se meu app já tem tela de login?

Sim. Sua tela de login controla o seu app, e o seu app não é o único caminho até o seu banco de dados. O Supabase dá a cada projeto um endereço web que responde a pedidos diretamente, e a chave para falar com ele está no código que o seu app envia para cada visitante. As políticas são a parte que vale por qualquer porta que o pedido tenha entrado.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Não sabe como está o seu próprio app?

Faça um scan gratuito e receba uma nota clara de A a F em cerca de 20 segundos. Sem conta e sem cartão.

Verificar meu app grátis

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.