Scanner de chaves de API expostas

Tudo no seu bundle é público.

Digite uma URL e buscamos o JavaScript que sua página manda o navegador carregar, e então lemos atrás de credenciais. Não é um chute a partir do HTML: são os arquivos de script de verdade, os mesmos que qualquer um abre no devtools.

Cheque sites que você possui ou tem autorização para testar. Pedimos a página como um navegador pediria e lemos o que volta. Veja nossos termos.

O que isso verifica, e como é uma boa resposta

Chaves service_role do Supabase

A pior coisa desta lista. A chave service_role ignora toda política de row level security que você escreveu, então quem a tiver lê, altera e apaga cada linha de cada tabela, não importa com qual usuário esteja logado. Ela é visualmente idêntica à chave publicável anon, e é por isso que ela vai parar no código do cliente: alguém bateu num erro de permissão, trocou uma chave pela outra, o erro sumiu, e aquilo foi para produção.

Chaves anon do Supabase e config do Firebase

Essas são feitas para ser públicas, e encontrá-las não é um achado por si só. Reportamos porque o que elas valem depende inteiramente das suas regras: uma chave anon com row level security desligado lê seu banco inteiro, e uma config do Firebase com as regras em allow read, write: if true faz o mesmo. A chave não é a vulnerabilidade. A chave mais uma política ausente é.

Chaves secretas da Stripe

Uma chave sk_live_ em código de cliente significa que qualquer um cria cobranças, emite reembolsos e lê sua lista de clientes. Sinalizamos chaves de teste também, com severidade menor: uma sk_test_ no bundle não custa dinheiro, mas diz que a publicável e a secreta se confundiram uma vez, e o mesmo erro tende a chegar em produção mais cedo ou mais tarde.

Chaves de acesso da AWS, tokens do GitHub e chaves privadas

Um par de chaves da AWS num bundle é uma fatura e um vazamento ao mesmo tempo, e essas são as credenciais que os coletores automáticos mais caçam. Um token do GitHub expõe o código e, se tiver escopo de escrita, a capacidade de dar push. Um bloco BEGIN PRIVATE KEY em código de cliente não tem motivo legítimo nenhum para estar ali.

Chaves de provedores de modelo

Chaves da OpenAI, da Anthropic e parecidas em código de cliente são a entrada mais nova desta lista e cada vez mais a mais comum, porque chamar o modelo direto do navegador é o caminho mais curto para uma demo funcionando. Também é uma credencial medida que qualquer um pode gastar. Essa é a falha que vira uma conta de quatro dígitos da noite para o dia em vez de um vazamento de dados.

Tokens de autorização fixos no código

Um valor literal Authorization: Bearer ... gravado no bundle, em geral esquecido depois de um teste contra um endpoint real. Ele concede o que quer que o token conceda, para todo mundo, enquanto continuar válido.

Strings de conexão de banco

Uma URL postgres:// ou mongodb+srv:// com a senha dentro, parada em um arquivo que todo visitante baixa. Esta é a versão mais direta do problema inteiro: não tem escopo de usuário, não tem política nenhuma por trás, e quem copiar consegue ler todas as tabelas, alterar todas as linhas e derrubar tudo. Nunca devolvemos a senha para você, só o host, para você saber de qual banco se trata. Exemplos de tutorial como postgres://user:password@localhost são reconhecidos e ignorados, e é por isso que documentar a sua configuração no seu próprio site não acende este alerta.

Slack, SendGrid, Twilio, npm, Resend, Mapbox e Algolia

A cauda longa, e a razão pela qual um scanner que só conhece as cinco famosas deixa tanta coisa passar. Um token de bot do Slack lê os seus canais privados. Uma chave do SendGrid ou do Resend envia e-mail carregando a reputação do seu domínio, e é por isso que as vazadas viram phishing em horas. Um token de publicação do npm consegue enviar código para todo mundo que instala você. Duas destas são classificadas de propósito como menos graves: uma URL de webhook do Slack só escreve em um canal, e um identificador de conta do Twilio sozinho não abre nada, então nenhuma das duas é vestida de incidente. A verificação do Algolia é escrita como pergunta e não como veredito, porque a chave de busca e a de admin são strings idênticas e diferenciá-las exigiria usar a chave, coisa que um scan passivo não faz.

Como corrigir

Só existe uma correção de verdade e ela tem duas metades: rotacione a credencial, porque tudo que chegou a um navegador tem que ser tratado como se já estivesse nas mãos de outra pessoa, e então mova a chamada que precisava dela para o servidor. A rotação não é opcional nem coisa para deixar para depois. Chaves em bundles públicos são encontradas por scanners automáticos em horas.

Os nomes de variável que vazam
# Anything with these prefixes is INLINED INTO THE BUNDLE at build
# time. Not read at runtime, not kept on the server: pasted into a
# file anyone can download. A secret here is a published secret.

NEXT_PUBLIC_*      # Next.js
VITE_*             # Vite, so also most Lovable and Bolt output
PUBLIC_*           # Astro, SvelteKit
REACT_APP_*        # Create React App
EXPO_PUBLIC_*      # Expo

# Server-only names have no prefix, and your host's secret store is
# where they belong:
SUPABASE_SERVICE_ROLE_KEY=...
STRIPE_SECRET_KEY=...
OPENAI_API_KEY=...
Mova a chamada para o servidor (Next.js)
// app/api/summarise/route.ts
// The key stays on the server. The browser calls YOUR route, which
// is also the only place you can enforce who is allowed to ask.
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const session = await auth();
  if (!session) return NextResponse.json({ error: "sign in" }, { status: 401 });

  const answer = await fetch("https://api.openai.com/v1/responses", {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({ model: "gpt-5.4-mini", input: await request.text() }),
  });

  return NextResponse.json(await answer.json());
}
Mova a chamada para o servidor (Supabase Edge Function)
// supabase/functions/admin-task/index.ts
// service_role belongs here and nowhere else. Deno.env reads the
// function's own secrets, which are never bundled to a client.
import { createClient } from "jsr:@supabase/supabase-js";

const admin = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);

Deno.serve(async (request) => {
  const token = request.headers.get("authorization");
  if (!token) return new Response("unauthorized", { status: 401 });
  // Check who is asking BEFORE using a key that ignores every policy.
  const { data: user } = await admin.auth.getUser(token.replace("Bearer ", ""));
  if (!user) return new Response("unauthorized", { status: 401 });

  return Response.json({ ok: true });
});
Rotacionar, na ordem
1. Issue the new credential first, so nothing goes down.
2. Update your host's secret store and redeploy.
3. Revoke the old one. This is the step that actually helps, and
   the one people skip because everything already works without it.
4. Check the provider's audit log for use you cannot account for.
5. Search your git HISTORY, not just your files:
     git log -p -S 'sk_live_' -- . | head -50
   Deleting a key from a file leaves it in every clone of the repo.
O que esta ferramenta não consegue ver

Isto lê os scripts aos quais sua página se liga, ou seja, vê o que o navegador de um visitante vê e nada além. Uma chave que só aparece numa rota que não carregamos, num bundle atrás de login, ou no seu histórico do git em vez do build atual, é invisível aqui e igualmente perigosa. O histórico do git em particular é onde a maioria das chaves vazadas realmente mora: remover um segredo de um arquivo não o remove do repositório, e esse é um dos primeiros lugares em que um scan do próprio código vai olhar.

Analisar o código tambémGrátis para começar. Conecte um repositório e o primeiro scan roda em cerca de dois minutos.