Cómo funciona esta comprobación
Scanaris carga tu página principal, recoge los archivos JavaScript que enlaza desde tu propio dominio y los lee buscando patrones de credenciales conocidas: claves secretas y restringidas de Stripe (sk_live_, rk_live_), identificadores de acceso de AWS (AKIA…), claves de API de Google (AIza…), claves de OpenAI, tokens de GitHub (ghp_…) y claves privadas en formato PEM. Además decodifica cualquier JWT que encuentre y, si su carga lleva role: service_role, lo marca como clave de servicio de Supabase expuesta, que es crítico. Las claves públicas (pk_live_ de Stripe, la anon o publishable de Supabase, la configuración de Firebase) se excluyen a propósito: están diseñadas para ir en el navegador.
Qué hace un atacante con esto
Exactamente lo mismo que el escáner, pero a escala: hay robots que descargan el JavaScript de dominios nuevos en cuanto aparecen en los registros de certificados y prueban cada clave que encuentran. Una clave secreta de Stripe permite crear reembolsos o leer clientes; una de OpenAI, gastar tu cuota; una service_role de Supabase, leer y borrar toda tu base de datos saltándose las políticas. No hace falta "hackear" nada: el archivo es público por definición, lo recibe cada visitante.
Por qué acaba una clave en el bundle
Casi siempre por una variable de entorno con el prefijo público: NEXT_PUBLIC_ en Next.js, VITE_ en Vite, NUXT_PUBLIC_ en Nuxt. El framework incrusta esas variables en el JavaScript en el momento del build, que es justo lo que quieres para una clave pública y justo lo que no quieres para una secreta. El otro camino habitual es pegar la clave en el chat de Lovable, Bolt o Cursor para "conectar" un servicio: el modelo la escribe en el componente que la necesita. Más detalle en secretos que tu app filtra en el navegador.
Cómo arreglarlo
- Rota la clave primero. Dala por comprometida aunque la quites del código: los bundles anteriores siguen en cachés y en el historial de despliegues. Genera una nueva en el panel del servicio y revoca la antigua.
- Mueve la llamada al servidor. En Next.js, a un Route Handler o una Server Action; en Nuxt, a una ruta de
server/api; con Supabase, a una Edge Function; en Netlify, a una función. El navegador llama a tu servidor y el servidor, con la clave, al servicio. - Quita el prefijo público a la variable y guárdala en el panel de tu hosting como variable de servidor. Si una variable necesita el prefijo, es que es pública: pregúntate si debería serlo.
- Revisa el historial de git. Si la clave se subió alguna vez a un repositorio, está en el historial aunque el archivo actual esté limpio.
Comprueba que está limpio
curl -s https://tudominio.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -5
curl -s https://tudominio.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
El primer comando lista tus scripts; el segundo busca los patrones en uno de ellos. Si no sale nada en ninguno, el bundle está limpio. Repite tras cada despliegue grande, porque una regeneración con IA puede volver a meter la clave.
Preguntas frecuentes
¿La clave anon de Supabase o la pk_live de Stripe son un problema?
No. Son públicas por diseño y el escáner no las marca. Lo que protege tus datos con la anon es RLS; lo que protege Stripe con la pk_live es que solo sirve para crear tokens de pago, no para cobrar.
Me marca una clave de Google que uso para Maps. ¿Es grave?
Las claves AIza pueden ser legítimamente públicas si están restringidas por dominio y por API en la consola de Google Cloud. Sin restricciones, cualquiera puede gastar tu cuota. Restríngela y el riesgo real desaparece, aunque el patrón siga apareciendo.
Ya la quité del código y sigue saliendo.
El bundle desplegado puede ser el antiguo, o la clave puede estar en otro archivo (un chunk compartido, un script de terceros alojado en tu dominio). Despliega de nuevo, vuelve a escanear y, en cualquier caso, rota la clave.