Pular para o conteúdo

Noções de segurança

New row violates row-level security policy no Supabase. E agora?

"New row violates row-level security policy" significa que o Supabase recusou uma escrita. O conserto rápido reabre a tabela para qualquer um.

Vlad Tkachenko8 min de leitura
Uma pilha de linhas dentro de uma tabela, com mais uma linha esperando fora, desenhada só em contorno porque não deixaram ela entrar.

Em resumo

  • New row violates row-level security policy significa que o seu banco conferiu as regras daquela tabela e não achou nenhuma que permitisse a linha que você estava salvando. Ele recusou a escrita e deixou a tabela como estava.
  • Uma mensagem só cobre três situações diferentes: nenhuma regra, uma regra que fala apenas de leitura, ou uma regra de escrita que a sua linha não satisfaz.
  • A resposta que lidera qualquer busca faz o erro parar permitindo qualquer escrita de qualquer um. A regra que você realmente quer custa uns dois minutos a mais.

Alguma coisa mandou você ligar o Row Level Security. O advisor dentro do seu painel do Supabase, o resultado de um scan, um checklist, alguém num Discord. Você ligou. Agora o seu app não consegue salvar absolutamente nada. Toda tentativa volta dizendo que a new row violates row-level security policy:

new row violates row-level security policy for table "profiles"

Aqui está a parte que guia após guia conta errado: a resposta que faz esse erro sumir em dez segundos desfaz exatamente o que você acabou de ligar. É o primeiro resultado que você vai achar, funciona na hora, e deixa a tabela exatamente onde ela estava antes de alguém mandar você consertar.

O que significa "new row violates row-level security policy"

Pediram ao seu banco para salvar uma linha, ele conferiu as regras daquela tabela, não achou nenhuma que deixasse aquela linha específica entrar, e recusou.

Essa é a mensagem inteira. Nada está corrompido e nada se perdeu: a linha nunca chegou a ser salva, e o resto da tabela está igualzinho a um minuto atrás. A redação vem do Postgres, o motor de banco de dados em que o Supabase roda, e por isso ela soa a maquinário em vez de soar a algo escrito pelo seu builder. O Supabase repassa do jeito que veio, com o número do lado, 42501.

O que a mensagem não vai te dizer é qual regra estava faltando. Uma frase só cobre três situações diferentes, e te dizer em qual você está descreveria as suas regras para quem provocou o erro:

  • A tabela está com o Row Level Security ligado e nenhuma regra escrita.
  • Ela tem uma regra, e essa regra fala apenas de leitura.
  • Ela tem uma regra de escrita, e a linha que você mandou não satisfaz ela.

As três imprimem essa mesma linha. A segunda é a comum num app recém-lançado, onde uma regra foi adicionada para as telas voltarem a funcionar e de salvar ninguém falou.

Por que ele apareceu bem na hora em que você ligou o Row Level Security

Porque ligar recusa tudo até uma regra dizer o contrário, e o seu próprio app faz parte de tudo.

Essa é a sequência que quase todo mundo passa. Você liga a configuração. As telas ficam em branco, porque sem regra escrita o banco agora não entrega linha para ninguém, o seu app incluído. Você adiciona uma regra para as listas voltarem, normalmente a primeira que um resultado de busca ou o seu builder sugere. As telas enchem. E na primeira vez que alguém aperta salvar, esse erro aparece, numa tabela que você dava por resolvida.

Nada deu errado nessa sequência. A regra que você adicionou falava de leitura, e salvar é outra pergunta que até ali ninguém tinha feito.

Uma regra tem duas metades, e só uma fala de leitura

Uma policy é uma condição, e onde o banco aplica essa condição depende do que você pediu.

USING se aplica às linhas que já estão na tabela. Ela decide quais você pode ver, mudar ou tirar. WITH CHECK se aplica à linha que você está tentando criar, antes de ela existir em qualquer lugar. Ela decide se aquela linha pode vir a existir.

Essa segunda metade é a parte que surpreende, porque é uma regra sobre algo que ainda não está lá.

