← All posts

Five JWT mistakes I keep finding in VAPT

alg confusion, decode-instead-of-verify, kid injection, missing claim checks and crackable secrets — how each JWT flaw is exploited, and the few lines of code that prevent it.

JSON Web Tokens are everywhere in modern APIs, and they fail in very predictable ways. A JWT is just three base64url segments — header.payload.signature — and almost every vulnerability comes from the server trusting the first two before properly checking the third.

Here are the five issues I test for on every API engagement, with the attack and the fix side by side.

1. Decoding instead of verifying

It sounds too simple to be real, but it's the most common: the code reads the token without checking the signature.

// ❌ jwt.decode() does NOT verify anything
const user = jwt.decode(req.headers.authorization.split(' ')[1]);

Attack: edit the payload ("role":"user" → "role":"admin"), re-encode it, keep or drop the signature. If the API accepts it, it's game over.

Fix: always call the library's verify function, and do it in one middleware so no route can skip it.

2. Algorithm confusion (none and RS256 → HS256)

The header tells the server which algorithm was used — and older or misconfigured libraries believed it.

  • alg: none — the attacker sets "alg":"none" and sends an empty signature. Some libraries historically accepted this as "valid, unsigned".
  • RS256 → HS256 key confusion — the server signs with an RSA private key and verifies with the RSA public key. The attacker changes the header to HS256 and signs the forged token with HMAC, using the public key as the secret. If the server passes "the key" to a generic verify function, it HMAC-verifies with the public key, and the signature checks out. Public keys are, by definition, public — often served at /.well-known/jwks.json.

Fix: pin the expected algorithm on the server side; never let the token choose.

jwt.verify(token, publicKey, { algorithms: ['RS256'], issuer: ISS, audience: AUD });

3. Header injection: kid, jku, jwk

Headers can point the server at a key:

  • kid (key ID) is often used in a file path or database lookup. "kid": "../../../../dev/null" can make the server verify with an empty key, which the attacker can then sign with. If it's used in SQL, it's a SQL injection point.
  • jku / x5u give a URL to fetch keys from. If the server follows it, the attacker hosts their own key set.
  • jwk embeds a public key directly in the header. A server that trusts it lets the attacker sign with their own private key.

Fix: treat kid as an opaque lookup into an allowlisted key set, ignore jku/jwk from tokens, and load keys only from configuration you control.

4. Missing claim validation

A cryptographically valid token can still be the wrong token:

  • No exp check → tokens live forever; a leaked token never stops working.
  • No aud check → a token minted for service A is accepted by service B on the same identity provider.
  • No iss check → tokens from a different tenant or environment (staging!) are accepted in production.

Fix: validate exp, nbf, iss and aud on every request, and keep access tokens short-lived (minutes, not days) with refresh-token rotation.

5. Weak HMAC secrets

HS256 tokens are only as strong as the shared secret. If it's secret, changeme or the app name, the token can be cracked offline — one captured JWT is enough, no further requests needed. Hashcat supports JWTs directly (mode 16500), and wordlists of common JWT secrets exist precisely because this is so common.

hashcat -a 0 -m 16500 captured.jwt jwt-secrets.txt

Fix: use at least 256 bits of random secret from a secrets manager, rotate it, or move to asymmetric signing (RS256/ES256/EdDSA) so services that only verify never hold a signing key.

Bonus: where the token lives

Storing JWTs in localStorage means any XSS can read them and exfiltrate a long-lived session. For browser apps I prefer an HttpOnly, Secure, SameSite cookie holding a short-lived token (with CSRF protection), or a backend-for-frontend pattern that keeps tokens out of JavaScript entirely.

Testing quick list

  1. Tamper with the payload, keep the signature → accepted?
  2. alg: none with an empty signature.
  3. RS256 → HS256 using the published public key.
  4. kid path traversal and SQL injection; attacker-hosted jku; embedded jwk.
  5. Expired token, wrong-audience token, staging token in production.
  6. Offline crack attempt on any HS256 token.

Burp Suite's JWT Editor extension automates most of this.

The takeaway

JWT libraries are mostly safe if you configure them explicitly: fixed algorithms, fixed keys, and all claims validated. Every bug above comes from letting the token tell the server how to trust it.

References