Cómo funciona esta comprobación
Scanaris lee el JavaScript de tu página principal, incluidos los scripts que carga desde tu dominio, y busca llamadas a localStorage.setItem o sessionStorage.setItem cuya clave sugiera una credencial: token, jwt, auth, secret, password, api_key, access, refresh o bearer. Si encuentra una, abre un hallazgo de severidad baja. Es baja porque guardar un token en el navegador no es una brecha por sí misma; se convierte en una cuando se combina con cualquier inyección de script, y por eso merece la pena saberlo.
Por qué importa
Todo lo que hay en localStorage lo puede leer cualquier código que se ejecute en tu página: tus scripts, los de terceros que cargas (analítica, chats, widgets) y cualquier script que un atacante consiga inyectar. Una sola vulnerabilidad XSS, en tu código o en una dependencia, basta para que localStorage.getItem('token') viaje a un servidor ajeno y alguien use la sesión de tu usuario desde otro sitio, sin su contraseña y sin que lo note. Una cookie con HttpOnly es invisible para JavaScript, así que ese mismo XSS no puede robarla; puede hacer peticiones mientras la página está abierta, que es malo, pero no llevarse la sesión.
De dónde viene normalmente
De la configuración por defecto de muchas librerías de autenticación en aplicaciones de una sola página. El cliente de Supabase para el navegador guarda la sesión en localStorage si no le dices otra cosa, igual que muchos ejemplos de JWT "hecho a mano" con Express. Lovable, Bolt y v0 generan ese patrón porque es el que aparece en la documentación de inicio rápido.
Cómo arreglarlo
- Sesión en cookies HttpOnly. Si tienes servidor (Next.js, Nuxt, Express, una Edge Function), que sea él quien reciba el token del proveedor y lo guarde en una cookie con
HttpOnly,SecureySameSite=Lax. El navegador la envía sola y nunca la ve. - Con Supabase: en Next.js o Nuxt usa
@supabase/ssr, que guarda la sesión en cookies; en una SPA pura sin servidor, acepta el riesgo y compénsalo con una CSP estricta, porque no hay otro sitio donde guardarla. - Tokens cortos y refresco seguro. Si el token de acceso tiene que vivir en memoria o en storage, que caduque en minutos y que el de refresco vaya en una cookie
HttpOnly. - Memoria en lugar de storage. Guardar el token en una variable de JavaScript (no en storage) lo protege de scripts que lean storage, aunque no de un XSS activo en la misma página. Mejor que localStorage, peor que una cookie.
- Reduce la superficie de XSS. Una CSP sin
'unsafe-inline'enscript-src, sanear cualquier HTML de usuario y revisar los scripts de terceros que cargas. Es la defensa que hace que el token en storage no importe.
Comprueba que está arreglado
curl -s https://tudominio.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -3
curl -s https://tudominio.com/assets/index-XXXX.js | grep -oE "(local|session)Storage\.setItem\([^)]{0,60}" | head
Si el segundo comando devuelve líneas con token, auth o similares en la clave, el patrón sigue ahí. En las herramientas del navegador, pestaña Aplicación, puedes ver qué guarda tu app en storage tras iniciar sesión.
Preguntas frecuentes
Supabase guarda la sesión en localStorage por defecto. ¿Está mal?
Es el compromiso que asume una SPA sin servidor. Si tienes servidor, cambia a @supabase/ssr. Si no, cuida la CSP y los scripts de terceros: ese token es tan seguro como la página que lo rodea.
¿sessionStorage es más seguro que localStorage?
Solo en duración: se borra al cerrar la pestaña. Cualquier script de la página lo lee igual.
¿Por qué es severidad baja si puede robar la sesión?
Porque requiere otra vulnerabilidad (un XSS) para explotarse. El check te avisa para que decidas con información; si tu web ya tiene una CSP estricta y no muestra contenido de usuario sin sanear, el riesgo real es pequeño.