I’ve been doing API pentests for years, and JWT misconfigurations remain in my top 5 most-found vulnerability classes. Not because the attacks are new — most of these have been documented since 2015 — but because they keep getting reintroduced by developers who learned about JWTs from a tutorial that didn’t cover the security pitfalls.

This post is the field guide I wish existed: five real JWT bugs I’ve found in production engagements over the last 12 months, with the actual payloads and the actual fixes.

1. The none algorithm

You’d think this one would be dead. It isn’t.

Some libraries still accept the none algorithm if the JWT header explicitly requests it. The attack:

  1. Take any valid token
  2. Decode the header, change "alg": "RS256" to "alg": "none"
  3. Re-encode, drop the signature, and submit

If the server’s library trusts the header’s alg claim, you’ve forged a valid token without any key.

Fix: Hard-code the expected algorithm on the verification side. Never call verify() with algorithms taken from the token header.

2. Algorithm confusion (RS256 → HS256)

This one is more subtle and still surprisingly common.

If a server is configured to accept tokens signed with RS256 (asymmetric, public/private key pair), but the verification call doesn’t enforce the algorithm, an attacker can:

  1. Get the server’s public key (often available at /.well-known/jwks.json)
  2. Forge a token with "alg": "HS256"
  3. Sign it using the public key as the HMAC secret

The verification code does verify(token, publicKey). The attacker’s token says HS256, so the library treats publicKey as a symmetric secret. Signature matches. Token is “valid.”

Fix: Same as above — explicitly specify the allowed algorithms. Never trust the alg header for verification logic.

3. The kid parameter — path traversal and SQL injection

The kid (key ID) header tells the verifier which key to use. Many implementations look it up by reading from a filesystem path or a database.

I’ve found:

  • "kid": "../../../../dev/null" — Used the contents of /dev/null (empty string) as the HMAC key. Sign with empty key. Token validates.
  • "kid": "key1' OR '1'='1" — SQL injection in the kid lookup. Returned the first key in the table. Sign with known value.

Fix: Treat kid as a tainted user input. Validate it against an allowlist. Don’t pass it to filesystem or database calls.

4. JWK injection via the jwk header

Some libraries support an optional jwk header — an embedded JSON Web Key the verifier should use to validate the token. In a token the verifier received from a client.

This is as bad as it sounds. Attacker generates their own keypair, embeds the public key in the jwk header, signs the token with their private key. If the library trusts the jwk header, validation succeeds.

Fix: Disable any auto-discovery of keys from token-controlled headers. Only use server-side configured keys.

5. Weak secrets on HS256

If the server uses HS256 (symmetric HMAC), the secret needs to be long and random. I still find production systems where the JWT secret is:

  • The string "secret" (yes, really)
  • The application name
  • A short word picked from a wordlist

hashcat -m 16500 will crack a weak HMAC secret on a JWT in minutes. Once you have the secret, you can sign anything as anyone.

Fix: Use 256+ bits of entropy from a CSPRNG. Rotate periodically. Or just use RS256 / ES256 and dodge the whole problem.

What to do on every JWT engagement

Here’s my standard checklist when I find JWTs in scope:

  1. Decode the token. Look at the algorithm, the claims, the kid. (jwt.io is fine for this.)
  2. Try none. Always. Costs nothing.
  3. Try algorithm confusion. If you can find a public key (JWKS endpoint, openssl s_client output, anywhere), try signing HS256 with it.
  4. Fuzz the kid. Path traversal, SQL injection, command injection. All have hit in real engagements.
  5. Check the secret strength if HS*. If you can capture a token, you can attempt offline cracking.
  6. Check expiration handling. Does the server reject expired tokens? What about tokens with exp claims removed entirely?
  7. Check the iss and aud claims. Do they get validated? Cross-tenant token reuse is a classic.

The fix layer

Almost every JWT bug I’ve described comes from one root cause: trusting attacker-controlled headers in security-critical decisions. The token header is part of the input. It is not configuration. Every claim in there should be validated against a server-side allowlist before it’s used to look up keys, choose algorithms, or anything else.

If you’re a developer reading this: please use jose (Node), PyJWT (Python), or the equivalent library that accepts an explicit algorithms parameter, and always pass it. Don’t trust the token to tell you how to verify itself.

If you’re a defender: log every JWT validation failure with the algorithm, kid, and source IP. The alg distribution will surface attacks immediately — legitimate clients all use the same algorithm.

References

  • RFC 7515 (JWS) — surprisingly readable, worth reading the security considerations section
  • Auth0’s JWT vulnerabilities post — old but the patterns hold
  • PortSwigger’s JWT labs — practical hands-on for each of these classes

If you want a JWT review on your API, get in touch.