How the attack works
A JWT consists of three Base64url-encoded parts separated by dots: Header (metadata including the algorithm "alg"), Payload (claims such as sub, role, exp), and Signature. Header and payload are only encoded, not encrypted, so they are readable and modifiable by anyone.
Security rests entirely on the cryptographic signature, which the server generates with a secret key and verifies on receipt. Attacks arise when this verification fails:
- alg=none / unsigned token - some implementations treat "none" as an already-verified signature and trust the modified token; filters against this can sometimes be bypassed with tricks such as mixed capitalization and unexpected encodings.
- Missing signature verification - developers confuse, for example in the Node.js library jsonwebtoken, the method decode() (decoding only) with verify() (verification), so the signature is never checked at all.
- Algorithm confusion (key confusion) - many libraries offer a generic verify() method that decides how to check the token based on the "alg" header. If the code expects RS256 (asymmetric) and passes in the fixed public key, but an attacker sets alg to HS256 (symmetric), the library uses the public key as the HMAC secret. Since this key is publicly known, the attacker can sign arbitrary tokens as valid themselves.
- Weak HMAC secret - with HMAC algorithms everything hinges on the secret; with a valid token, an attacker can crack the secret offline via brute force (e.g. Hashcat, John the Ripper) and then re-sign arbitrary tokens.
- Missing expiry/audience check and replay - without checking exp/aud, tokens remain valid indefinitely or can be reused on other services.
Missing or incorrect signature verification lets forged tokens through.
Example
Algorithm confusion (RS256 -> HS256), following PortSwigger. Starting point: the server issues a token signed with RS256 and verifies it with its public key.
- Step 1 - the attacker retrieves the server's public key via a standard endpoint:
GET /.well-known/jwks.jsonreturns the JWK object (PortSwigger mentions/jwks.jsonor/.well-known/jwks.json). - Step 2 - the key is converted into the matching format (e.g. X.509 PEM), exactly identical to the server's copy, including non-printable characters such as line breaks.
- Step 3 - the attacker builds a token with a modified payload and sets
"alg":"HS256"in the header: header{"alg":"HS256"}, payload{"sub":"administrator","role":"admin"}. - Step 4 - they sign this token via HMAC-SHA256, using the public PEM key as the HMAC secret.
- Step 5 - they send the token:
GET /admin HTTP/1.1 ... Cookie: session=eyJhbGciOiJIUzI1NiJ9... The server calls its genericverify(token, secretOrPublicKey), seesalg=HS256, and checks the HMAC signature with exactly this public key - the check succeeds, and the forged admin token is accepted.
Impact
Successful JWT attacks are usually highly critical: an attacker can create forged tokens with arbitrary claims that are accepted as valid, bypassing authentication and access control.
A typical consequence is impersonating already-authenticated users, especially escalation to administrator privileges (e.g. role from "user" to "admin").
Since JWTs are used centrally for authentication, session handling, and authorization, this can compromise the entire application and its users.
How to protect against it
- Use an up-to-date, well-maintained JWT library, and make sure developers fully understand how it works and its security implications (PortSwigger).
- Perform robust signature verification on every received token and account for edge cases such as unexpected algorithms - never call only decode() instead of verify() (PortSwigger).
- Explicitly enforce the expected algorithm during validation, so that tokens with an unexpected algorithm, such as alg=none or HS256 instead of RS256, are rejected (OWASP, PortSwigger).
- Choose a strong, unique HMAC secret: at least 64 characters from a secure random source; alternatively use RSA instead of HMAC (OWASP).
- Set an expiry date (exp) for every issued token (PortSwigger).
- Set and verify the aud claim (audience), so a token cannot be reused on other services/websites (PortSwigger).
- Avoid transmitting tokens in URL parameters wherever possible, and allow the issuing server to block/revoke tokens, e.g. on logout (PortSwigger).
- Enforce a strict host whitelist for header parameters such as jku, and secure kid against path traversal / SQL injection (PortSwigger).
Sources
- JWT attacks - Web Security Academy PortSwigger, 2026
- Algorithm confusion attacks - Web Security Academy PortSwigger, 2026
- JSON Web Token for Java Cheat Sheet OWASP Cheat Sheet Series, 2026