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.
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.
As duas famílias de chave
| Provedor | Chave | Segura no navegador? | O que ela faz |
|---|---|---|---|
| Supabase | sb_publishable_…, ou anon em projetos antigos | É o lugar dela | Diz de qual projeto se trata. Todo pedido continua filtrado pelas suas regras de Row Level Security. |
| Supabase | sb_secret_…, ou service_role em projetos antigos | Nunca | Ignora o Row Level Security por completo. Lê e escreve em toda linha de toda tabela, independentemente das suas regras. |
| Stripe | pk_live_… (publicável) | É o lugar dela | Cria formulários de pagamento. Não move dinheiro nem lê clientes. |
| Stripe | sk_live_… (secreta) | Nunca | Acesso total à conta: cobranças, reembolsos, repasses, cadastros de clientes. |
| OpenAI / Anthropic | qualquer chave | Nunca | Nã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
}
"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
roledentro 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_…ouanon) 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ê.