Pular para o conteúdo
Heartscale

supabasePublicado em · Equipe Heartscale

RLS no Supabase: como configurar e testar se o seu app vaza dados

O que é RLS, em uma frase

RLS (Row Level Security, segurança por linha) é a regra do banco que decide quais linhas cada pessoa pode ler, criar, alterar ou apagar. Sem ela, quem pode acessar a tabela acessa a tabela inteira.

Por que isso importa no Lovable, no Bolt e no Base44

Apps feitos com essas ferramentas costumam falar com o banco direto do navegador. Para isso, a chave publicável do Supabase vai junto com o site. Ela foi feita para ficar exposta, mas com uma condição: o Supabase pede que você ligue o RLS em todas as tabelas antes de publicar (Supabase, API keys).

A documentação do Supabase resume o risco: uma tabela num schema exposto, sem RLS, pode ser lida e alterada por qualquer papel com permissão sobre ela (Supabase, Row Level Security). E em projetos antigos, tabelas novas no schema public podem nascer com todas as permissões dadas aos papéis do cliente.

Os exemplos abaixo usam uma tabela pedidos com uma coluna user_id, que guarda o dono de cada linha. Troque pelos nomes do seu app.

Passo 1: ache as tabelas expostas

No painel do Supabase, abra o Security Advisor. Ele também roda pela CLI com supabase db advisors (Supabase, Database Advisors). Os alertas que importam aqui:

Alerta O que quer dizer
0013_rls_disabled_in_public Tabela pública com RLS desligado. Quem tem a URL do projeto lê, edita e apaga as linhas
0007_policy_exists_rls_disabled Há política, mas o RLS está desligado. A política não vale
0008_rls_enabled_no_policy RLS ligado e nenhuma política. A API não devolve nada
0010_security_definer_view View que roda com o poder de quem a criou e passa por cima do RLS
0002_auth_users_exposed View que expõe a tabela de usuários do login

Comece pelos de nível ERROR: o 0013, o 0010 e o 0002.

No Lovable Cloud, a lista de políticas fica em More → Database → Manage → Policies. Ela é só de leitura: as mudanças são pedidas ao Lovable no chat (Lovable, Database access). Os passos abaixo servem para você saber o que pedir e o que conferir.

Passo 2: ligue o RLS

No SQL Editor:

alter table public.pedidos enable row level security;

Atenção: a partir daqui, nada passa pela chave publicável até existir uma política (Supabase, Row Level Security). Se o app já está no ar, ele vai parar de mostrar os pedidos. Faça os passos 2 a 4 juntos, de preferência num projeto de teste.

Passo 3: revise as permissões dos papéis

O Supabase usa dois papéis para quem vem do navegador:

  • anon: quem não fez login;
  • authenticated: quem fez login.

Tire tudo e devolva só o necessário. Se só usuário logado mexe em pedidos:

revoke all on table public.pedidos from anon, authenticated;
grant select, insert, update, delete on table public.pedidos to authenticated;

Passo 4: escreva uma política por operação

A doc do Supabase recomenda uma política para cada operação e sempre com to, para dizer a que papel ela vale. No caso “cada usuário só mexe nos próprios pedidos”:

create policy "ler os próprios pedidos"
on public.pedidos for select
to authenticated
using ( (select auth.uid()) = user_id );

create policy "criar pedido em nome próprio"
on public.pedidos for insert
to authenticated
with check ( (select auth.uid()) = user_id );

create policy "alterar os próprios pedidos"
on public.pedidos for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

create policy "apagar os próprios pedidos"
on public.pedidos for delete
to authenticated
using ( (select auth.uid()) = user_id );

using e with check não são a mesma coisa:

  • using decide quais linhas que já existem a pessoa pode ver, alterar ou apagar.
  • with check decide como a linha pode ficar depois de criada ou alterada.

No update, escreva os dois, como faz a doc do Supabase. Se o with check faltar, o PostgreSQL usa o using também para a linha nova (PostgreSQL, CREATE POLICY). Funciona, mas esconde a intenção: quem ler a política depois não sabe se a omissão foi de propósito.

Um erro comum é criar só a política de select e achar que a tabela está protegida. Cada operação precisa da sua.

