Todo lo que hay en tu bundle es público.
Escribe una URL y descargamos el JavaScript que tu página le dice al navegador que cargue, y luego lo leemos buscando credenciales. No es una suposición a partir del HTML: son los archivos de script reales, los mismos que cualquiera abre en devtools.
Qué comprueba esto, y cómo es una buena respuesta
Claves service_role de Supabase
Lo peor de esta lista. La clave service_role se salta cada política de row level security que hayas escrito, así que quien la tenga puede leer, cambiar y borrar cada fila de cada tabla sin importar con qué usuario haya entrado. Es idéntica a la vista a la clave publicable anon, y por eso acaba en el código del cliente: alguien se topó con un error de permisos, cambió una clave por la otra, el error desapareció, y eso se publicó.
Claves anon de Supabase y configuración de Firebase
Estas están pensadas para ser públicas, y encontrarlas no es un hallazgo por sí mismo. Las reportamos porque lo que valen depende por completo de tus reglas: una clave anon con row level security desactivado lee tu base de datos entera, y una configuración de Firebase con las reglas en allow read, write: if true hace lo mismo. La clave no es la vulnerabilidad. La clave más una política ausente sí lo es.
Claves secretas de Stripe
Una clave sk_live_ en código de cliente significa que cualquiera puede crear cargos, emitir reembolsos y leer tu lista de clientes. Marcamos también las de prueba, con menor severidad: una sk_test_ en el bundle no cuesta dinero, pero dice que la publicable y la secreta se mezclaron una vez, y el mismo error suele acabar llegando a producción.
Claves de acceso de AWS, tokens de GitHub y claves privadas
Un par de claves de AWS en un bundle es una factura y una brecha al mismo tiempo, y son las credenciales que más cazan los recolectores automáticos. Un token de GitHub expone el código y, si lleva permiso de escritura, la capacidad de hacer push. Un bloque BEGIN PRIVATE KEY en código de cliente no tiene ninguna razón legítima para estar ahí.
Claves de proveedores de modelos
Las claves de OpenAI, Anthropic y similares en código de cliente son la entrada más nueva de esta lista y cada vez la más común, porque llamar al modelo directamente desde el navegador es el camino más corto a una demo que funciona. También es una credencial medida que cualquiera puede gastar. Este es el fallo que se convierte en una factura de cuatro cifras de un día para otro en vez de en una brecha de datos.
Tokens de autorización escritos a mano
Un valor literal Authorization: Bearer ... horneado en el bundle, normalmente olvidado tras probar contra un endpoint real. Concede lo que conceda el token, a todo el mundo, mientras siga siendo válido.
Cadenas de conexión a la base de datos
Una URL postgres:// o mongodb+srv:// con la contraseña dentro, en un archivo que descarga cada visitante. Esta es la versión más directa de todo el problema: no tiene alcance de usuario, no hay ninguna política detrás, y quien la copie puede leer todas las tablas, cambiar todas las filas y tirarlo todo abajo. Nunca te devolvemos la contraseña, solo el host, para que sepas de qué base de datos se trata. Los ejemplos de tutorial como postgres://user:password@localhost se reconocen y se descartan, y por eso documentar tu configuración en tu propio sitio no enciende esta alarma.
Slack, SendGrid, Twilio, npm, Resend, Mapbox y Algolia
La cola larga, y la razón por la que un escáner que solo conoce las cinco famosas deja escapar tanto. Un token de bot de Slack lee tus canales privados. Una clave de SendGrid o de Resend envía correo con la reputación de tu dominio detrás, y por eso las filtradas acaban en phishing en cuestión de horas. Un token de publicación de npm puede mandar código a todo el que te instale. Dos de estas se gradúan a propósito por debajo: una URL de webhook de Slack solo escribe en un canal, y un identificador de cuenta de Twilio por sí solo no abre nada, así que ninguna se disfraza de brecha. La comprobación de Algolia está escrita como pregunta y no como veredicto, porque la clave de búsqueda y la de administración son cadenas idénticas y distinguirlas exigiría usar la clave, algo que un análisis pasivo no hace.
Cómo corregirlo
Solo hay una corrección de verdad y tiene dos mitades: rota la credencial, porque hay que dar por hecho que todo lo que llegó a un navegador está ya en manos de otra persona, y después mueve al servidor la llamada que la necesitaba. La rotación no es opcional ni algo que dejar para luego. Las claves en bundles públicos las encuentran escáneres automáticos en horas.
# 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=...// 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());
}// 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 });
});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.Esto lee los scripts a los que enlaza tu página, es decir, ve lo que ve el navegador de un visitante y nada más. Una clave que solo aparece en una ruta que no cargamos, en un bundle tras un login, o en tu historial de git en vez de en tu build actual, es invisible aquí e igual de peligrosa. El historial de git en particular es donde vive la mayoría de las claves filtradas: quitar un secreto de un archivo no lo quita del repositorio, y ese es uno de los primeros sitios donde mira un análisis del propio código.