Quando pedes a uma IA que "ligue a tua app ao Stripe, à OpenAI ou ao Supabase", muitas vezes ela mete a chave diretamente no código do frontend. Funciona à primeira, e é por isso que ninguém revê. O problema é simples e absoluto: tudo o que chega ao browser é público. Não há forma de esconder um texto que o browser precisa de ler. Este guia explica porque acontece, como um atacante o encontra num minuto, que chaves são perigosas e quais não, como as procurares tu mesmo e o que fazer se uma já fugiu.
Porque é que a chave acaba no browser
Há três caminhos habituais. O primeiro é o prefixo público das variáveis de ambiente: no Next.js tudo o que começa por NEXT_PUBLIC_ é embutido no JavaScript do cliente, tal como VITE_ no Vite, NUXT_PUBLIC_ no Nuxt ou REACT_APP_ no Create React App. A IA vê que "a variável não chega" e acrescenta-lhe o prefixo para que chegue. O segundo é a chamada direta: o assistente escreve fetch('https://api.openai.com/...') num componente de interface com a chave no cabeçalho, porque é o exemplo mais curto que conhece. O terceiro é um ficheiro de constantes partilhado, importado pelo servidor e pelo cliente, onde alguém deixou a chave "temporariamente".
Nos três casos o resultado é o mesmo: a chave viaja num ficheiro .js que qualquer pessoa pode descarregar, e fica na cache dos CDN, no histórico do browser dos teus utilizadores e nos arquivos dos rastreadores que percorrem a internet à procura exatamente disso.
Como um atacante o vê em 60 segundos
- DevTools. Abre o teu site, carrega em F12, vai ao separador Sources, carrega em Ctrl+Shift+F para procurar em todos os ficheiros e escreve
sk_live,sk-ant,AKIA,service_roleou-----BEGIN. Se aparecer, ele já a tem. - Código-fonte. Clique direito, ver código-fonte, e abrir cada ficheiro
.jsligado. Os bundles vão minificados mas não cifrados; uma pesquisa de texto chega. - Source maps. Se publicas os
.map, além do código vê os nomes originais das variáveis e os comentários, incluindo os// TODO: mover para o servidor. - Scanners automáticos. Há bots que percorrem domínios novos a descarregar o JavaScript e a testar cada padrão de chave contra o fornecedor correspondente. Uma chave da AWS fugida num bundle é usada em minutos, não em dias.
Que chaves são perigosas e o que acontece se fugirem
- Stripe
sk_live_. Permite criar cobranças, reembolsos e ler dados de clientes. É dinheiro real. - OpenAI, Anthropic e semelhantes (
sk-…). Quem a tiver consome à tua conta. As faturas de milhares de euros por uma chave num repositório público são uma história que se repete. - AWS
AKIA…e Google Cloud. Consoante as permissões: buckets, bases de dados e criar máquinas para minerar à tua custa. - Supabase
service_role. Salta o Row Level Security por completo: leitura e escrita total da tua base de dados. É a mais grave de todas em apps vibe-coded. - Contas de serviço do Firebase (JSON). Equivalente ao
service_role: controlo total do projeto. - Fornecedores de email e SMS (Resend, SendGrid, Twilio). Enviar emails e mensagens em teu nome, ou seja, phishing com o teu domínio.
- Strings de ligação à base de dados com utilizador e password, e segredos de JWT: com o segredo assinam-se sessões de qualquer utilizador.
- Tokens de acesso guardados em
localStorage. Não são chaves tuas mas dos teus utilizadores, e qualquer XSS lê-os e rouba-os.
Que chaves podem ir para o cliente
Nem toda a chave é um segredo. Algumas foram desenhadas para viver no browser, desde que o que está por trás esteja bem configurado:
- A chave
anonoupublishabledo Supabase, protegida pelas políticas de RLS de cada tabela. - A chave pública do Stripe (
pk_live_), que só serve para iniciar pagamentos, não para os cobrar. - A configuração do Firebase (apiKey, projectId…), protegida pelas regras de segurança e, melhor ainda, pelo App Check.
- Uma chave do Google Maps restringida por domínio na consola da Google.
- Identificadores de analítica e de widgets de terceiros.
A regra para as distinguir: uma chave pode ir para o cliente se o fornecedor lhe chamar explicitamente pública e se, mesmo tendo-a, um desconhecido não conseguir fazer nada que tu não queiras. Se tens dúvidas, é secreta.
Como as procurares tu mesmo
No bundle já construído, procura padrões conhecidos. Com Next.js ou Vite, depois de construir:
grep -rEo "sk_live_[0-9a-zA-Z]+|sk-ant-[0-9a-zA-Z-]+|AKIA[0-9A-Z]{16}|service_role|eyJhbGciOi[0-9a-zA-Z._-]+" .next/static dist 2>/dev/null | head
No repositório, antes de chegar ao bundle, ferramentas como o gitleaks ou o trufflehog percorrem o histórico completo, não só o último commit. E no painel do teu alojamento, revê que variáveis têm prefixo público: cada uma delas é enviada para o browser.
Três sítios onde as pessoas não olham: os componentes marcados com 'use client' que importam um ficheiro de configuração partilhado; os parâmetros do URL, onde às vezes viaja um token que acaba em logs e no histórico; e os repositórios públicos no GitHub, onde a chave pode estar há meses mesmo que já não esteja no código atual.
Como corrigir: a chave fica no servidor
O padrão correto é o browser falar com o teu servidor e o teu servidor falar com o fornecedor. Um endpoint teu recebe o pedido, verifica a sessão do utilizador, aplica um limite de utilização e faz a chamada com a chave lida de uma variável de ambiente sem prefixo público. Assim, em Next.js:
// app/api/summarize/route.ts (só servidor)
import { getSession } from '@/lib/auth'
export async function POST(req: Request) {
const session = await getSession(req)
if (!session) return new Response('Unauthorized', { status: 401 })
const { text } = await req.json()
if (typeof text !== 'string' || text.length > 5000) {
return new Response('Bad request', { status: 400 })
}
const r = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: text }] }),
})
return new Response(r.body, { headers: { 'Content-Type': 'application/json' } })
}
Com o Supabase, a mesma ideia: as operações que precisam de service_role vão numa Edge Function ou no teu backend, nunca no cliente. Com o Firebase, numa Cloud Function. E em qualquer caso, a variável de ambiente chama-se OPENAI_API_KEY, não NEXT_PUBLIC_OPENAI_API_KEY.
Para os tokens de sessão, prefere cookies com HttpOnly a localStorage: uma cookie HttpOnly não é legível a partir do JavaScript, por isso um XSS não a consegue roubar. Se a tua biblioteca guarda a sessão em localStorage por defeito, como faz o cliente do Supabase no browser, compensa com uma CSP rigorosa ou usa a variante para servidor com cookies.
Se já fugiu: rodar, não apagar
Tirar a chave do código não serve de nada: já está em caches, no histórico do git e, provavelmente, nas mãos de alguém. A ordem correta é:
- Criar uma chave nova no fornecedor e publicá-la no servidor.
- Revogar a antiga logo a seguir. No Stripe, na OpenAI e na AWS faz-se no painel com um clique.
- Rever a utilização: faturação, logs da API e cobranças dos últimos dias. Se houve abuso, abre um pedido ao fornecedor; às vezes anulam os encargos.
- Pôr limites de gasto e alertas em cada fornecedor que o permita, para que da próxima vez doa menos.
- Se a chave dava acesso a dados pessoais de utilizadores, avalia se tens de o notificar ao abrigo do RGPD.
Limpar o histórico do git com ferramentas como o BFG ou o git filter-repo é boa higiene, mas não substitui a rotação.
Como o Scanaris o verifica
O Scanaris descarrega o JavaScript que o teu site serve, tal como faria um atacante, e procura padrões de chaves dos fornecedores habituais, tokens JWT, URLs de webhooks e strings de ligação. Também avisa se os teus source maps estão acessíveis e se há ficheiros sensíveis ao alcance. Cada resultado traz um prompt para colar na tua IA com a correção exata: mover a chamada para o servidor e rodar a chave. Se ativares a monitorização, avisa-te quando uma sessão nova de vibe coding voltar a meter uma chave no cliente. Faz parte da checklist de lançamento e do guia de segurança para apps criadas com IA.
Perguntas frequentes
A chave anon do Supabase é um segredo?
Não. Foi pensada para o browser e é protegida pelo RLS de cada tabela. O que é secreto é o service_role. Se o teu RLS está mal, o problema não é a chave anon mas as políticas.
E se a chave estiver restringida por domínio?
As restrições por referer (como no Google Maps) reduzem o abuso a partir de outros sites, mas não a partir de scripts que falsificam o cabeçalho. Servem para chaves de baixo risco, não para chaves que gastam dinheiro ou leem dados.
Basta apagá-la do repositório?
Não. Uma chave que esteve num bundle público ou num commit público está comprometida. Roda-a sempre, e depois limpa o histórico se quiseres.