Pular para o conteúdo

Noções de segurança

Ative Row Level Security em toda tabela do Supabase e comprove

Ativar Row Level Security no Supabase sem política fecha a tabela por completo. Uma política sem a chave ligada não faz nada. Aqui está o SQL, e o teste.

Vlad Tkachenko13 min de leitura
Uma lista de tabelas de banco de dados em um painel, cada uma com um interruptor de segurança ao lado, e a maioria ainda desligada.

Em resumo

  • Ativar Row Level Security no Supabase sem política fecha uma tabela por completo, e escrever uma política sem ativar a opção não faz absolutamente nada. Toda tabela precisa das duas coisas.
  • Três formatos de política cobrem quase tudo que um construtor de IA cria: linhas que pertencem a uma pessoa, linhas que qualquer um pode ler e linhas que o seu app grava em nome de um visitante.
  • Depois confira de fora do seu app e sem login, porque essa é a requisição que um desconhecido faz, e é a única que diz o que as suas políticas fazem em vez do que elas dizem.

Disseram para você ligar o Row Level Security, ou o seu construtor de IA mencionou isso de passagem enquanto consertava outra coisa. O seu projeto no Supabase tem entre quatro e quarenta tabelas, e você não sabe quais estão cobertas.

A instrução que você encontra em todo lugar é uma linha de SQL por tabela, e até aí ela está certa. Onde quase todo guia para é no passo seguinte. Uma política que existe não é uma política que funciona, e nada no seu dashboard vai mostrar a diferença. Então ativar o Row Level Security no Supabase são três trabalhos e não um: ligar em todas as tabelas, escrever as duas ou três políticas que remontam o seu app, e depois perguntar ao seu próprio banco o que um desconhecido perguntaria a ele.

O que ativar o Row Level Security faz de verdade?

Faz o Postgres consultar as suas regras antes de entregar uma linha. Com a opção desligada não há regras para consultar, então a resposta para qualquer requisição é tudo.

Imagine um bibliotecário que busca o que você pedir. Row Level Security é a instrução de ler um bilhete sobre você antes de encher o carrinho. O bilhete é a sua política, e ele pode dizer que essa pessoa pode levar os livros que ela mesma escreveu, ou pode dizer que qualquer um pode levar qualquer coisa. Com a instrução em vigor e nenhum bilhete escrito ainda, o bibliotecário volta com o carrinho vazio e não explica por quê.

É nessa última parte que as pessoas tropeçam, e ela decide como um teste vai ser mais adiante. Row Level Security filtra linhas. Ele não recusa requisições. Uma tabela que você não tem permissão de ler responde com uma lista vazia e um código de sucesso, não com um erro nem com uma tela de login. Ao seu app nunca é dito que ele foi recusado. Ele não recebe nada e desenha uma tela em branco.

Uma requisição, três configurações e o mesmo código de sucesso todas as vezes. O Row Level Security muda o que volta, nunca se a requisição deu certo.

Então ligar a opção num projeto que ainda não tem políticas não tranca os desconhecidos longe dos seus dados. Tranca todo mundo, o seu próprio app incluído, até você dizer quem pode ver o quê.

Como ativo o RLS em todas as tabelas do Supabase de uma vez?

Um laço, rodado uma vez no SQL Editor. Ele percorre cada tabela do seu esquema public e liga a opção onde ela estiver desligada.

Comece vendo onde você está. Isto lista as suas tabelas e diz se cada uma tem a opção agora:

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

Toda linha em que rowsecurity aparece como false é uma tabela entregando o seu conteúdo para qualquer um que tenha a chave publicável que vai junto com o seu app. Se essa lista for curta, faça uma de cada vez e veja o que quebra:

alter table public.orders enable row level security;

Se não for curta, isto dá conta de todas:

do $$
declare t record;
begin
  for t in
    select tablename from pg_tables where schemaname = 'public'
  loop
    execute format(
      'alter table public.%I enable row level security', t.tablename
    );
  end loop;
end $$;

Rode isso e o seu app fica em branco. O Supabase diz isso com todas as letras na própria documentação: os dados ficam inacessíveis pela API com uma chave publicável até que políticas sejam definidas. É a opção fazendo o trabalho dela, e é por isso que a próxima seção é a que vale deixar aberta antes de apertar executar.

As três políticas de que você realmente precisa

