Banco de dados · Supabase

Supabase sem RLS: qualquer pessoa pode ler — e modificar — seus dados?

Publicado em 27 mar 2026·Leitura: ~12 min

Sim. Um banco Supabase sem Row Level Security ativo é acessível por qualquer pessoa que tenha a URL do projeto e a chave anon — ambas encontradas no bundle JavaScript do seu app. Com essas duas informações, qualquer um pode fazer uma requisição HTTP direta à API do Supabase e ler, inserir ou modificar dados sem qualquer autenticação. Isso afeta dados de usuários, pedidos, mensagens privadas, informações de pagamento — qualquer coisa armazenada nas tabelas sem RLS.

A diferença entre a chave anon e a service_role — e onde cada uma é segura

O Supabase gera duas chaves no dashboard de cada projeto. A anon key é projetada para uso público — ela aparece no frontend por design, está na documentação oficial e o Supabase a chama explicitamente de "safe to use in a browser". Isso é verdade, mas com uma condição fundamental: apenas quando o RLS está ativo nas tabelas.

A service_role key é a chave de administrador — ela ignora o RLS completamente e tem acesso irrestrito a tudo. Nunca deve aparecer no frontend. Ferramentas de vibe coding ocasionalmente geram código usando a service_role com prefixo NEXT_PUBLIC_ — criando o cenário mais crítico possível: chave administrativa visível publicamente.

anon key — pública por design

  • ✓ Pode ir no frontend (NEXT_PUBLIC_)
  • ✓ Segura com RLS ativo
  • ✓ Necessária para autenticação do usuário
  • ✗ Expõe tudo se RLS estiver desativado

service_role key — nunca no frontend

  • ✗ Jamais com NEXT_PUBLIC_
  • ✗ Ignora RLS completamente
  • ✓ Apenas em server-side (API routes, edge functions)
  • ✓ Para operações administrativas no servidor

O que é possível fazer sem RLS — demonstração com curl

A API REST do Supabase é documentada e padronizada. Qualquer pessoa que conheça a URL e a chave anon pode construir as requisições abaixo — informações que estão disponíveis no bundle JavaScript do seu app para qualquer visitante que abra o DevTools.

# Com a URL e a chave anon (encontradas no bundle JS do seu app):
# Um atacante consegue ler TODOS os usuários sem autenticação:

curl 'https://xyz.supabase.co/rest/v1/users?select=*' \
  -H "apikey: eyJhbGciOiJIUzI1NiJ9..." \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."

# Resposta se RLS estiver desativado:
# [{"id":"uuid-1","email":"usuario1@email.com","nome":"João",...},
#  {"id":"uuid-2","email":"usuario2@email.com","nome":"Maria",...}, ...]

# Inserir um registro arbitrário:
curl -X POST 'https://xyz.supabase.co/rest/v1/orders' \
  -H "apikey: eyJhbGciOiJIUzI1NiJ9..." \
  -H "Content-Type: application/json" \
  -d '{"user_id": "qualquer-uuid", "total": 0, "status": "paid"}'

Não é necessário nenhuma ferramenta especial — apenas curl ou o fetch do próprio navegador. O ataque é trivial e totalmente automatizável.

Como verificar se suas tabelas têm RLS ativo

Existem duas formas: visualmente no Dashboard ou via SQL. A consulta SQL é mais confiável porque mostra todas as tabelas de uma vez, sem precisar clicar em cada uma.

-- Execute no SQL Editor do Supabase Dashboard
-- Retorna todas as tabelas com status do RLS
SELECT
  tablename,
  rowsecurity AS rls_ativo
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;

Qualquer tabela com rls_ativo = false está exposta. Se você armazena qualquer dado de usuário nessas tabelas, trate como incidente de segurança e ative o RLS imediatamente.

Como ativar o RLS e escrever políticas corretamente

Ativar o RLS sem criar políticas bloqueia todas as operações — inclusive as do seu próprio app. Isso é o comportamento correto do PostgreSQL: sem permissão explícita, ninguém acessa. O processo completo tem duas etapas: ativar o RLS e criar as políticas que definem quem pode fazer o quê.

