Como funciona esta verificação
O Scanaris carrega a tua página principal, recolhe os ficheiros JavaScript que ela liga a partir do teu próprio domínio e lê-os à procura de padrões de credenciais conhecidas: chaves secretas e restritas do Stripe (sk_live_, rk_live_), identificadores de acesso da AWS (AKIA…), chaves de API da Google (AIza…), chaves da OpenAI, tokens do GitHub (ghp_…) e chaves privadas em formato PEM. Além disso descodifica qualquer JWT que encontre e, se a carga tiver role: service_role, marca-o como chave de serviço do Supabase exposta, o que é crítico. As chaves públicas (pk_live_ do Stripe, a anon ou publishable do Supabase, a configuração do Firebase) são excluídas de propósito: foram desenhadas para ir no navegador.
O que um atacante faz com isto
Exatamente o mesmo que o scanner, mas em escala: há robôs que descarregam o JavaScript de domínios novos assim que aparecem nos registos de certificados e testam cada chave que encontram. Uma chave secreta do Stripe permite criar reembolsos ou ler clientes; uma da OpenAI, gastar a tua quota; uma service_role do Supabase, ler e apagar toda a tua base de dados saltando as políticas. Não é preciso "hackear" nada: o ficheiro é público por definição, cada visitante recebe-o.
Como é que uma chave acaba no bundle
Quase sempre por uma variável de ambiente com o prefixo público: NEXT_PUBLIC_ em Next.js, VITE_ em Vite, NUXT_PUBLIC_ em Nuxt. A framework embute essas variáveis no JavaScript no momento do build, que é precisamente o que queres para uma chave pública e precisamente o que não queres para uma secreta. O outro caminho habitual é colar a chave no chat do Lovable, do Bolt ou do Cursor para "ligar" um serviço: o modelo escreve-a no componente que precisa dela. Mais detalhe em segredos que a tua app deixa fugir no navegador.
Como corrigir
- Roda a chave primeiro. Dá-a por comprometida mesmo que a tires do código: os bundles anteriores continuam em caches e no histórico de deploys. Gera uma nova no painel do serviço e revoga a antiga.
- Move a chamada para o servidor. Em Next.js, para um Route Handler ou uma Server Action; em Nuxt, para uma rota de
server/api; com Supabase, para uma Edge Function; na Netlify, para uma função. O navegador chama o teu servidor e o servidor, com a chave, chama o serviço. - Tira o prefixo público à variável e guarda-a no painel do teu alojamento como variável de servidor. Se uma variável precisa do prefixo, é porque é pública: pergunta-te se devia ser.
- Revê o histórico do git. Se a chave alguma vez foi enviada para um repositório, está no histórico mesmo que o ficheiro atual esteja limpo.
Confirma que está limpo
curl -s https://oteudominio.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -5
curl -s https://oteudominio.com/assets/index-XXXX.js | grep -oE "sk_live_[A-Za-z0-9]+|rk_live_[A-Za-z0-9]+|AKIA[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{35}|ghp_[A-Za-z0-9]{36}|BEGIN (RSA |EC )?PRIVATE KEY" | head
O primeiro comando lista os teus scripts; o segundo procura os padrões num deles. Se não sair nada em nenhum, o bundle está limpo. Repete depois de cada deploy grande, porque uma regeneração com IA pode voltar a meter a chave.
Perguntas frequentes
A chave anon do Supabase ou a pk_live do Stripe são um problema?
Não. São públicas por desenho e o scanner não as marca. O que protege os teus dados com a anon é o RLS; o que protege o Stripe com a pk_live é que só serve para criar tokens de pagamento, não para cobrar.
Marca-me uma chave da Google que uso para o Maps. É grave?
As chaves AIza podem ser legitimamente públicas se estiverem restringidas por domínio e por API na consola do Google Cloud. Sem restrições, qualquer pessoa pode gastar a tua quota. Restringe-a e o risco real desaparece, mesmo que o padrão continue a aparecer.
Já a tirei do código e continua a aparecer.
O bundle publicado pode ser o antigo, ou a chave pode estar noutro ficheiro (um chunk partilhado, um script de terceiros alojado no teu domínio). Faz deploy de novo, volta a analisar e, em qualquer caso, roda a chave.