Back to the blog
Vulnerability10 min readUpdated

JWT alg:none Is Still in Production in 2026. Here's How

JWT alg:none became a meme in 2015. In 2026 it still shows up in production. A breakdown of the 4 modern variants and how to test and fix them.

Diego Melo, author
Diego Melo

Security researcher and founder of No Vuln

JWT alg:none Is Still in Production in 2026. Here's How

In 2015, JWT alg: none became a meme — the vulnerability so basic nobody could possibly still have it in production. In 2026, I still find it in production. How is that possible?

Short answer: the original bug “died” (modern libraries reject it by default), but 4 modern variants keep showing up in pentests with uncomfortable regularity. This post is the technical guide to each variant, how to detect it in 30 seconds, and how to fix it.

The original bug (a quick recap)

JWT lets you specify the signing algorithm in the header. In 2015, people discovered that many libraries accepted alg: none — that is, a JWT with no signature. The attacker edited the payload, stripped the signature, and the server accepted it:

  • Header: {"alg":"none","typ":"JWT"}
  • Payload: {"sub":"admin","role":"admin"}
  • Signature: (empty)

The server processed it as a valid admin token. A global catastrophe.

Today, modern libraries (jsonwebtoken v9+, PyJWT 2+, JJWT 0.12+) reject alg: none by default. Bug dead. Except it isn't dead. It changed shape.

Variant 1: Algorithm Confusion (HS256 vs RS256)

The bug: the server is configured to validate the JWT with RS256 (asymmetric signature), but the verification function accepts HS256 (symmetric signature). The attacker uses the server's RSA PUBLIC key as the HMAC secret.

Why it works:

  • RS256 validates with the public_key
  • HS256 validates with the shared_secret
  • The RSA public key is available at a public endpoint (/.well-known/jwks.json or similar)
  • The jwt.verify(token, public_key) function, without specifying an algorithm, accepts both
  • The attacker forges a JWT with alg: HS256 using the public key as the HMAC secret
  • The server validates with HS256 + the public key = success

How to detect it in a pentest:

  1. Capture a legitimate JWT
  2. Decode the header (without validating) and identify alg: RS256
  3. Try forging with HS256 using the public key as the secret
  4. If the server accepts it → vulnerability confirmed

How to fix it:

  • ❌ Vulnerable: jwt.decode(token, public_key)
  • ✅ Correct: jwt.decode(token, public_key, algorithms=['RS256']) with an explicit whitelist

Variant 2: JKU / X5U SSRF

The bug: optional JWT header parameters (jku, x5u) let you point to an external URL from which to fetch the verification key. A server that trusts it blindly makes a request to a URL the attacker controls.

Exploitation: the attacker adds "jku": "https://attacker.com/jwks.json" to the JWT header. The server sends a GET to the attacker's URL, receives the attacker's public key, validates the JWT with it → the attacker has forged a valid token.

Bonus: beyond an auth bypass, this becomes an SSRF chain. Adding "jku": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" makes the server request internal AWS metadata. The response goes to the JWKS parser (it fails to parse, but the request was made). Cloud metadata exfiltrated.

How to detect it:

  1. Add jku to a legitimate JWT header pointing to https://burpcollaborator.net/
  2. Send the request with the modified JWT
  3. If Burp Collaborator receives a ping → vulnerability confirmed

How to fix it:

  • An explicit allowlist of permitted JKU URLs
  • Better: ignore the jku/x5u parameters entirely if you don't use them (configure the library to disable them)

Variant 3: kid Path Traversal

The bug: the kid (Key ID) parameter is used to select which key validates the token. A server that uses kid as a path to a file on the filesystem accepts path traversal.

Classic exploitation: the attacker sends "kid": "../../../../tmp/known_content". If the attacker can write to /tmp/known_content (via a file upload, log poisoning, etc.), they can make the server “read” that known key and validate a JWT forged with it.

The more common variant in 2026: kid accepts SQL injection (the server looks up the key in the DB). The attacker sends "kid": "1' UNION SELECT 'KNOWN_KEY' --".

How to detect it:

  1. Capture a JWT, change kid to ../etc/passwd
  2. If the validation error changes (file not found, a specific parse error) → vulnerable
  3. Test the kid for SQL injection (' OR 1=1 --)

How to fix it: kid must be an opaque ID, validated against a fixed allowlist. NEVER used as a filesystem path or a SQL string.

Variant 4: Token expiration ignored

The bug: simpler and more common than it sounds. The server doesn't validate the exp claim. Some libraries don't validate it by default — you have to pass an explicit flag.

A token stolen in 2023 is still valid in 2026. An eternal session. It's the kind of bug that survives multiple pentests because nobody thinks to test it (everyone assumes it's validated).

How to detect it:

  1. Change exp in the payload to a past date
  2. Re-sign it (if you have the test secret) or use a real expired token
  3. If the server accepts it → vulnerable

How to fix it: always pass options={'require': ['exp'], 'verify_exp': True} or the library's equivalent.

Why these variants still exist in 2026

Three practical reasons:

1. Old libraries in legacy production systems

ERP systems, government systems, legacy banking systems use libraries from 2017–2020 that still have the bugs. A major-version migration is work nobody prioritizes.

2. Suboptimal default configurations

Developers copy the README example: jwt.decode(token, key). Without reading that they need to pass algorithms=['RS256']. A CVE waiting to happen in production.

3. Code review with no focus on auth

Auth code rarely goes through specialized code review. The bug lands in a “minor” feature PR and never gets noticed. Statistically, any team without dedicated auth review has at least 1 of these variants in production.

How to test it yourself in 30 minutes

Use jwt.io or a similar tool:

  1. Paste your JWT into the decoder
  2. Check the alg in the header
  3. Change it to none and re-paste — if the server accepts it (testing against a protected endpoint), it's vulnerable
  4. If alg: RS256 — try HS256 using the public key as the secret (variant 1)
  5. Add jku to the header pointing to a server you control — if the server makes a request, variant 2 is confirmed
  6. Change exp to a past date — if it's accepted, variant 4

5 tests. 30 minutes. If any of them passes — you have an open CVE in production.

The complete secure configuration

The correct pattern that every JWT implementation should follow:

  • Algorithms whitelist: algorithms=['RS256'] or equivalent, with no fallback
  • Required claims: options={'require': ['exp', 'iat']}
  • Active validation: verify_exp, verify_iat, verify_nbf, verify_aud, verify_iss all True
  • Expected audience: audience='api.target.com'
  • Expected issuer: issuer='https://auth.target.com'
  • No jku accepted. No kid that accepts a path. No algorithm fallback. No optional expiration.

The most important lesson

The 4 patterns above are not obscure bugs. Each one shows up in pentests regularly. The difference between a vulnerable team and a secure team isn't talent — it's process:

  • Code review focused on auth
  • Periodic pentesting that specifically validates these patterns
  • Keeping libraries updated (PyJWT, jsonwebtoken always on the latest major)
  • CI with automated JWT validation tests

If your application uses JWT (and almost every one does in 2026), it's worth 30 minutes testing the 4 vectors yourself before you discover them the hard way.

To talk about a pentest focused on auth and JWT, see the dedicated pages: API penetration testing and SaaS penetration testing. To understand other auth patterns that dominate ATO in 2026, see OAuth Account Takeover.

Share

WhatsAppLinkedInX

Next step

Want to apply this to your system?

No Vuln runs in-depth penetration tests with the same methodology described in this article. Request a proposal: mutual NDA within 24h, scope defined on a technical call.