JWT & Token Security Pitfalls

Why a format that lets the token declare its own verification algorithm produced alg:none and the HMAC/RSA confusion bug — plus the revocation problem that no amount of correct crypto fixes.

A JWT is three base64url segments — header, payload, signature — joined by dots. The cryptography underneath (HMAC or a signature) is sound. Nearly every JWT vulnerability comes instead from one design decision: the token tells the verifier how to verify it. Attacker-controlled input selecting the verification algorithm is a structural flaw, and it produced two of the most instructive bugs in modern applied crypto.

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← header  {"alg":"HS256","typ":"JWT"}
eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciJ9   ← payload {"sub":"123","role":"user"}
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1g    ← signature

alg: none #

The none algorithm exists in the JWS spec for cases where integrity is provided by an outer layer. Libraries implemented it literally: a token with {"alg":"none"} and an empty signature segment was accepted as valid.

text
{"alg":"none","typ":"JWT"}.{"sub":"123","role":"admin"}.

Strip the signature, change role to admin, set alg to none, send. The 2015 Auth0 disclosure found this across many major libraries at once. It still surfaces in audits a decade later.

Algorithm confusion: RS256 → HS256 #

Subtler and more interesting. A server verifies RS256 tokens with an RSA public key. The attacker changes the header to HS256 and signs the tampered token using that public key as the HMAC secret.

The server's verify call is polymorphic: it dispatches on the token's alg and passes whatever key it has. For RS256 that key is public; for HS256 it becomes a shared secret the attacker also knows. Both sides use the same bytes and the HMAC checks out.

The fix is to stop asking the token. Decide the algorithm server-side and reject anything else:

python
jwt.decode(token, key, algorithms=["RS256"])   # explicit allow-list, never from the header

An allow-list of exactly one algorithm closes both alg:none and confusion at once. RFC 8725 makes this a normative requirement.

HS256 versus RS256 is a trust-topology choice #

HS256 uses HMAC: every verifier needs the same secret and can therefore mint indistinguishable tokens. It fits one tightly controlled security boundary, but adding a read-only verifier silently gives that service issuer power. RS256 uses an RSA private key to sign and a public key to verify, so many services can validate without gaining authority to mint.

Question HS256 RS256
verifier material shared secret public key
can a verifier forge? yes no
compromise blast radius any verifier exposes issuer capability verifier compromise does not reveal signing key
operational cost secret distribution to every verifier private-key protection plus public-key rotation/distribution

Neither is universally “more secure.” The decisive question is who may issue tokens and whether verifiers cross trust boundaries. In both cases pin the allowed algorithm server-side, bind each key to one algorithm, and validate issuer, audience, token type, and lifetime.

The rest of the checklist #

Pitfall What to do
Missing claim validation verify exp, nbf, iss, aud — a signature only proves the token wasn't altered, not that it was meant for you
Cross-service replay always check aud; otherwise a token for service A is valid at service B
Weak HMAC secret HS256 with "secret" or a short passphrase is offline-brute-forceable from a single captured token. Use ≥256 bits from the CSPRNG
kid header injection kid is attacker-controlled — it has been used for path traversal and SQL injection into key lookup. Treat as untrusted; look up in a fixed map
jku/x5u header tells the server where to fetch the verification key. Disable, or strictly allow-list the origin
Non-constant-time compare see MAC verification
No expiry a token without exp is a permanent credential

The problem correct crypto does not solve #

The usual reason to choose JWTs is statelessness — any server can verify without a session store. That property is the drawback: you cannot revoke a stateless token. Logout, a password change, a role downgrade, or a detected compromise cannot invalidate a signed token that is already issued. It stays valid until exp.

Mitigations, all of which reintroduce state:

  • Short-lived access tokens (5–15 min) plus a longer-lived, revocable refresh token held server-side. This is the standard architecture and the reason OAuth 2 is shaped the way it is.
  • A denylist of revoked JTIs until their expiry — a session store with extra steps, but a small one.
  • A token_version claim checked against the user record — one cheap read, and it invalidates every prior token on password change.

Sources

  1. docsRFC 7519 — JSON Web Token (JWT)
  2. docsRFC 8725 — JSON Web Token Best Current Practices
  3. articleCritical vulnerabilities in JSON Web Token libraries (Auth0, 2015)
  4. articleRS256 vs HS256: What's the Difference? (Auth0)
  5. docsPASETO — Platform-Agnostic Security Tokens

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close