Pular para o conteúdo

Noções de segurança

Supabase security checker: faça você mesmo as cinco verificações

Um Supabase security checker lê o seu app publicado em vez das configurações do projeto. Aqui estão as cinco verificações e como fazer cada uma sozinho.

Vlad Tkachenko10 min de leitura
Uma janela de terminal vista de frente, com cinco comandos curtos digitados sob um prompt verde e um cursor esperando na sexta linha.

Em resumo

  • Um Supabase security checker lê o seu app no ar do jeito que um estranho lê: o bundle que ele entrega, as respostas das suas tabelas, seus buckets de armazenamento, seus cabeçalhos.
  • O Security Advisor do próprio Supabase lê a outra metade, que é a configuração do seu projeto. Nenhum dos dois enxerga o que o outro enxerga.
  • Cinco verificações cobrem a vista de fora. Todas rodam de um terminal, não pedem conta nenhuma e juntas levam uns dez minutos.

Você pesquisou por um Supabase security checker e apareceram nove. Um quer o seu repositório. Outro quer uma extensão de navegador. Outro quer acesso de leitura à sua conta inteira do Supabase, o que é parecido o bastante com aquilo que estava te preocupando para te fazer hesitar.

Aqui está o que nenhuma dessas páginas de produto diz em voz alta. São cinco verificações, e você pode fazer todas sozinho, sem conta, sem instalar nada e sem nenhuma das credenciais que seria perigoso entregar. Dez minutos num terminal cobrem o mesmo terreno que as ferramentas.

As cinco abaixo são as que o nosso próprio scanner roda contra um projeto Supabase, escritas como comandos. Se você construiu com Lovable, Bolt, v0, Cursor, Replit, Windsurf ou Base44 e o seu app guarda qualquer coisa, o banco por trás é muito provavelmente Supabase, e estas são as perguntas que um estranho faria a ele.

O que um Supabase security checker verifica de verdade?

O seu app publicado. Os ajustes dentro do projeto são trabalho de outra ferramenta, e o Supabase já entrega essa ferramenta.

O Security Advisor no seu painel lê o catálogo do próprio projeto: quais tabelas estão com Row Level Security desligada, quais funções têm um search path frouxo, quais extensões vivem no schema public. Ele está lendo o que você configurou.

Um checker lê o que o seu app entrega a um estranho. O bundle JavaScript que seus visitantes baixam, a resposta que uma tabela dá a uma requisição sem login, o conteúdo de um bucket de armazenamento, os cabeçalhos das suas páginas. Da sua configuração ele não vê nada, e não precisa, porque está olhando a consequência dela.

As duas metades se separam com mais frequência do que os nomes sugerem. Uma tabela pode passar pelo Advisor com a chave ligada e uma policy escrita, e ainda assim entregar as linhas dela para qualquer pessoa, porque a policy que foi escrita deixa todo mundo entrar. Isso merece uma leitura própria se for novidade para você: o botão não é a proteção.

Verificação 1: qual chave do Supabase o seu app entrega?

Abra o seu app no navegador, aperte F12 e olhe uma única requisição.

Vá para a aba Rede, recarregue a página e digite supabase no filtro. Seu app vai fazer pelo menos uma requisição para um endereço terminado em .supabase.co. Clique nela. As duas coisas de que você precisa para o resto do artigo estão ali dentro:

  • O endereço da requisição começa com a URL do seu projeto, algo como https://abcdefghij.supabase.co. As verificações 2 e 3 miram nele.
  • O cabeçalho de requisição apikey carrega a chave que o seu app entrega a cada visitante.

Copie as duas para algum lugar e então leia a própria chave. Uma que começa com sb_publishable_ pertence ao seu app e não há nada a consertar. Uma que começa com sb_secret_ nunca pertence, e encontrá-la encerra a lista mais cedo: gire essa chave hoje, antes de tudo o mais nesta página. Projetos mais antigos carregam um par sem prefixo legível, anon e service_role, e distinguir os dois significa ler a role dentro da chave.

Esse último caso é raro. Entre os 30.998 apps no ar que escaneamos em agosto de 2026, uma chave secreta do Supabase no navegador apareceu três vezes.