Etapa 1 — Ativar RLS nas tabelas

-- 1. Ativar RLS em todas as tabelas públicas
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Repita para cada tabela em public.*

Etapa 2 — Políticas básicas (usuário acessa apenas seus dados)

-- 2. Política: usuário lê apenas seus próprios dados
CREATE POLICY "users_select_own"
  ON public.users
  FOR SELECT
  USING (auth.uid() = id);

-- 3. Política: usuário atualiza apenas seus próprios dados
CREATE POLICY "users_update_own"
  ON public.users
  FOR UPDATE
  USING (auth.uid() = id)
  WITH CHECK (auth.uid() = id);

-- 4. Política: usuário vê apenas seus próprios pedidos
CREATE POLICY "orders_select_own"
  ON public.orders
  FOR SELECT
  USING (auth.uid() = user_id);

-- 5. Política: usuário cria pedidos apenas em seu próprio nome
CREATE POLICY "orders_insert_own"
  ON public.orders
  FOR INSERT
  WITH CHECK (auth.uid() = user_id);

Casos especiais: admins e dados públicos

Nem todo acesso é de usuário para seus próprios dados. Admins precisam acessar todos os registros. Alguns dados são intencionalmente públicos. O RLS suporta esses casos com políticas específicas.

Política para administradores

-- Política para admins: acesso total (usando coluna role na tabela users)
CREATE POLICY "admin_all_access"
  ON public.orders
  FOR ALL
  USING (
    EXISTS (
      SELECT 1 FROM public.users
      WHERE users.id = auth.uid()
        AND users.role = 'admin'
    )
  );

Política para conteúdo público

-- Política para dados públicos (ex: posts públicos de um blog)
CREATE POLICY "posts_public_select"
  ON public.posts
  FOR SELECT
  USING (published = true);

-- Usuário autenticado pode criar posts
CREATE POLICY "posts_insert_authenticated"
  ON public.posts
  FOR INSERT
  WITH CHECK (auth.uid() IS NOT NULL AND auth.uid() = author_id);

Erros comuns ao configurar RLS

Ativar RLS mas não criar políticas

Bloqueia tudo — inclusive seu próprio app. Usuários não conseguem mais ler seus dados. Deve-se criar políticas explícitas para cada operação legítima.

Criar política usando o ID do body em vez do auth.uid()

A política fica vulnerável a IDOR: USING (id = (request.body->'userId')::uuid) pode ser manipulado. Sempre use auth.uid() — o ID vem do JWT verificado pelo Supabase, não da requisição.

Esquecer políticas para INSERT e UPDATE

Criar apenas a política de SELECT é insuficiente. Um usuário sem política de INSERT pode ser bloqueado de criar dados. Um sem política de UPDATE pode modificar registros de outros se a condição USING não estiver correta.

Usar service_role no cliente Supabase do frontend

O cliente criado com createClient(url, service_role_key) ignora completamente o RLS — mesmo que você tenha configurado todas as políticas. Nunca inicialize o cliente Supabase com a service_role key no frontend.

Como testar se suas políticas estão funcionando

O Supabase oferece um SQL Editor onde você pode simular o contexto de diferentes tipos de usuário para validar que suas políticas funcionam como esperado antes de ir para produção.

-- Testar como usuário anônimo (substitui o contexto de autenticação)
SET LOCAL role = anon;
SELECT * FROM public.users; -- deve retornar zero linhas ou dar erro de permissão

-- Testar como usuário específico
SET LOCAL role = authenticated;
SET LOCAL request.jwt.claims = '{"sub": "uuid-do-usuario"}';
SELECT * FROM public.orders; -- deve retornar apenas os pedidos desse usuário

Verifique a segurança completa do seu domínio

O QuickScan detecta credenciais Supabase expostas no bundle, headers de segurança ausentes, SSL mal configurado e mais — sem cadastro, em minutos.

Escanear meu app agora — grátis

Sem cadastro · Resultado por e-mail em minutos

Leia também