Quase toda tabela num app como o seu tem um de três formatos: linhas que pertencem a uma pessoa, linhas que qualquer um pode ler e linhas que o seu app grava em nome de um visitante. Aqui está cada uma, pronta para colar e renomear.

Linhas que pertencem a uma pessoa. Pedidos, mensagens, itens salvos, qualquer coisa com dono.

create policy "read own orders"
on public.orders for select
to authenticated
using ( (select auth.uid()) = user_id );

auth.uid() é o id de quem está logado naquela requisição. user_id é a coluna da sua tabela que registra o dono, então confira o nome antes de rodar: os construtores também escrevem owner_id, profile_id e created_by. A linha to authenticated significa que a política nem chega a ser considerada para um visitante deslogado, e é isso que mantém a tabela fechada ao público.

Linhas que qualquer um pode ler. Um catálogo de produtos, artigos publicados, um mapa de locais.

create policy "anyone may read products"
on public.products for select
to anon, authenticated
using ( true );

Escreva essa de propósito ou não escreva, porque é também a política à qual um construtor de IA recorre no momento em que você pede para ele consertar uma tela vazia. A pergunta a resolver antes: essa tabela poderia ser uma página do seu site, exatamente como está, sem tirar nada? Um não quer dizer que ela quer a política de propriedade acima.

Linhas que o seu app grava. Ler e escrever são permissões separadas no Postgres, então uma tabela em que o seu app salva precisa de uma segunda política, e esta confere a linha na entrada e não na saída.

create policy "insert own orders"
on public.orders for insert
to authenticated
with check ( (select auth.uid()) = user_id );

Qual cláusula vai onde é a parte que pega as pessoas, e a linha update carrega um requisito que não tem nada de óbvio:

OperaçãoCláusulaTambém precisa de
selectusing
insertwith check
updateusing e with checkuma política select na mesma tabela
deleteusing

A documentação do Supabase é explícita sobre a última: sem uma política de select correspondente, um update não funciona como você espera. E se a mensagem new row violates row-level security policy foi o que colocou você a procurar, a distância entre essas duas cláusulas é todo esse erro.

Por que todo exemplo escreve (select auth.uid()) em vez de auth.uid()

Porque os parênteses fazem o Postgres calcular o valor uma vez para a consulta inteira em vez de uma vez para cada linha que ele olha.

A versão embrulhada vira o que o Postgres chama de initPlan: rodada uma única vez e depois reaproveitada pelo resto do comando. Sem os parênteses a função é chamada de novo na linha um, na linha dois, na linha três, e assim por diante numa tabela que pode ter cem mil delas. O próprio Performance Advisor do Supabase reporta a versão sem parênteses sob uma regra chamada Auth RLS Initialization Plan, e ela é uma das entradas que as pessoas mais encontram lá dentro.

Os parênteses são toda a diferença. À esquerda a função roda uma vez por linha; à direita roda uma vez e a resposta é reaproveitada.

Uma ressalva, e ela é do próprio Supabase: isso funciona porque a resposta não muda de linha para linha. Uma função cujo resultado realmente depende da linha à frente dela não pode ser tirada do laço, então essa deixe sem parênteses.

Já que você está por aqui, adicione um índice na coluna que as suas políticas filtram. A política vira uma condição em toda leitura daquela tabela, então numa tabela com muitas linhas uma coluna sem índice aparece nos seus tempos de resposta:

create index orders_user_id_idx on public.orders (user_id);

Testar uma política sem fabricar uma conta falsa

O SQL Editor do Supabase consegue rodar uma consulta como se um visitante específico a tivesse enviado, e isso cobre o caso anônimo por inteiro.

O editor traz um controle para o papel com que uma consulta deve rodar. Coloque no papel anônimo e depois rode um select comum contra a tabela que você acabou de mudar. O que volta é o que um desconhecido deslogado recebe. Para um visitante logado, pegue o id de um usuário que você já tem em vez de criar um novo; qualquer linha da sua própria tabela serve para testar.

Se você prefere digitar a clicar, a mesma coisa em SQL:

begin;
set local role anon;
select * from public.orders;
rollback;

O begin e o rollback estão aí para a troca de papel durar aquele bloco e nada além, o que importa se você estiver passando por várias tabelas de uma sentada.

O que isso diz é o que as suas políticas fazem dentro do banco. O que isso não consegue dizer é o que o seu projeto entrega para a internet, porque a requisição que os seus visitantes realmente fazem não começa no SQL Editor. Ela começa num navegador, carrega uma chave publicável e chega pelo endereço público do seu projeto.