Enquanto a aba Rede estiver aberta, anote os nomes de tabela que você conseguir ver nesses endereços. A próxima verificação precisa deles.

Tudo o que vem a seguir mira em um de dois endereços: o do seu app e o do seu projeto. A verificação 1 é o que te dá o segundo.

Verificação 2: suas tabelas respondem a um estranho?

Pergunte ao banco quantas linhas ele entregaria a alguém sem login, e leia o número na resposta.

curl -s -i \
  -H "apikey: SUA_CHAVE_PUBLICAVEL" \
  -H "Prefer: count=exact" \
  -H "Range: 0-0" \
  "https://SEU_PROJETO.supabase.co/rest/v1/profiles?select=count" \
  | grep -i content-range

Ponha a chave e o endereço do projeto da verificação 1, e o nome da sua própria tabela onde está profiles. O curl já vem instalado no macOS, no Linux e no Windows atual, então não há nada para baixar antes.

Essa requisição não busca linha nenhuma. select=count pede uma contagem, Range: 0-0 não pede nenhuma das linhas em si, e a resposta inteira chega em um único cabeçalho. É a mesma requisição que o nosso scanner faz, e é por isso que ele pode rodar contra o app no ar de alguém sem tocar nos dados dessa pessoa.

Quatro coisas podem voltar, e uma delas é um achado:

O que apareceO que significa
content-range: */0Nada legível sem login. Essa tabela está fazendo o trabalho dela.
content-range: 0-0/128128 linhas ao alcance de qualquer um com a chave que viaja na sua página.
401 ou 403A chave foi recusada antes de chegar à tabela. Sem resposta, não limpo.
404Não existe tabela com esse nome. Ou você chutou, ou ela se escreve de outro jeito.
As duas requisições deram certo. Um cabeçalho é toda a diferença entre uma tabela que faz o trabalho dela e uma que entrega linhas a quem pedir.

A recusa é a linha com a qual é preciso ter cuidado. Um 401 diz que a requisição parou antes de chegar à tabela, o que não te conta nada sobre a tabela estar protegida, e é a mais fácil das quatro de arredondar para um visto verde.

Rode o comando para cada tabela que o seu app nomeou na verificação 1, e depois para as comuns que um app costuma ter: users, profiles, customers, orders, messages, invoices. Nessas seis moram os dados de outras pessoas.

Essa é a verificação que mais encontra. Dos 3.680 apps com Supabase em que conseguimos completá-la, 2.096 tinham pelo menos uma tabela respondendo a uma requisição sem login, e em 394 deles a tabela aberta tinha nome de pessoas. A medição completa e de que ela é uma fatia está em um artigo separado.

Verificação 3: um bucket de armazenamento lista os próprios arquivos?

Peça a um bucket o conteúdo dele e veja se volta alguma coisa.

curl -s -X POST \
  -H "apikey: SUA_CHAVE_PUBLICAVEL" \
  -H "Content-Type: application/json" \
  -d '{"prefix":"","limit":1}' \
  "https://SEU_PROJETO.supabase.co/storage/v1/object/list/avatars"

Mande o body. Um POST para esse endereço sem nada dentro volta com "Body cannot be empty" para todo bucket de todo projeto, esteja ele configurado como estiver. Sabemos disso porque a nossa própria verificação de buckets saiu assim e passou meses informando alegremente que nada era listável.

Duas formas de resposta:

  • [] quer dizer que o bucket está fechado, ou que não existe bucket com esse nome. De fora você não consegue saber qual das duas, então uma lista vazia não é prova de nada.
  • Um array com um arquivo dentro quer dizer que um estranho pode pedir ao seu projeto um diretório daquele bucket e recebê-lo.

Experimente os nomes de bucket que o seu app usou, e depois os comuns: avatars, public, uploads, files, images, documents.

Listável e público são dois ajustes diferentes guardados em dois lugares diferentes, e a diferença decide o quanto um bucket público importa. A listagem é a que se fecha, porque ela poupa de um estranho o trabalho de adivinhar um nome de arquivo. Dos 27.269 apps em que pudemos perguntar, 792 responderam com uma lista.

