Banco de dados · Supabase
Supabase sem RLS: qualquer pessoa pode ler — e modificar — seus dados?
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árioVerifique 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átisSem cadastro · Resultado por e-mail em minutos