Como testo o RLS do Supabase de fora do meu app?

Mande a requisição que um desconhecido mandaria. Você precisa de dois valores e os dois já estão no código que o seu site entrega a cada visitante: a URL do seu projeto e a sua chave publicável.

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

Troque um nome de tabela por vez e leia o que volta. Uma lista vazia, [], quer dizer que a política segurou para um visitante deslogado. Uma linha quer dizer que qualquer um com uma chave que vai junto com o seu app consegue ler aquela tabela. Um erro que menciona a própria chave quer dizer que você copiou o valor errado, e vale descartar isso antes de concluir qualquer coisa.

O teste no editor nunca sai do prédio. A requisição de fora chega pelo mesmo endereço público que os seus visitantes usam, levando a mesma chave que o seu app entrega.

Num projeto Supabase mais novo essa chave começa com sb_publishable_, e num mais antigo é a chave anon. As duas são seguras de usar assim e seguras de ficar no seu app, é para isso que servem; quais chaves de API pertencem a um frontend cobre o par que não pertence, e onde encontrá-las no dashboard são os quatro valores daquela página de configurações.

Esse é o teste que corresponde à realidade, e rodá-lo em escala é como sabemos o quanto essa falha é comum. Entre 12 e 14 de agosto de 2026 rodamos nove verificações externas em 30.998 apps no ar publicados pelo Lovable, Base44, Replit, v0 e Bolt. Dos 3.680 apps com Supabase em que a verificação conseguiu terminar, 2.096 responderam a uma requisição anônima com linhas de pelo menos uma tabela. Isso dá 57%, e é uma fatia dos apps que nos deram uma resposta clara, não de tudo o que escaneamos. O conjunto de dados completo está publicado, e de que esses 57% são uma fatia percorre a contagem.

Fazer isso na mão está bom com quatro tabelas e cansa com quarenta. O nosso scan gratuito manda essa requisição por você, descobre quais tabelas existem sem que você as nomeie e diz quais responderam, ao lado de outras oito verificações que ele faz de fora. Ele lê o seu site no ar do mesmo jeito que qualquer visitante pode, leva cerca de 20 segundos e não precisa de conta: escaneie o seu app.

Antes de reescrever políticas numa tabela no ar

Tire primeiro uma cópia do seu banco. Você está prestes a mudar permissões em todas as tabelas que tem, com as mesmas ferramentas que produziram o problema.

Vale separar dois riscos diferentes, porque só um deles tem a ver com o trabalho de hoje. Se uma tabela era legível e gravável por qualquer um, apertar agora não muda nada do que já aconteceu, e essa versão costuma aparecer como uma mensagem de suporte sobre dados que mudaram sozinhos. O outro risco é a própria migração. Uma política apagada e recriada um pouco torta é uma terça-feira comum, e o caminho de volta é uma cópia de como as coisas estavam uma hora atrás.

Num plano pago do Supabase a cópia de ontem à noite está esperando no console. No plano gratuito não há nada em que se apoiar, porque o Supabase não faz nenhum backup automático no plano gratuito. Se esse é o seu caso, tire um antes de começar.

O Reeve Care é a versão disso de que você não precisa lembrar. O seu banco Supabase é copiado num calendário, guardado fora da sua conta Supabase, criptografado, e lido de volta para confirmar que restaura antes que a data no seu painel se mexa. Os arquivos enviados pelos seus usuários viajam junto assim que você conecta uma chave de Storage, e essa chave é pedida à parte porque o Supabase não emite chave só de leitura para arquivos: a que copia os seus uploads também consegue gravar, e a que copia o seu banco não. Conectá-la é opcional e o seu banco é salvo de qualquer jeito. Os backups são só do Supabase, então se os seus dados moram em outro lugar a gente diz isso em vez de vender uma assinatura que vigia uma caixa vazia.

Restaurar é a parte que importa num dia como esse. O Care copia o estado atual do seu banco antes de reproduzir a versão que você escolheu, então apertar o botão tem um desfazer próprio.