Verificação 4: seu código-fonte é publicado junto com o app?

Leia a última linha do seu bundle JavaScript.

De volta à aba Rede da verificação 1, ache o maior arquivo .js que o seu app carregou e copie o endereço dele. Então:

curl -s "https://seu-app.example/assets/index-abc123.js" | tail -c 120

Uma última linha dizendo //# sourceMappingURL=index-abc123.js.map quer dizer que o seu build escreveu um source map e disse aos navegadores onde ele está. Se ele de fato publicou o arquivo, isso uma requisição a mais esclarece:

curl -s -o /dev/null -w "%{http_code}\n" \
  "https://seu-app.example/assets/index-abc123.js.map"

200 quer dizer que qualquer um pode baixar. Um source map transforma o JavaScript comprimido de volta nos seus arquivos originais, com a sua estrutura de pastas, seus comentários e sua lógica intactos. O que ele expõe é o seu código, e qualquer chave que ele revele já estava ali do lado no bundle, que era o assunto da verificação 1. 3.885 dos 30.987 apps que pudemos examinar publicam o deles, e em alguns builders isso é um padrão da plataforma e não uma decisão de alguém: o que um source map publicado mostra.

Verificação 5: o que a sua hospedagem envia com cada página

Uma requisição ao endereço do próprio app, lendo o que voltou junto.

curl -s -i "https://seu-app.example" | grep -i -E \
  "content-security-policy|strict-transport-security|x-frame-options|x-content-type-options|referrer-policy"

Cinco cabeçalhos, e os que não aparecem são os que estão faltando. Eles mandam o navegador recusar a página se ela for carregada dentro do site de outra pessoa, insistir numa conexão criptografada na próxima vez, e parar de adivinhar que tipo de arquivo acabou de receber.

Quase todo app falha nessa e quase ninguém consegue agir a respeito. A 30.756 dos 30.981 apps que pudemos examinar faltava pelo menos um, porque quem envia esses cabeçalhos é a plataforma de hospedagem e um dono em subdomínio de builder não tem onde mudá-los. Nós contamos e limitamos a médio: o que cabeçalhos faltando preveem e o que não preveem.

Como ler o que você encontrou

Por gravidade, que não é a ordem em que você as rodou.

Uma chave secreta no bundle vem primeiro. Ela pula todas as regras que você escreveu em todas as tabelas, então gire essa chave antes de olhar qualquer outra coisa aqui.

Uma tabela cheia de pessoas respondendo à verificação 2 vem em seguida. Em users, profiles, orders e messages moram os dados de outra gente, e eles estão alcançáveis agora. Uma tabela products respondendo do mesmo jeito pode estar exatamente certa, e você é a única pessoa capaz de distinguir as duas.

Depois um bucket listável, depois os maps e os cabeçalhos, que costumam ser os padrões da sua plataforma e não algo que você escolheu.

Uma coisa para levar das cinco. Uma verificação que não obteve resposta não passou. Um 401, uma requisição que estourou o tempo, uma tabela cujo nome você chutou errado: cada uma delas é uma pergunta ainda em aberto. Nosso scanner escreve "Não foi possível verificar" nessas linhas e deixa a nota em paz, e fazer isso à mão significa manter essa lista você mesmo.

Tudo isso descreve também o app que estava no ar no momento em que você perguntou. A Row Level Security é desligada para uma página carregar, um bucket é aberto para um upload, uma chave é colada para um recurso sair hoje à noite.

O que fazer esta semana

O que fazer

  • Rode a verificação 2 contra cada tabela que o seu app nomeia, e depois contra users, profiles, customers, orders e messages. É ali que estão os achados.
  • Se a verificação 1 revelou uma chave secreta, gire essa chave primeiro. Apagar do código deixa o valor antigo funcionando para quem já o tem.
  • Anote toda sonda que recebeu recusa ou resposta nenhuma. Essas ficam sem resposta, e uma pergunta sem resposta não é um resultado limpo.
  • Deixe em paz as tabelas que devem ser públicas. Uma lista de produtos respondendo a um estranho é o seu app funcionando direito.
  • Rode as cinco de novo depois de um deploy e depois que alguém mexer numa regra do banco. Nenhum dos dois se anuncia sozinho.