Passo 5: corrija as views

View criada no schema public roda, por padrão, com o poder de quem a criou e ignora o RLS de quem consulta. A correção indicada pelo Supabase é criar a view com security_invoker=on (Supabase, Database Advisors):

create view public.resumo_pedidos
with (security_invoker = on) as
select user_id, count(*) as total
from public.pedidos
group by user_id;

Nunca exponha auth.users numa view pública. Se precisa de nome e foto do usuário, crie uma tabela de perfil com RLS própria.

Passo 6: crie índice nas colunas das políticas

Política roda em toda consulta. Para não deixar o app lento, o Supabase pede duas coisas (Supabase, Row Level Security):

  1. Índice em toda coluna usada nas políticas:
create index pedidos_user_id_idx on public.pedidos using btree (user_id);
  1. Escrever (select auth.uid()) em vez de auth.uid(). Assim o Postgres calcula o valor uma vez por consulta, e não uma vez por linha.

Passo 7: teste se ainda vaza

Faça os testes no seu próprio projeto.

Teste 1: como estranho. Use a chave publicável, a mesma que está no site, e chame a API REST do Supabase (Supabase, REST API):

curl "https://SEU-PROJETO.supabase.co/rest/v1/pedidos?select=*" \
  -H "apikey: SUA_CHAVE_PUBLICAVEL"

O esperado, com o revoke do passo 3, é um erro de permissão ou uma lista vazia. Se vierem pedidos, qualquer visitante do site consegue o mesmo.

Teste 2: como outro cliente. Crie os usuários A e B. Com A, crie um pedido. Entre com B e tente ver, editar e apagar o pedido de A pelo app, trocando o ID no endereço se o app usar IDs na URL. O Lovable pede esse mesmo teste antes de publicar (Lovable, Security best practices).

Rode de novo o Security Advisor no fim. Os alertas do passo 1 devem ter sumido.

O erro “new row violates row-level security policy”

Esse erro aparece depois que o RLS é ligado e é bom sinal: o banco recusou uma linha. Pela documentação do PostgreSQL, quando a expressão do with check não é verdadeira para a linha nova, o banco devolve erro em vez de gravar (PostgreSQL, CREATE POLICY).

Quase sempre a causa é uma destas:

  • não há política de insert para o papel que está gravando;
  • o app não manda o user_id, ou manda um diferente do usuário logado;
  • a pessoa não está logada, e a requisição chega como anon.

Não resolva com uma política que aceita tudo (with check (true)). Corrija o app para mandar o user_id certo, ou preencha a coluna com um valor padrão auth.uid().

O mesmo erro em storage quer dizer que falta política no bucket de arquivos. A lógica é a mesma.

Cuidado com a chave secreta e as Edge Functions

A chave secreta (sb_secret_..., ou a antiga service_role) passa por cima de todas as políticas. O Supabase diz para nunca usá-la no navegador, num app distribuído ou no controle de versão (Supabase, API keys).

Ela tem lugar em função de servidor. Mas aí o RLS não protege mais nada: a função tem que checar sozinha quem chamou e o que essa pessoa pode fazer (Lovable, Security best practices). Revise toda Edge Function que usa a chave secreta.

Se achar a chave secreta no código do site, troque-a: crie uma nova, substitua em todo lugar e só depois desative a antiga.

Peça o diagnóstico da Heartscale

No diagnóstico da Heartscale, nossa equipe lê todas as tabelas, políticas, views e funções do seu Supabase ou Lovable Cloud. Também testa o acesso com a chave pública e confere onde a chave secreta é usada. Você recebe a lista do que está aberto, por gravidade, e o preço da correção.

O diagnóstico custa R$ 398 e é abatido se você fechar a correção. Só trabalhamos em app de quem prova ser dono.

Pedir o diagnóstico do meu app

Comece pelo diagnóstico.

O diagnóstico custa R$ 398, pagos por Pix. Entregamos o relatório em 2 dias úteis e apresentamos os resultados em reunião. O valor é abatido se você contratar o serviço em até 15 dias. Tudo o que configuramos fica no seu nome.

Reduz o risco, não o zera. Atendemos somente o dono do app, mediante comprovação.