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.

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
apikeycarrega 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.
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 aparece | O que significa |
|---|---|
content-range: */0 | Nada legível sem login. Essa tabela está fazendo o trabalho dela. |
content-range: 0-0/128 | 128 linhas ao alcance de qualquer um com a chave que viaja na sua página. |
401 ou 403 | A chave foi recusada antes de chegar à tabela. Sem resposta, não limpo. |
404 | Não existe tabela com esse nome. Ou você chutou, ou ela se escreve de outro jeito. |
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,ordersemessages. É 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.
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.