Buenas prácticas de seguridad JWT: ataques, defensas y una checklist para 2026
Los JSON Web Tokens sostienen casi toda la autenticación moderna y, aun así, las buenas prácticas que los mantienen a salvo se ignoran más a menudo de lo que debería. JWT es el formato de credencial de facto para OAuth 2.0, OpenID Connect y las llamadas entre servicios dentro de los microservicios. También aporta su cuota de CVEs cada año, y casi todos se remontan a los mismos errores evitables: aceptar tokens sin firmar, fiarse del algoritmo que eligió el atacante, usar un secreto de firma débil u omitir la validación de los claims.
Un JWT es seguro cuando se cumplen cuatro condiciones a la vez. La firma está intacta, el atacante no puede cambiar el algoritmo, los claims se comprueban de verdad y el token se guarda en un lugar del que no se pueda robar con facilidad. Rompe cualquiera de ellas y lo que te queda es un bypass de autenticación. Abajo están los tres ataques que más importan y, después, las defensas: elegir y fijar un algoritmo, gestionar las claves, validar los claims y almacenar los tokens. Al final hay una checklist que puedes pegar en una revisión.
Cómo te protege realmente una firma JWT (y qué no protege)
Antes de que cualquier ataque tenga sentido, necesitas tener claro un hecho: un JWT está codificado, no cifrado. Un token firmado tiene tres segmentos en Base64URL unidos por puntos: header.payload.signature. La cabecera y el payload son JSON en Base64URL, sin más. Cualquiera que tenga el token puede leer todos sus claims. Pega cualquier token en nuestro decodificador JWT y verás la cabecera y el payload desplegados en JSON legible, sin necesidad de clave alguna. El payload es público por diseño.
¿De dónde sale entonces la seguridad? De la firma, y solo de la firma. Es un valor criptográfico calculado sobre la cabecera y el payload con un secreto (HMAC) o una clave privada (RSA, ECDSA). Un atacante puede leer un token sin problemas, pero no puede producir un token distinto que supere la verificación sin la clave de firma. En esa propiedad se apoya todo el modelo de confianza.
De ahí se derivan dos consecuencias. Primera: nunca pongas secretos en el payload, ni contraseñas ni claves de API ni datos personales, porque es legible para cualquiera que intercepte el token. Segunda: toda tu postura de seguridad descansa en un solo paso, verificar la firma correctamente. Y ese es justo el paso que persiguen los atacantes. Si quieres un recorrido más detallado sobre cómo leer los tokens segmento a segmento, consulta cómo decodificar un JWT.
Los 3 ataques JWT críticos (y cómo frenar cada uno)
La mayoría de las vulnerabilidades JWT son variaciones de un mismo tema: el servidor se fía de algo que controla el atacante. Estos son los tres que rompen la autenticación de raíz, con su mecanismo y su solución.
1. El ataque alg:none — bypass con token sin firmar
La especificación JWS incluye un valor de alg igual a none, que significa «sin firmar». Un token con alg:none tiene el segmento de firma vacío y aun así termina con un punto final, como header.payload.. El ataque es sencillo: coge un token válido, cambia el alg de la cabecera a none, mete los claims que quieras (por ejemplo "role": "admin") y elimina la firma. Las primeras bibliotecas JWT lo aceptaban por defecto, así que el token falsificado pasaba la verificación sin más. El atacante se hace pasar por quien quiera sin tocar una sola clave.
Puedes ver qué aspecto tiene un token así cargando el ejemplo «alg:none» en nuestro decodificador JWT: muestra una advertencia roja explícita de que el token está sin firmar y jamás debe aceptarse para autenticar. Reproducir uno tú mismo lleva un minuto y deja clara la amenaza.
La defensa es una lista de algoritmos permitidos explícita en cada llamada de verificación. Nunca dejes que el valor por defecto de la biblioteca decida qué es aceptable: los valores por defecto antiguos eran permisivos, y ser explícito solo te cuesta una opción de más.
// WRONG — the library may accept alg:none or any algorithm
jwt.verify(token, key);
// RIGHT — pin the exact algorithm you expect
jwt.verify(token, key, { algorithms: ['RS256'] });
none nunca debe aparecer en ese array. Si tu biblioteca no permite fijar algoritmos, cámbiala.
2. Confusión de algoritmos — RS256 degradado a HS256
Esta es la vulnerabilidad JWT más peligrosa en la práctica, conocida desde 2015 y todavía presente en auditorías hoy. Aprovecha los servidores que deciden cómo verificar según el campo alg de la cabecera, la única parte del token que un atacante puede reescribir.
Este es el mecanismo. Tu servidor emite tokens RS256: firma con una clave privada RSA y verifica con la clave pública correspondiente. Esa clave pública es, por definición, pública: puede estar en tu endpoint JWKS o en tu repositorio. El atacante la coge, cambia la cabecera del token de RS256 a HS256 y firma un payload falsificado con HMAC-SHA256 usando la cadena de la clave pública como secreto HMAC. Ahora, en el lado de la verificación: si tu código lee alg de la cabecera y elige HMAC en consecuencia, calcula HMAC-SHA256 sobre el token con esa misma clave pública como secreto. Las firmas coinciden. El token falsificado se acepta.
La causa raíz es la colisión de dos hechos: el verificador se fió del alg controlado por el atacante, y la clave pública RSA estaba a su alcance para usarla como clave HMAC. Ninguno de los dos es un fallo por sí solo. Una clave pública está hecha para ser pública, y una cabecera alg está hecha para describir el token. La vulnerabilidad aparece cuando tu lógica de verificación deja que esa cabecera elija qué tipo de clave y qué algoritmo usar, porque entonces un valor que escribe el atacante dirige el camino criptográfico que ejecuta el servidor.
// WRONG — verification method follows the header's alg field
jwt.verify(token, publicKeyOrSecret);
// RIGHT — hard-code the expected algorithm; never let the header choose
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Fija el algoritmo asimétrico de forma explícita (solo RS256 o ES256), mantén la verificación HMAC en un camino de código completamente separado del de la verificación RSA y usa una biblioteca con mantenimiento que distinga los tipos de clave. Nuestro decodificador JWT marca cualquier token de la familia HS con una advertencia de confusión de clave pública precisamente porque este ataque es tan común: cuando un token que esperabas asimétrico aparece como HS256, esa advertencia es tu señal.
3. Secreto HMAC débil — ataques de fuerza bruta y de diccionario
Cuando sí usas HMAC (HS256/384/512), toda la seguridad del token descansa en la entropía de un único secreto. Si ese secreto es corto, una palabra de diccionario o un valor como secret o password123, un atacante que capture un solo token válido puede crackearlo sin conexión. Herramientas como hashcat prueban miles de millones de candidatos por segundo contra la firma del token. En cuanto cae el secreto, el atacante puede acuñar cualquier token que se le antoje, incluidas credenciales de admin válidas para siempre.
Lo peligroso de este ataque es que ocurre por completo sin conexión. El atacante no martillea tu endpoint de login, así que no dispara ningún rate limit ni deja rastro en tus logs. Captura un token, craquea el secreto en su propio hardware y solo regresa cuando ya puede firmar tokens que superan todas tus comprobaciones. Por eso la regla aquí no admite excusas: usa al menos 32 bytes aleatorios (256 bits) de una fuente criptográficamente segura y guárdalo en un gestor de secretos, nunca en el código ni en un repositorio.
// WRONG — guessable, low entropy, crackable in seconds
const secret = "password123";
// RIGHT — 256 bits from a CSPRNG, then load from KMS at runtime
const secret = require('crypto').randomBytes(32).toString('base64');
¿Necesitas un valor robusto rápido? Nuestro generador de contraseñas produce cadenas de alta entropía válidas como clave HMAC. Para notar la diferencia en la práctica, firma un token de prueba con un secreto fuerte en nuestro codificador JWT, que se ejecuta por completo en el navegador, así que el secreto nunca sale de tu máquina. Y cuando la verificación cruza una frontera de confianza, con varios servicios o verificadores externos, deja de usar HS256 del todo y pásate a un algoritmo asimétrico. Eso es lo que viene ahora.
Elegir y fijar el algoritmo correcto
La elección del algoritmo es donde se gana o se pierde el ataque de confusión, así que elige con cuidado. Los tres que vas a usar de verdad:
| Algoritmo | Tipo | Clave de firma / verificación | Cuándo usarlo |
|---|---|---|---|
| HS256 | Simétrico (HMAC) | Un secreto compartido | Una sola frontera de confianza, la misma parte firma y verifica |
| RS256 | Asimétrico (RSA) | La clave privada firma / la pública verifica | Entre servicios, verificación por terceros, rotación con JWKS |
| ES256 | Asimétrico (ECDSA) | La clave privada firma / la pública verifica | Igual que RS256, con claves más pequeñas y rápidas; preferido en sistemas nuevos |
La regla es corta. Si la misma parte firma y verifica dentro de una frontera de confianza, HS256 está bien y es rápido. Si alguien que no sea quien firma necesita verificar (otro servicio, un socio, un cliente público), usa un algoritmo asimétrico y prefiere ES256, cuyas claves y firmas son mucho más pequeñas que las de RSA a igual nivel de fuerza. Puedes firmar tokens HS256, RS256 y ES256 de muestra en paralelo en el codificador JWT para comparar su estructura y la longitud de la firma.
Elijas lo que elijas, la defensa que de verdad importa es la misma: fija un conjunto de algoritmos explícito en la llamada de verificación y nunca te fíes del campo alg de la cabecera. Todo lo demás se apoya en esa lista de permitidos.
Gestión y rotación de claves
Los algoritmos son tan seguros como las claves que los respaldan, y la gestión de claves es donde la mayoría de las guías se quedan calladas. Para HS256, el secreto tiene al menos 32 bytes aleatorios y vive en un gestor de secretos: AWS Secrets Manager, HashiCorp Vault o Azure Key Vault. Para los algoritmos asimétricos, la clave privada pertenece a un HSM o KMS y nunca toca el código de la aplicación; la clave pública se publica, normalmente a través de un endpoint JWKS que consultan los verificadores.
La rotación tiene que ser rutina, no emergencia. Etiqueta cada clave con un kid (key ID) en la cabecera del JWT para que los verificadores sepan qué clave firmó un token concreto. Mantén un conjunto pequeño de claves válidas en el lado verificador, la clave actual más la anterior reciente, para que los tokens firmados justo antes de una rotación sigan verificándose durante su vida útil. Ese solapamiento es lo que hace que la rotación sea fluida en lugar de una caída del servicio.
Una checklist breve para las claves:
- Rota las claves de firma al menos cada 90 días, y de inmediato ante cualquier sospecha de compromiso.
- Publica las claves públicas vía JWKS; versiónalas con
kid. - Guarda las claves privadas y los secretos HMAC en un KMS o HSM: nunca en git, nunca en código cliente, nunca hardcodeados.
- Ante una filtración, rota la clave y revoca de golpe los refresh tokens pendientes.
Validación de claims que no puedes omitir
Comprobar la firma demuestra que un token es auténtico. No demuestra que el token sea para ti, ahora mismo. De eso se encarga la validación de claims, y es la defensa más barata que puedes añadir. Hay cinco claims que conviene comprobar en cada petición:
exp(expiración) — rechaza los tokens cuya caducidad ya pasó.nbf(not before) — rechaza los tokens usados antes de que se abra su ventana de validez.iat(issued at) — opcionalmente, rechaza los tokens inverosímilmente antiguos.iss(issuer) — confirma que el token vino del emisor en el que confías.aud(audience) — confirma que el token se acuñó para tu servicio. Saltarse la comprobación deaudes el agujero silencioso más común: permite que un token emitido para una API se reutilice contra otra.
La mayoría de las bibliotecas validan estos claims por ti cuando les pasas los valores esperados:
jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 5, // seconds, for distributed clock skew
});
Permite una pequeña tolerancia de reloj (cinco segundos es lo habitual) para que una deriva menor entre servidores no rechace tokens que por lo demás son válidos. Resiste la tentación de agrandarla: una tolerancia generosa amplía la ventana en la que un token expirado todavía funciona, que es justo lo que exp existe para cerrar. Para comprobar a ojo los valores exp e iat de un token, suéltalo en el decodificador JWT y convierte las marcas de tiempo con nuestro convertidor de timestamp Unix.
Vida útil del token y dónde almacenar los JWT
Las comprobaciones del lado del servidor son solo la mitad de la historia. El lugar donde el cliente guarda el token determina lo fácil que es robarlo, y ahí es donde se cruzan el XSS y el secuestro de sesión. El patrón que aguanta es un access token de vida corta (de 15 a 60 minutos) emparejado con un refresh token aparte, de vida más larga y revocable.
La decisión de almacenamiento se reduce a un único compromiso:
| Ubicación de almacenamiento | Exposición a XSS | Riesgo de CSRF | Recomendación |
|---|---|---|---|
| localStorage | Alta: cualquier JavaScript de la página puede leerlo | Ninguno | Evítalo para tokens de sesión |
| Cookie HttpOnly + Secure + SameSite=Strict | Baja: invisible para JavaScript | Necesita protección CSRF | Recomendado para sesiones |
Un token en localStorage es legible por cualquier script que se ejecute en la página, así que un solo fallo de XSS filtra toda la sesión, y el atacante puede reutilizarla desde su propia máquina mientras siga viva. JavaScript no puede leer una cookie HttpOnly en absoluto, lo que reduce el daño del XSS a lo que un atacante consigue hacer dentro de una página activa: malo, pero no una credencial robada que pueda llevarse. El coste del enfoque con cookies es que ahora necesitas protección CSRF, ya que las cookies viajan automáticamente en cada petición; SameSite=Strict más un token CSRF lo resuelven. Mantén el access token corto para que una filtración tenga un radio de impacto pequeño, y pon el refresh token en una cookie HttpOnly, Secure y SameSite. Al cerrar sesión o ante sospecha de compromiso, revoca el refresh token en el servidor y rota la clave de firma. Para el contexto más amplio sobre XSS, CSRF y cookies seguras, consulta nuestra guía de buenas prácticas de seguridad web.
Checklist de seguridad JWT
Repasa esto antes de poner en producción cualquier autenticación basada en JWT:
- La verificación fija una lista de algoritmos permitidos explícita y rechaza
alg:none. - La verificación asimétrica hardcodea el algoritmo esperado y nunca lee
algde la cabecera (bloquea la confusión). - Los secretos HS256 tienen al menos 32 bytes aleatorios, cargados desde un KMS.
- Las claves privadas viven en un HSM/KMS; las públicas se publican vía JWKS y se versionan con
kid. - Las claves de firma rotan al menos cada 90 días.
- Cada petición valida
exp,nbf,iat,issyaud, con una tolerancia de reloj de 5 segundos o menos. - Los access tokens duran de 15 a 60 minutos; los refresh tokens viven en una cookie
HttpOnly. - Ningún secreto en el payload: está codificado, no cifrado.
FAQ
¿Es JWT seguro por defecto?
No. La seguridad de JWT depende de la configuración. Tienes que fijar el algoritmo, rechazar alg:none, usar un secreto o una clave de alta entropía y validar los claims. Las configuraciones de biblioteca por defecto o permisivas permiten bypasses de autenticación con frecuencia.
¿Cuál es la vulnerabilidad JWT más peligrosa?
La confusión de algoritmos, en la que RS256 se degrada a HS256 y la clave pública se usa como secreto HMAC. Se conoce desde 2015 y aún aparece en auditorías, porque aprovecha los servidores que eligen el método de verificación a partir del alg de la cabecera.
¿Debería usar HS256 o RS256?
Usa HS256 cuando la misma parte firma y verifica dentro de una frontera de confianza. Usa RS256 o ES256 cuando otro servicio o un tercero deba verificar, o cuando necesites rotación con JWKS. En sistemas nuevos, prefiere ES256: claves más pequeñas y rápidas a igual nivel de fuerza.
¿Dónde debería almacenar un JWT?
Para los tokens de sesión, prefiere una cookie HttpOnly, Secure y SameSite, ya que JavaScript no puede leerla y un solo fallo de XSS no puede robarla. Evita localStorage para los tokens de sesión: cualquier XSS filtra la sesión entera para reutilizarla.
¿Con qué frecuencia debería rotar las claves de firma JWT?
Rótalas al menos cada 90 días como rutina, y de inmediato ante cualquier sospecha de compromiso. Versiona las claves con kid y mantén en el verificador tanto la clave activa como la anterior reciente, para que los tokens firmados justo antes de la rotación sigan validándose.
¿Se puede manipular un JWT?
No sin la clave de firma: ningún atacante puede falsificar un token que supere la verificación. Pero si tu servidor acepta alg:none, es vulnerable a la confusión de algoritmos o usa un secreto débil, la firma puede sortearse. Esos son fallos de configuración, no defectos del propio JWT.
¿Qué claims debo validar?
Valida exp (expiración), nbf (not before), iat (issued at), iss (issuer) y aud (audience). La falta de una comprobación de aud es la vulnerabilidad silenciosa más común: permite que un token destinado a un servicio se reutilice contra otro.
Conclusión
La seguridad de JWT no es complicada, pero cada capa tiene que aguantar. La firma es tu única garantía, así que verifícala correctamente: fija un único algoritmo explícito y no te fíes nunca del alg de la cabecera. Usa claves fuertes, rotadas y guardadas en un KMS. Valida exp, nbf, iat, iss y aud en cada petición. Y guarda los tokens donde el XSS no llegue.
Para llevarlo a la práctica, pega cualquier token en nuestro decodificador JWT: así inspeccionas su algoritmo y sus claims y detectas riesgos de alg:none o de confusión HS. Y usa el codificador JWT para experimentar con la firma por completo en tu navegador, donde tus claves nunca salen de tu dispositivo.