A outra metade é a verificação que você acabou de fazer na mão. Uma política que afrouxa numa migração posterior não é coisa que alguém encontre olhando, então o Care roda de novo a mesma requisição anônima num calendário e avisa quando uma tabela começa a responder tendo ficado calada na semana passada. Monitoramento de uptime e um relatório mensal estão na mesma assinatura. Nada disso escreve as suas políticas por você, e nenhum backup fecha uma tabela aberta. O que muda é o quanto uma migração malfeita custa a você. O que o Reeve salva no Supabase, com que frequência, e o que uma restauração faz percorre o ciclo inteiro, e os planos estão na página de preços.

A ordem em que trabalhar

O que fazer

  • Liste as suas tabelas com select tablename, rowsecurity from pg_tables where schemaname = 'public' e veja quantas estão abertas antes de mudar qualquer coisa.
  • Tire um backup e depois ative o Row Level Security em toda tabela do esquema public. Espere o seu app ficar em branco; é a opção funcionando.
  • Dê a cada tabela uma das três políticas. A de propriedade é o caso padrão; USING (true) é uma decisão que você toma tabela por tabela, não um jeito de recuperar as telas.
  • Escreva (select auth.uid()) em vez de auth.uid(), e indexe a coluna que as suas políticas filtram.
  • Teste cada tabela de fora e sem login. Uma lista vazia é aprovação. Uma linha é uma tabela que qualquer um consegue ler, diga o que disser o seu dashboard.

Comece pela tabela que mais constrangeria você como página pública e siga para baixo a partir daí. A checklist de segurança de 10 minutos cobre isso junto com o resto do que vale confirmar num app recém-lançado, e o guia de segurança do Supabase passa pelo que mais costuma ficar aberto.

Perguntas frequentes

O que acontece se eu ativar o RLS e não escrever nenhuma política?

A tabela para de responder, inclusive para o seu próprio app. Com a opção ligada, o Postgres consulta as suas políticas antes de entregar uma linha, e sem políticas não existe nada que possa dizer sim, então as requisições voltam vazias. O Supabase documenta isso direto: os dados ficam inacessíveis pela API com uma chave publicável até que políticas sejam definidas. Nada é apagado e nada quebra. Escreva uma política para as linhas que o seu app deve mostrar e as telas voltam.

O Row Level Security deixa as minhas consultas mais lentas?

Pode deixar, e os dois ajustes são pequenos. Escreva `(select auth.uid())` na condição da política, com os parênteses, para o Postgres calcular o valor uma vez para a consulta inteira e depois reaproveitá-lo em cada linha. O Supabase aponta a versão sem parênteses no próprio Performance Advisor, sob uma regra chamada Auth RLS Initialization Plan. Depois adicione um índice na coluna que a política filtra, normalmente `user_id`. Numa tabela com alguns milhares de linhas é pouco provável que você note diferença. Numa tabela grande, as duas coisas importam.

Preciso de RLS se a minha tabela não tem dados pessoais?

Você ainda quer a opção ligada, e a política pode ser a permissiva. Uma tabela com Row Level Security desligado é legível por qualquer um que tenha a chave publicável que vai junto com o seu app, o que é aceitável para um catálogo de produtos e bem menos aceitável para qualquer coisa que você não publicaria como página. Ligar e escrever uma política de leitura com `USING (true)` dá esse mesmo acesso público de propósito, e faz a tabela ser lida como decidida em vez de esquecida na próxima vez que alguém passar pela lista.

Por que minha inscrição no realtime parou de funcionar depois que ativei o RLS?

Porque o Realtime consulta as mesmas políticas antes de mandar uma mudança para quem está inscrito. Uma tabela com Row Level Security ligado e sem política de select para aquele visitante não entrega linhas a uma consulta nem mudanças a uma inscrição, exatamente pelo mesmo motivo. Adicione a política de select de que aquela inscrição precisa e o fluxo volta. Se você usa Realtime broadcast ou presence em vez de mudanças no banco, esses são autorizados à parte, por políticas escritas na tabela `realtime.messages`.

Como testo uma política sem criar um usuário falso?

De dois jeitos, e nenhum deles precisa de conta nova. O SQL Editor do Supabase consegue rodar uma consulta como se um papel específico a tivesse enviado, e isso cobre o caso anônimo por inteiro: o que volta é o que um desconhecido recebe. Para o caso logado você precisa de um id de usuário para servir de dublê, e qualquer linha que já esteja na sua tabela serve. O segundo jeito é uma requisição de fora levando a sua chave publicável, que testa o caminho todo e não só a política.

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

Todos os artigos

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.