As mesmas cinco verificações, mais outras quatro, devolvidas como relatório. A pontuação é 85 e a nota é C, porque um achado alto limita a letra diga o que disser a aritmética. A linha tracejada é o banco de dados, que nunca respondeu.

Nosso scanner de segurança gratuito roda essas cinco e mais quatro contra qualquer URL no ar em uns vinte segundos, sem conta e sem que você entregue credenciais. Ele lê, nunca escreve, e onde não consegue resposta ele diz isso na linha.

As cinco você pode fazer sozinho hoje. O que nenhuma passada isolada consegue te dizer é se as respostas continuam as mesmas no mês que vem, e foi para isso que construímos o Reeve Care: ele repete essas verificações em uma agenda, te manda e-mail quando uma resposta piora, e guarda backups verificados do seu banco Supabase, mais os arquivos que seus usuários enviam assim que você conecta uma credencial de armazenamento. O que ele vigia e quanto custa.

Se um terminal não é onde você quer estar, o passo a passo em linguagem simples para apps com Supabase explica para que serve cada um desses ajustes e onde encontrá-lo no painel.

Perguntas frequentes

O Security Advisor do Supabase pega tudo?

Pega tudo na metade dele. O Advisor lê a configuração do seu projeto e informa tabelas com Row Level Security desligada, funções com search path frouxo e ajustes parecidos que você controla pelo painel. O que ele não consegue ver é o seu app publicado: qual chave foi parar no JavaScript que seus visitantes baixam, se uma policy que você escreveu de fato barra uma requisição anônima, ou quais cabeçalhos a sua hospedagem envia. Rode o Advisor e as cinco verificações deste artigo, porque eles olham para coisas diferentes.

É seguro rodar essas verificações contra o meu próprio projeto?

Sim. Todo comando aqui lê e nenhum escreve. A verificação de tabelas pede ao seu banco uma contagem de linhas e lê o número de um cabeçalho da resposta, então nenhuma linha é buscada. A de buckets pede uma listagem e para por aí, sem baixar arquivo nenhum. As duas últimas são uma requisição de página comum, do tipo que seus visitantes fazem o dia inteiro. Rodá-las contra um projeto que não é seu é outra questão, e a resposta é pedir permissão a quem é dono antes.

Minha chave anon está no meu bundle. Isso é um problema?

Não. A chave anon nos projetos antigos, e sb_publishable_ nos novos, foi feita para ficar no código que seus visitantes baixam. Ela nomeia o seu projeto e sozinha não concede nada; é a Row Level Security que decide quais linhas uma requisição recebe de volta. A chave que nunca pode estar ali é a secreta, chamada service_role nos projetos antigos e sb_secret_ nos novos, porque ela pula todas as regras que você escreveu.

O que significa content-range: */0?

É o seu banco dizendo que vai entregar zero linhas daquela tabela para quem está chamando. O asterisco quer dizer que a resposta não contém linha alguma, e o número depois da barra é a contagem que quem chamou tem permissão de ver. Então */0 é a resposta que você quer de uma tabela com dados pessoais, e 0-0/128 quer dizer que 128 linhas estão ao alcance de qualquer um que tenha a chave que viaja no seu app.

Preciso da minha chave service_role para testar isso?

Não, e um checker que pede essa chave está pedindo a coisa errada. Toda verificação aqui usa a chave publicável que já está no seu app, porque é essa que um estranho teria. Testar com uma chave secreta diz o que um administrador alcança, o que nunca esteve em dúvida. Não cole uma chave service_role nem sb_secret_ em nenhum scanner, incluindo o nosso: nada do que fazemos precisa dela.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Todos os artigos

Não sabe como está o seu próprio app?

Faça um scan gratuito e receba uma nota clara de A a F em cerca de 20 segundos. Sem conta e sem cartão.

Verificar meu app grátis

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.