Volver al blog
28 de julio de 2026

Nuestra propia aplicación guardaba los tokens de autenticación en localStorage. Esto es lo que cambiamos.

Viktor Bulanek
Founder & CTO, Penetrify
MSc IT Security · 20+ years in security · 4x Ex-CTO

En junio apuntamos Penetrify a nuestra propia aplicación, como hacemos antes de una release grande. El escaneo volvió con un problema en nuestro flujo de inicio de sesión con Cognito, y al tirar de ese hilo se convirtió en algo que no me gustó leer: el panel de Penetrify guardaba los tokens de acceso y de refresco de Cognito en localStorage, en texto plano, legibles por cualquier JavaScript que se ejecutara en la página.

No nos sentamos a decidir eso. Es el valor por defecto de la librería de autenticación que usamos, y nunca lo cuestionamos. Esa es justamente la parte que merece un artículo.

Qué era realmente el hallazgo

Nuestro panel es una aplicación single-page. Al iniciar sesión recibía los tokens de AWS Cognito y el SDK los ponía en el almacenamiento del navegador, que es donde los ponen los SDK cuando ningún servidor participa en la sesión. Cualquier script con acceso a la página podía leerlos.

Eso significa que un solo fallo de cross-site scripting en cualquier parte de la aplicación, una sola extensión maliciosa del navegador o una sola dependencia npm comprometida, y un atacante se va con los dos tokens. El token de acceso lo mete en la cuenta. El de refresco lo mantiene dentro, porque sigue siendo válido mucho después de que la sesión parezca cerrada, hasta que algo lo revoque explícitamente.

Para nuestros usuarios, el alcance incluía datos de facturación, resultados de escaneo de sus aplicaciones y datos de pagos de afiliados. Por lo que podemos juzgar, nadie lo explotó. No es el punto. Era alcanzable, y era nuestro propio valor por defecto el que había que arreglar.

Por qué cifrar localStorage no es la solución

El primer instinto es cifrar los tokens antes de guardarlos. No ayuda. La clave de descifrado tiene que vivir en el mismo JavaScript que el atacante ya está ejecutando. Has añadido un paso, no una frontera.

El segundo instinto es mover los tokens a una cookie desde el cliente. Tampoco ayuda. Una cookie puesta por JavaScript es legible por JavaScript. La propiedad que importa es HttpOnly, y solo un servidor puede establecerla, mediante una cabecera Set-Cookie. Si tu arreglo corre en el navegador, no es un arreglo.

Qué desplegamos en su lugar

Movimos toda la sesión a cookies puestas por el servidor, con un patrón token handler dentro del backend que ya teníamos:

  • El backend FastAPI hace las llamadas a Cognito y devuelve la sesión como cookies con HttpOnly, Secure y SameSite. El navegador nunca ve un token.
  • El middleware de autenticación lee el token de acceso de la cookie en lugar de una cabecera Authorization.
  • Como las cookies se envían automáticamente, las peticiones que modifican estado llevan ahora una cabecera con token CSRF, que el backend verifica.
  • El cierre de sesión hace un global sign-out en el lado de Cognito, así un token que se filtró antes muere con la sesión en vez de sobrevivirla.
  • El SDK de autenticación ha desaparecido del navegador por completo.

