JWT alg:none todavía existe en producción en 2026
JWT alg:none fue un meme en 2015. En 2026 aún aparece en producción en SaaS brasileños. Las 4 variantes modernas y cómo probarlas y corregirlas.
Investigador de seguridad y fundador de No Vuln

En 2015, JWT alg: none se volvió un meme — la vulnerabilidad tan básica que nadie podría seguir teniendo en producción. En 2026, todavía la encuentro en producción. ¿Cómo es posible?
Respuesta corta: el bug original “murió” (las librerías modernas lo rechazan por defecto), pero 4 variantes modernas siguen apareciendo en pentest con una regularidad incómoda. Este post es la guía técnica de cada variante, cómo detectarla en 30 segundos y cómo corregirla.
El bug original (repaso rápido)
JWT permite especificar el algoritmo de firma en el header. En 2015 se descubrió que muchas librerías aceptaban alg: none — es decir, un JWT sin firma. El atacante editaba el payload, quitaba la firma, y el servidor lo aceptaba:
- Header:
{"alg":"none","typ":"JWT"} - Payload:
{"sub":"admin","role":"admin"} - Signature: (vacía)
El servidor lo procesaba como un token válido de admin. Catástrofe global.
Hoy, las librerías modernas (jsonwebtoken v9+, PyJWT 2+, JJWT 0.12+) rechazan alg: none por defecto. Bug muerto. Salvo que no está muerto. Cambió de forma.
Variante 1: Algorithm Confusion (HS256 vs RS256)
El bug: el servidor está configurado para validar el JWT con RS256 (firma asimétrica), pero la función de verificación acepta HS256 (firma simétrica). El atacante usa la clave PÚBLICA RSA del servidor como secreto HMAC.
Por qué funciona:
- RS256 valida con
clave_publica - HS256 valida con
secreto_compartido - La clave pública RSA está disponible en un endpoint público (
/.well-known/jwks.jsono similar) - La función
jwt.verify(token, clave_publica)sin especificar el algoritmo acepta ambos - El atacante forja un JWT con
alg: HS256usando la clave pública como secreto HMAC - El servidor valida con HS256 + clave pública = éxito
Cómo detectarlo en pentest:
- Capturar un JWT legítimo
- Decodificar el header (sin validar) e identificar
alg: RS256 - Intentar forjar con HS256 usando la clave pública como secreto
- Si el servidor lo acepta → vulnerabilidad confirmada
Cómo corregirlo:
- ❌ Vulnerable:
jwt.decode(token, public_key) - ✅ Correcto:
jwt.decode(token, public_key, algorithms=['RS256'])con whitelist explícita
Variante 2: JKU / X5U SSRF
El bug: los parámetros opcionales del header del JWT (jku, x5u) permiten indicar una URL externa desde donde buscar la clave de verificación. Un servidor que confía a ciegas hace una petición a una URL controlada por el atacante.
Explotación: el atacante agrega "jku": "https://atacante.com/jwks.json" en el header del JWT. El servidor hace un GET a la URL del atacante, recibe la clave pública del atacante, valida el JWT con ella → el atacante forjó un token válido.
Bonus: además del bypass de auth, se convierte en una SSRF chain. Agregar "jku": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" hace que el servidor solicite los metadatos internos de AWS. La respuesta va al parser de JWKS (el parseo falla, pero la petición ya se hizo). Metadatos de la nube exfiltrados.
Cómo detectarlo:
- Agregar
jkual header de un JWT legítimo apuntando ahttps://burpcollaborator.net/ - Enviar la petición con el JWT modificado
- Si el collaborator recibe un ping → vulnerabilidad confirmada
Cómo corregirlo:
- Allowlist explícita de las URLs JKU permitidas
- Mejor: ignorar por completo los parámetros
jku/x5usi no los usas (configurar la librería para deshabilitarlos)
Variante 3: kid Path Traversal
El bug: el parámetro kid (Key ID) se usa para seleccionar con qué clave validar el token. Un servidor que usa kid como path a un archivo del filesystem acepta path traversal.
Explotación clásica: el atacante envía "kid": "../../../../tmp/known_content". Si el atacante logra escribir en /tmp/known_content (vía upload de archivos, log poisoning, etc.), puede hacer que el servidor “lea” esa clave conocida y valide un JWT forjado con ella.
Variante más común en 2026: kid acepta SQL injection (el servidor consulta la clave en la DB). El atacante envía "kid": "1' UNION SELECT 'CLAVE_CONOCIDA' --".
Cómo detectarlo:
- Capturar un JWT, modificar
kida../etc/passwd - Si el error de validación cambia (file not found, parse error específico) → vulnerable
- Probar SQL injection en el
kid(' OR 1=1 --)
Cómo corregirlo: el kid debe ser un ID opaco, validado contra una allowlist fija. NUNCA usado como path del filesystem o como string SQL.
Variante 4: expiración del token ignorada
El bug: más simple y más común de lo que parece. El servidor no valida el claim exp. Algunas librerías no lo validan por defecto — hay que pasar un flag explícito.
Un token robado en 2023 sigue válido en 2026. Sesión eterna. Es el tipo de bug que sobrevive a varios pentests porque a nadie se le ocurre probarlo (todos asumen que se valida).
Cómo detectarlo:
- Modificar
expen el payload a una fecha pasada - Volver a firmarlo (si tienes el secreto de prueba) o usar un token expirado real
- Si el servidor lo acepta → vulnerable
Cómo corregirlo: pasar siempre options={'require': ['exp'], 'verify_exp': True} o el equivalente de la librería.
Por qué estas variantes siguen existiendo en 2026
Tres motivos prácticos:
1. Librerías antiguas en sistemas legados
Sistemas ERP, sistemas gubernamentales, sistemas legados de bancos usan librerías de 2017–2020 que todavía tienen los bugs. Migrar a una nueva versión major es un trabajo que nadie prioriza.
2. Configuraciones por defecto subóptimas
Los devs copian el ejemplo del README: jwt.decode(token, key). Sin leer que hay que pasar algorithms=['RS256']. CVE en producción esperando.
3. Code review sin foco en auth
El código de auth rara vez pasa por un code review especializado. El bug entra en un PR de una feature “menor” y nunca se ve. Todo equipo que no tiene una revisión de auth específica tiene al menos 1 de estas variantes en producción, estadísticamente.
Cómo probarlo tú mismo en 30 minutos
Usa jwt.io o una herramienta similar:
- Pega tu JWT en el decoder
- Verifica el
algen el header - Modifícalo a
noney vuelve a pegarlo — si el servidor lo acepta (probando contra un endpoint protegido), es vulnerable - Si es
alg: RS256— intenta HS256 usando la clave pública como secreto (variante 1) - Agrega
jkual header apuntando a un servidor tuyo — si el servidor hace la petición, variante 2 confirmada - Modifica
expa una fecha pasada — si lo acepta, variante 4
5 pruebas. 30 minutos. Si alguna pasa — CVE abierto en producción.
Configuración segura completa
El patrón correcto que toda implementación de JWT debería seguir:
- Whitelist de algoritmos:
algorithms=['RS256']o equivalente, sin fallback - Claims obligatorios:
options={'require': ['exp', 'iat']} - Validación activa:
verify_exp, verify_iat, verify_nbf, verify_aud, verify_isstodos enTrue - Audience esperada:
audience='api.objetivo.com' - Issuer esperado:
issuer='https://auth.objetivo.com' - Sin
jkuaceptado. Sinkidque acepte un path. Sin fallback de algoritmo. Sin expiración opcional.
La lección más importante
Los 4 patrones de arriba no son bugs oscuros. Cada uno aparece en pentest con regularidad. La diferencia entre un equipo vulnerable y un equipo seguro no es el talento — es el proceso:
- Code review enfocado en auth
- Pentest periódico que valida específicamente estos patrones
- Actualización de librerías (PyJWT, jsonwebtoken siempre en la última major)
- CI con pruebas automatizadas de la validación de JWT
Si tu aplicación usa JWT (y casi todas lo usan en 2026), vale la pena dedicar 30 minutos a probar tú mismo los 4 vectores antes de descubrirlo por las malas.
Para hablar de un pentest enfocado en auth y JWT, mira las páginas dedicadas: pentest para API y pentest para SaaS. Para entender otros patrones de auth que dominan el account takeover en 2026, mira OAuth Account Takeover.
Siguiente paso
¿Quieres aplicar esto a tu sistema?
No Vuln hace pentests en profundidad con la misma metodología descrita en este artículo. Solicita una propuesta: NDA bilateral en 24h y alcance definido en una llamada técnica.