Pular para o conteúdo

Noções de segurança

Quais chaves de API são seguras no navegador e quais não são

Sua chave anon do Supabase foi feita para ser pública. Sua chave service_role não, e ela ignora todas as regras que você criar. Como diferenciar as duas.

Vlad Tkachenko7 min de leitura

Em resumo

  • Encontrar uma chave de API no código do seu app não é automaticamente um problema. Algumas chaves foram feitas para estar ali.
  • Chaves publicáveis são seguras no navegador. Chaves secretas não: uma chave secreta no navegador é um caixa aberto.
  • Duas conferências separam as duas em menos de um minuto: ler o prefixo e, numa chave antiga do Supabase, decodificar o papel.

Quem criou o app com Lovable, Bolt, v0, Cursor ou Replit uma hora passa por isto: alguém abre o site, aperta F12 e avisa que a sua chave de API está "exposta". É uma notícia assustadora sobre um app cujo código você não consegue ler.

Aqui está o que guia após guia erra: algumas dessas chaves deveriam mesmo estar ali. Tratar toda chave visível como um vazamento leva ou a um susto sem motivo ou, bem pior, a aprender a ignorar o aviso, e a ignorá-lo também no dia em que ele importa.

É ruim que minha chave de API esteja visível?

Normalmente não. Depende inteiramente de qual chave é.

Todo serviço que conversa com um navegador entrega dois tipos diferentes de credencial. Um é um endereço. O outro é o molho de chaves do prédio. Os dois se chamam "chave de API", e é daí que vem quase toda a confusão.

Um endereço pode ser publicado sem risco. Ele só diz a qual projeto um pedido pertence; a checagem de permissão acontece em outro lugar. Um molho de chaves não pode, porque ele é a checagem de permissão: quem o tiver faz tudo o que ele permite, de qualquer lugar.

Seu app precisa do endereço no navegador para funcionar. Ele nunca deveria precisar do molho de chaves ali.

Por que seu app manda chaves para o navegador

Porque o pedido sai do navegador do seu visitante, não do seu servidor.

Quando seu app carrega a lista de pedidos dos seus usuários, esse pedido sai direto do navegador do visitante para o seu provedor de banco de dados. Ele precisa dizer a qual projeto pertence, e esse identificador precisa estar na página, porque é de lá que o pedido é feito.

Não existe versão disso em que o identificador seja secreto. Ele chega a todo visitante por projeto. E é exatamente por isso que os provedores dividem as credenciais em duas: eles sabem que uma delas vai ser pública, então fizeram essa ser inofensiva.

A segurança não vem de esconder o endereço. Vem das regras que você define do outro lado: no caso do Supabase, o Row Level Security, que decide linha a linha quem pode ver o quê. São essas regras que valem a conferida, e ligá-las não é a mesma coisa que estar protegido.

É o Row Level Security que torna a chave anon segura. Uma chave service_role passa direto por ele.

As duas famílias de chave

ProvedorChaveSegura no navegador?O que ela faz
Supabasesb_publishable_…, ou anon em projetos antigosÉ o lugar delaDiz de qual projeto se trata. Todo pedido continua filtrado pelas suas regras de Row Level Security.
Supabasesb_secret_…, ou service_role em projetos antigosNuncaIgnora o Row Level Security por completo. Lê e escreve em toda linha de toda tabela, independentemente das suas regras.
Stripepk_live_… (publicável)É o lugar delaCria formulários de pagamento. Não move dinheiro nem lê clientes.
Stripesk_live_… (secreta)NuncaAcesso total à conta: cobranças, reembolsos, repasses, cadastros de clientes.
OpenAI / Anthropicqualquer chaveNuncaNão existe versão publicável. Toda chave cobra direto da sua conta.

O padrão vale além desses três. Se um provedor oferece só um tipo de chave, assuma que ela é secreta e que o lugar dela é um servidor.

Como diferenciar em 60 segundos

No Stripe, e na maioria dos provedores, basta o prefixo. pk_ é publicável e segura. sk_ é secreta e não é. Alguns provedores colocam _test_ ou _live_ no meio: uma chave de teste vazada é um problema bem menor do que uma de produção, mas troque as duas.

No Supabase depende de quão antigo é o seu projeto. O Supabase já emitiu dois formatos diferentes de chave, e os dois estão hoje em apps no ar.

Projetos novos: leia o prefixo, igual ao Stripe. sb_publishable_… é a que tem lugar no seu app. sb_secret_… é a que se troca hoje. Não há nada a decodificar: para que serve a chave está escrito na frente dela. O que mudou e o que isso significa para o seu app, caso você ainda não tenha esbarrado nelas.

Projetos antigos: é preciso olhar dentro da chave. O par original, anon e service_role, são JWTs: três blocos de rabisco separados por pontos, e o do meio é informação legível, não criptografia. Ali está o papel da chave em texto claro.

Em nenhum dos casos você precisa de ferramenta. Abra Settings → API Keys no painel do Supabase e ele diz qual é qual. Se preferir conferir a chave que você realmente achou no seu app, a parte do meio de uma chave antiga decodifica em algo assim.