O seu app pede paraConferido contra as linhas que já estão na tabelaConferido contra a linha sendo escrita
ler (select)USINGnada a conferir
salvar uma linha nova (insert)nada a conferirWITH CHECK
mudar uma linha (update)USINGWITH CHECK
tirar uma linha (delete)USINGnada a conferir
A mesma condição, apontada para duas coisas diferentes. Na leitura ela é perguntada sobre linhas que existem; no salvamento, sobre uma linha que ainda não existe.

Uma policy escrita com FOR SELECT só carrega a primeira metade, porque quando alguém lê não existe linha nova para conferir. Então ela nunca consegue permitir um salvamento, por mais permissiva que pareça. É esse o descompasso inteiro, e ele explica por que as suas leituras se recuperaram e as suas escritas não.

Se você preferir ver isso de fora, o nosso scan gratuito pergunta ao seu banco ao vivo o que um estranho já consegue ler dele, sem conta e em uns 20 segundos: escanear o seu app.

O conserto que faz o erro parar, e o que ele custa

O jeito mais rápido de fazer isso sumir é uma regra que permita qualquer escrita de qualquer um, e é por isso que ela é a resposta do topo em quase todo lugar que você olhar.

Ela costuma chegar com esta cara:

CREATE POLICY "Enable insert for all users"
  ON public.profiles
  FOR INSERT
  WITH CHECK (true);

Toda linha satisfaz true, então todo salvamento é permitido, o do seu app e o de qualquer outra pessoa com a chave que viaja dentro dele. Desligar o Row Level Security de novo faz o mesmo serviço de forma ainda mais completa.

Os dois funcionam. Os dois deixam você onde estava antes de o advisor apontar a tabela, e depois nenhum dos dois mostra qualquer sinal de que algo está aberto, porque o seu app se comporta igualzinho nas quatro combinações de configuração e regra. Uma tabela pode estar ligada, ter uma policy válida, mostrar um selo verde no seu painel e ainda assim entregar as linhas dela para um estranho. Essa é a outra metade deste problema, e vale ler antes de colar qualquer coisa.

A regra que deixa o seu app salvar, e só o seu app

Nomeie a quem a linha pertence, e compare com quem está pedindo:

CREATE POLICY "Users insert their own rows"
  ON public.profiles
  FOR INSERT
  TO authenticated
  WITH CHECK (auth.uid() = user_id);

auth.uid() é quem está logado e fazendo a requisição. user_id é a coluna na linha que diz a quem ela pertence. Quando os dois batem, a linha é salva. Quando não batem, ou quando não tem ninguém logado, ela é recusada, e quem foi recusado vê a mesma mensagem que você está olhando.

Dois detalhes dessa regra ganham o lugar deles. TO authenticated quer dizer que a regra vale para visitantes logados; tire isso e a policy passa a valer para todo mundo, estranhos incluídos. E o seu app precisa mandar de fato a coluna user_id, porque uma regra que compara auth.uid() com um valor que chegou vazio nunca vai bater.

Quando a regra está certa e o erro aparece mesmo assim

Olhe quem estava logado antes de olhar a policy de novo.

Três causas diferentes chegando até você como uma frase só. O código 42501 é o mesmo nas três, e é por isso que a mensagem sozinha não consegue te dizer em qual você está.

Três explicações cobrem quase tudo o que sobra:

Não tem ninguém logado. auth.uid() volta vazio para um visitante que não entrou, então uma regra que compara isso com uma coluna de dono não tem como bater. Isso é normal num formulário de cadastro, numa lista de espera ou num formulário de contato, e esses precisam de uma regra própria descrevendo o que um estranho pode adicionar.

A coluna de dono nunca chega. A sua regra compara auth.uid() com user_id, e o seu app manda tudo menos user_id. A comparação roda contra um vazio e falha toda vez.

A escrita está indo para o Storage. Uploads de arquivo caem no Supabase Storage, que mantém as próprias policies em storage.objects em vez de na sua tabela. Uma regra escrita em profiles não diz nada sobre um arquivo.

