← Voltar ao blog
SegurançaScanaris Team · 7 min de leitura · October 04, 2026

Os segredos que a tua app com Cursor ou Lovable está a vazar no navegador

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_role ou -----BEGIN. Se aparecer, ele já a tem.
  • Código-fonte. Clique direito, ver código-fonte, e abrir cada ficheiro .js ligado. 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 anon ou publishable do 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.

Como está o teu site?

Passa-lhe o Scanaris e verifica em 30 segundos, grátis.

Analisar o meu site

Verificações relacionadas

Guias de segurança por stack