{
  "iss": "supabase",
  "ref": "abcdefghij…",
  "role": "anon",          ← é esta palavra que importa
  "iat": 1750000000
}
Numa chave antiga a seção do meio é texto legível, não criptografia. Uma palavra ali decide se a chave podia ser publicada.

"role": "anon" é a segura. "role": "service_role" é a que se troca hoje. Numa chave antiga essa única palavra é toda a diferença, e é por isso que um scanner que só procura textos com cara de chave devolve ruído em vez de resposta: ele não consegue dizer qual de duas chaves idênticas você publicou.

Se você prefere não vasculhar seu app chave por chave, nosso scan gratuito lê seu site no ar e diz quais dessas dá para ver de fora. Leva uns 20 segundos e não precisa de conta: escaneie seu app.

O que acontece de fato quando uma chave secreta vaza

Toda linha do seu banco de dados passa a poder ser lida e alterada por quem encontrou a chave, incluindo tabelas que você nunca expôs ao app e os dados pessoais dos seus usuários. O Row Level Security não se aplica a uma chave secreta do Supabase (sb_secret_…, ou service_role num projeto antigo). Esse é o propósito da chave.

O estrago também não é teórico. Normalmente ele aparece como uma mensagem de suporte sobre dados que mudaram sozinhos, ou como uma tabela que de repente está vazia.

Uma chave sk_live_ do Stripe vazada significa reembolsos, cobranças e cadastros de clientes. Uma chave vazada de um provedor de IA significa uma fatura, às vezes bem alta, chegando antes de alguém perceber.

Nada disso exige um atacante sofisticado. Há scanners automáticos varrendo sites públicos atrás exatamente desses textos, e eles não precisam saber quem você é para achar a sua.

Se quiser a versão específica da plataforma sobre o que conferir, temos um guia em linguagem simples para apps com Supabase.

O que fazer agora

O que fazer

  • Ache cada chave do seu app e identifique cada uma. O prefixo no Stripe e nas chaves novas do Supabase; o campo role dentro das chaves antigas do Supabase.
  • Se achar uma chave secreta no navegador, troque primeiro. Apagar do código não fecha a porta: o valor antigo continua no seu histórico de versões e em cópias em cache do seu site.
  • Mova para um servidor o que precisava daquela chave: uma edge function, uma rota serverless, qualquer coisa que não seja o navegador.
  • Ligue o Row Level Security em todas as tabelas e depois confira se ele realmente funciona. Uma chave publicável (sb_publishable_… ou anon) só é segura por causa dessas regras; sem elas, ela lê o seu banco inteiro.
  • Confira cobrança e registros depois de qualquer exposição de chave secreta. Trocar a chave impede o que vem depois, não o que já aconteceu.

Se preferir ir por uma lista, o checklist de segurança de 10 minutos cobre isso e as outras coisas que vale desligar num app recém-lançado.

E se um vazamento chegar a esvaziar uma tabela, recuperá-la depende inteiramente de o que você estava copiando.

Perguntas frequentes

Disseram que minha chave de API está exposta. Devo me preocupar?

Não até saber de qual chave se trata. Se começa com pk_ ou é uma chave anon do Supabase, ela foi feita para ser pública e está tudo certo. Se começa com sk_ ou é uma chave service_role, gere uma nova agora e depois veja o que ela conseguia alcançar.

Posso simplesmente esconder a chave para ninguém achar?

Não. Tudo o que o seu navegador consegue usar, um visitante consegue ler: minificar, renomear ou ofuscar só atrasa alguém por alguns segundos. A solução nunca é esconder uma chave secreta no navegador, e sim movê-la para um servidor ou usar uma chave publicável que já era segura de expor desde o início.

Por que o Supabase me dá uma chave que qualquer um pode ler?

Porque não é a chave anon que protege seus dados. Ela só diz com qual projeto você está falando. A proteção de verdade é o Row Level Security, que decide linha a linha o que cada visitante pode ver. Por isso uma chave anon com RLS ligado não é problema, e a mesma chave sem RLS é um banco de dados aberto.

Minha chave do Supabase começa com sb_publishable_. É a mesma coisa que a chave anon?

Ela faz o mesmo trabalho. O Supabase renomeou suas chaves: sb_publishable_ substituiu a chave anon e sb_secret_ substituiu a service_role. Então uma chave que começa com sb_publishable_ foi feita para viver no seu app, e uma que começa com sb_secret_ nunca foi. Projetos antigos continuam carregando as chaves anon e service_role originais, e elas ainda funcionam; você pode até ver os dois pares lado a lado no painel.

Eu já colei uma chave secreta no meu app. E agora?

Gere uma nova primeiro: no painel do provedor, crie uma chave nova e revogue a antiga. É isso que fecha a porta de verdade; tirar do código não basta, porque o valor antigo continua no seu histórico de versões e em cópias em cache. Depois mova para um servidor o que precisava daquela chave e confira a cobrança e os registros à procura de algo que não foi você.

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

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.