JWT: los 3 fallos que veo en cada pentest
Los JSON Web Tokens están en todas partes: APIs, inicio de sesión, SSO. Y esconden una trampa mental: parecen opacos, pero un JWT no está cifrado, está codificado. Cualquiera puede leer su contenido sin clave. Eso no es el fallo —es de diseño—, pero es la puerta por la que entran los tres errores de implementación que me encuentro una y otra vez.
Anatomía en 20 segundos
Un JWT son tres partes en Base64url separadas por puntos: cabecera.payload.firma.
eyJhbGciOiJIUzI1NiJ9 . eyJ1c2VyIjoiYWRtaW4ifQ . <firma>
└─ {"alg":"HS256"} └─ {"user":"admin"} └─ HMAC/RSA
La cabecera y el payload son legibles por cualquiera (solo Base64). Lo único que impide falsificarlos es la firma. Toda la seguridad del token depende de que el servidor la verifique bien. Si falla ahí, se acabó.
alg.Fallo 1 — alg: none
La especificación permite el algoritmo none: un token «sin firma». Si el servidor lo acepta, el atacante cambia la cabecera a {"alg":"none"}, borra la firma y reescribe el payload a su gusto:
{"alg":"none"}.{"user":"admin","role":"admin"}.
Sin verificación de firma, el backend se lo cree. Es escalada de privilegios directa. La defensa: nunca aceptes none y no dejes que el token elija el algoritmo.
Fallo 2 — secreto HMAC débil (HS256)
Con HS256 la firma es un HMAC con un secreto compartido. Si ese secreto es débil (secret, changeme, una palabra de diccionario), se rompe offline: capturas un token válido y lanzas fuerza bruta contra la firma (hashcat modo 16500, jwt_tool). En cuanto sale el secreto, puedes firmar cualquier token: eres quien quieras.
Fallo 3 — confusión de algoritmo (RS256 → HS256)
Con RS256 el token se firma con la clave privada y se verifica con la pública (que, por definición, es pública). El fallo clásico: el atacante cambia la cabecera a HS256 y firma el token usando la clave pública como secreto HMAC. Si el servidor no fija el algoritmo y usa «la clave» sin más, verifica con la pública tratándola como secreto simétrico… y valida el token falso. El primo hermano de este fallo es el más tonto de todos: no verificar la firma en absoluto (más común de lo que parece en middlewares hechos a mano).
Y un recordatorio: el payload no es un secreto
Como es legible, meter datos sensibles en el payload (correos, roles internos, IDs, tokens de terceros) es filtrarlos a cualquiera que capture el JWT. El payload es íntegro si la firma es buena, pero nunca es confidencial.
Cómo blindarlo (lado servidor)
- ✅ Verifica siempre la firma, y fija el algoritmo esperado (no lo leas de la cabecera del token).
- ✅ Rechaza
alg: noneexplícitamente. - ✅ HS256: secreto largo y aleatorio (≥ 256 bits), rotado; nada de secretos de diccionario.
- ✅ RS256: usa la pública solo para verificar; asegúrate de que la librería no acepta HS con esa clave.
- ✅ Valida
exp(ynbf/aud/iss); caducidades cortas y refresh tokens. - ✅ No metas secretos en el payload.
Checklist de triaje (lado ofensivo)
- ✅ Decodifica el token y mira
algy los claims (Decodificador JWT). - ✅ Prueba
alg: noney firma vacía. - ✅ Si es HS256, intenta romper el secreto offline (débil/diccionario).
- ✅ Si es RS256, prueba la confusión HS con la clave pública.
- ✅ Manipula claims (
role,user,exp) y observa si el server verifica de verdad.
El JWT no es inseguro; casi siempre lo es cómo se verifica. Y como el token se lee sin clave, el primer paso —mirar dentro— lo puede dar cualquiera. Que ese vistazo lo des tú antes que un atacante.