No necesitábamos un servicio nuevo para esto. Nuestra aplicación estática y nuestra API ya son del mismo origen: CloudFront sirve la aplicación y enruta /api/* a API Gateway. Una cookie first-party se envía por tanto en cada llamada a la API sin infraestructura adicional.

Primero miramos el camino recomendado por el proveedor, que habría significado adoptar una interfaz de login alojada, y lo descartamos. Habría tirado nuestras propias pantallas de login, MFA y restablecimiento de contraseña para resolver un problema que sabemos resolver en el backend que ya tenemos.

Qué costó

Una semana de trabajo, más o menos, y ningún cambio visible para los usuarios. Ese es el resumen honesto de casi todo el trabajo de seguridad, y es exactamente por lo que esto se aplaza indefinidamente en una startup. Nada se mueve en la hoja de ruta. Ningún cliente lo pide. Lo único que cambia es lo que un único script inyectado puede hacerles a tus usuarios.

Un detalle práctico que hizo seguro el despliegue: durante el rollout el backend seguía aceptando la vía antigua por cabecera, así que revertir el despliegue del frontend era un rollback de un solo paso si algo salía mal.

Comprueba tu propia aplicación. Lleva unos 30 segundos.

Abre tu aplicación, inicia sesión y abre las DevTools:

  • Pestaña Application, Local Storage. Busca valores largos con dos puntos dentro. Esa forma es un JWT.
  • Comprueba Session Storage en el mismo panel. El mismo problema, con vida más corta.
  • En la consola escribe document.cookie. Todo lo que aparezca ahí es por definición legible por JavaScript, así que una cookie de sesión que veas en esa salida no te está protegiendo.

Si encuentras un token, eso es un hallazgo. No teórico, y no algo que te habría contado una auditoría de dependencias en verde o un análisis estático limpio.

Por qué publicamos esto

Dos razones, y la segunda va sobre nuestro propio producto.

Primera: los valores por defecto son decisiones que alguien tomó por ti. Llegan con una librería, son cómodos y casi nunca son un modelo de amenazas. El nuestro era un defecto razonable para un tutorial y malo para una aplicación que guarda datos de pago.

Segunda: un escaneo es una señal, no un veredicto. El nuestro marcó el flujo de inicio de sesión y demostró que era alcanzable. No nos entregó la frase «tu librería de autenticación usa localStorage por defecto y eso es lo que hay que cambiar». Ir de la señal al arreglo significó abrir nuestra propia ruta de autenticación y leerla. Así esperamos que uses también nuestros informes: el hallazgo te dice dónde mirar y muestra la petición que lo prueba, y luego tú decides cuál es el arreglo correcto en tu arquitectura.

Así que: ejecuta el escaneo en tu propia aplicación y luego lee el código al que apunta. Hicimos las dos cosas, y ninguna de las dos mitades habría bastado sola.

Si quieres la vista de fuera hacia dentro de tu aplicación ahora mismo, nuestro chequeo de seguridad gratuito lleva unos 60 segundos y no pide registro. Si quieres el informe completo con pruebas y arreglos, el primer escaneo cuesta $29 y se acredita a tu plan. Y si prefieres la cosa gratis que acabamos de describir, abre las DevTools y mira tu propio local storage. Esa no cuesta nada.

Frequently Asked Questions

¿Qué tipos de vulnerabilidades detecta Penetrify?

Penetrify detecta todas las categorías de vulnerabilidades del OWASP Top 10, incluyendo inyección SQL, XSS, CSRF, IDOR, autenticación rota, configuraciones de seguridad incorrectas y exposición de datos sensibles. También prueba la seguridad de APIs, la gestión de sesiones y configuraciones incorrectas comunes en Supabase, Firebase y Bubble.

¿Cuánto tiempo dura un test de penetración con IA?

Un escaneo rápido se completa en 15–30 minutos. Un escaneo estándar dura 1–2 horas con mayor cobertura. Un escaneo profundo puede durar varias horas en aplicaciones complejas.

¿Qué incluye un informe de Penetrify?

Cada informe incluye un resumen ejecutivo, una puntuación general de seguridad, hallazgos clasificados por severidad (Crítico, Alto, Medio, Bajo), pasos de reproducción detallados y orientación concreta de remediación escrita para desarrolladores, no para responsables de cumplimiento.

Related articles

Alternativas DAST en 2026: Cuando el escaneo dinámico no es suficiente (y qué usar en su lugar)
Los escáneres DAST pasan por alto los flujos de autenticación, la lógica de negocio y las API modernas. A continuación, una comparación honesta de DAST frente a SAST, IAST, PTaaS y el Penetration Testing autónomo con IA, y cuándo usar cada uno.
What an autonomous pentest agent found in 3,847 apps — and what your scanner didn't
A data breakdown of 47,291 exploitation-validated findings, with methodology and limitations. 91% of the SQL injection we found shipped despite a SAST gate in CI; 78% of critical findings needed no login.
Indirect Prompt Injection to Data Exfiltration: When the Model Has Tools
Prompt injection on its own is a curiosity. Prompt injection reaching a tool that holds real credentials is a breach — and the instruction does not have to come from your user. It can arrive inside the document your app was asked to summarise.

Explore more