Tem uma quarta explicação, e é a que vale descartar cedo: se salvar funciona no preview do seu builder e falha no seu site no ar, os dois não estão usando a mesma chave. Uma chave secreta ignora toda regra que você escreveu, é para isso que ela serve, então um app que só salva enquanto tem uma chave secreta em jogo é um app cujas regras nunca foram testadas de verdade. Quais chaves de API são seguras no seu frontend mostra como distinguir uma da outra.

O que fazer hoje

O que fazer

  • Leia o erro como uma recusa e não como um defeito. A escrita não aconteceu, a tabela está intacta, e não tem nada para recuperar.
  • Confira se a tabela tem alguma policy sobre escrita. Uma regra escrita com FOR SELECT consertou as suas telas e não disse nada sobre salvar.
  • Adicione uma policy FOR INSERT cuja condição WITH CHECK nomeie o dono da linha, e adicione a cláusula TO que você quis dizer.
  • Confirme que o seu app manda mesmo a coluna de dono, e tente salvar de novo estando logado.
  • Deixe WITH CHECK (true) para tabelas cujo conteúdo você publicaria numa página pública. Para qualquer coisa com uma pessoa dentro, gaste os dois minutos.

Comece pela tabela de onde veio o erro, e confira toda outra tabela em que você ligou a configuração na mesma tarde. O checklist de segurança de 10 minutos cobre o que mais costuma ficar aberto num app recém-lançado, e o guia de segurança do Supabase passa pelo resto do que um estranho consegue alcançar.

Perguntas frequentes

O que significa "new row violates row-level security policy"?

Significa que pediram ao seu banco para salvar uma linha, ele conferiu as regras daquela tabela e não achou nenhuma que deixasse aquela linha entrar. Então recusou a escrita. Nada foi perdido e nada está quebrado, porque a linha nunca chegou a ser salva e o resto da tabela está intacto. A mensagem vem do Postgres, o motor de banco de dados em que o Supabase roda, e ela carrega o código 42501. O que ela deliberadamente não diz é qual regra estava faltando, porque dizer isso descreveria as suas regras para quem provocou o erro.

Adicionei uma policy e a leitura funciona. Por que salvar continua falhando?

Porque uma policy sobre leitura não diz nada sobre escrita. Uma regra carrega uma metade USING, que o banco aplica às linhas que já estão na tabela, e uma metade WITH CHECK, que ele aplica à linha que você está tentando criar. Uma policy escrita com FOR SELECT só tem a primeira, já que na leitura não existe linha nova para conferir. As suas telas voltam a encher e o primeiro salvamento continua falhando. Adicione uma segunda policy FOR INSERT com uma condição WITH CHECK.

Posso simplesmente desligar o Row Level Security para isso sumir?

Isso faz o erro sumir, e deixa cada linha daquela tabela legível por qualquer um que tenha a chave que viaja dentro do seu app. O seu app funciona dos dois jeitos, então depois nada te diz qual dos dois você escolheu. Se a tabela guarda pessoas, pedidos ou mensagens, os dois minutos que custa escrever uma regra de verdade são a diferença entre uma tabela privada e uma pública.

Minha policy parece certa e os inserts continuam falhando. O que mais pode ser?

Três coisas explicam a maioria dos casos. Não tem ninguém logado, então auth.uid() volta vazio e uma regra que compara isso com uma coluna de dono nunca vai bater, o que é normal num formulário de cadastro ou de contato. Ou o seu app não manda a coluna de dono, e a regra compara contra um valor que chegou em branco. Ou a escrita vai para o Supabase Storage em vez de para uma tabela, e o Storage mantém as próprias policies em storage.objects.

Esse erro quer dizer que alguém tentou atacar o meu app?

Quase nunca. Num app recém-lançado é quase sempre o seu próprio app sendo recusado, porque o Row Level Security foi ligado antes de existir uma regra que cobrisse as suas próprias escritas. Ainda assim vale ler em vez de descartar: a mesma mensagem aparece quando uma requisição que não deveria estar escrevendo naquela tabela é recusada, e só pela mensagem você não distingue as duas.

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.