JSON Web Tokens are everywhere. Almost every modern web application, mobile backend, and microservice architecture uses them for authentication. And that means almost every pentest engagement I run involves at least one JWT issue, often several.
The frustrating part? Most JWT bugs are not exotic. They are the same handful of mistakes developers have been making since 2015, dressed up in slightly newer libraries. If you understand how the token is built and what each part of the header controls, you already have the keys to roughly 80 percent of JWT findings in the wild.
This post walks through the JWT attacks I actually find on real engagements, in the order I usually test them.
A Quick Refresher
A JWT is three base64url-encoded pieces joined by dots: header.payload.signature.
The header tells the server which algorithm was used to sign the token. The payload contains the claims, things like user id, role, expiry. The signature is what binds them together.
When the server receives a token, it reads the algorithm from the header, recomputes the signature using its own secret or key, and compares. If they match, the token is trusted and the payload is treated as gospel.
That last part is the whole game. Anything that gets the server to accept a token it should not have accepted is a vulnerability.
1. The alg: none Trick
This one should be dead. It is not.
The JWT specification allows an unsigned token by setting "alg": "none" in the header and leaving the signature empty. The intent was to support scenarios where the channel itself guarantees integrity. The reality is that many libraries used to accept these tokens by default, and a surprising number of homegrown JWT handlers still do.
To test it, decode an existing token, change the header to {"alg": "none", "typ": "JWT"}, modify the payload however you like (changing role to admin is a classic), and send the token with an empty third segment but keep the trailing dot.
If the server accepts it, you have an authentication bypass. I have found this in 2025 on more than one greenfield application written by teams that should have known better.
2. RS256 to HS256 Algorithm Confusion
This is the bug everyone wants to find because it is elegant.
RS256 uses an asymmetric key pair. The server signs tokens with a private key and verifies them with a public key. HS256 uses a symmetric key, the same secret signs and verifies.
If a library blindly trusts the algorithm field in the token header, you can take a token that was signed with RS256, change the header to claim HS256, and sign the new token using the server's public key as the HMAC secret. The server, thinking it should use HMAC because the header says so, will load its public key as the HMAC key and verify successfully.
The public key is often easy to find. Common locations include /.well-known/jwks.json, /oauth/keys, response headers, error messages, or simply the company's published certificate.
Steps to exploit:
- Grab the public key in PEM format.
- Decode the original token.
- Change
algtoHS256. - Modify the payload.
- Compute HMAC-SHA256 of
header.payloadusing the public key bytes as the secret. - Append the resulting signature.
Tools like jwt_tool automate every step of this, but doing it manually once or twice will teach you more than a hundred tool runs.
3. Weak Secrets and Brute Force
When a JWT uses HS256, the security of every token in the system depends on the secret being long and random. In practice, developers use things. like secret, password, jwtsecret, the company name, or worst of all, the default value from the framework tutorial they copied.
Drop a captured token into hashcat with mode 16500 and a decent wordlist. Rockyou plus a few rule files will crack lazy secrets in seconds on a modern GPU. Once you have the secret, you can mint arbitrary tokens for any user in the system.
A real engagement memory: a fintech client had rotated their JWT secret after a previous audit. The new secret was the name of their lead developer plus the year. We had it in under a minute.
4. The kid Header and Path Traversal
The kid (key ID) header parameter tells the server which key to use when verifying. Some implementations look up the key by reading a file based on the kid value, or by querying a database.
Anywhere user-controlled input touches a filesystem or a database, the usual suspects apply. If the kid value is concatenated into a file path, try traversal payloads pointing to a file with known contents, then sign the token using those contents as the HMAC key.
A classic payload is "kid": "../../../../../../dev/null". On many systems, /dev/null reads as empty, so you sign your token with an empty string as the HMAC secret. If the server is vulnerable, your token verifies.
For SQL-backed lookups, try injection in the kid value and see if you can return an attacker-controlled value as the key.
5. jku and x5u Header Injection
The jku parameter points to a URL where the server can fetch the JSON Web Key Set used to verify the token. The x5u parameter does the same for an X.509 certificate.
If the server fetches these URLs without strictly validating the host, you can host your own keys on a server you control, point the JWT at your URL, and sign the token with your private key. The server downloads your public key, uses it to verify, and trusts the token.
Mitigations exist. The server should whitelist the hosts it is willing to fetch from. Many do not. Even when whitelists exist, I have seen them defeated by open redirects, SSRF, or simply being too permissive with subdomain wildcards.
6. Claim Logic Failures
Even when the cryptography is bulletproof, the claims inside the token can betray the application.
Things to check on every engagement:
- Does the server actually validate the
expclaim? Try sending an expired token. You would be amazed how often it works. - Does the server validate
nbf(not before) andiat(issued at)? - Is the
aud(audience) claim checked? An attacker with a token for one service may be able to use it against a different service that does not verify the audience. - Is the
iss(issuer) validated? In SSO setups with multiple identity providers, a token issued by a less-trusted provider may be accepted by a service expecting tokens only from a more-trusted one. - Are role or permission claims trusted blindly? If the server reads
"role": "user"from the token instead of re-fetching the user record, anyone who can mint a token can elevate.
7. Token Storage and Leakage
The token itself can be the bug. Look for tokens in:
- URLs (which end up in browser history, server logs, and Referer headers)
- Local storage (accessible to any XSS)
- Console logs in JavaScript bundles
- Error messages
- Third-party analytics or APM tools that capture full requests
I have seen production JWT secrets committed to public GitHub repos, embedded in mobile app binaries that anyone can decompile, and printed to logs that ship to a third-party log aggregator. All real findings, all in the last twelve months.
A Testing Checklist
When I have a token in hand, I run through this sequence:
- Decode it. Read every field. Note the algorithm, the kid, any jku or x5u, the claims.
- Try
alg: none. - If RS256, try the RS256 to HS256 confusion with the public key.
- If HS256, run a wordlist and rule attack against the token.
- Tamper with each claim and observe what the server enforces.
- Send an expired token.
- Try traversal and injection in the kid value.
- If jku or x5u is present, test for SSRF and host whitelist bypass.
- Check where the token is stored client-side and how it flows through the system.
Closing Thought
JWT bugs are not about exotic cryptography. They are about libraries that trust user input, developers who treat the token as a magic box, and threat models that stop at "we signed it."
If you can hold the whole token structure in your head and reason about who controls each piece, the bugs become obvious. Read the source code of the library being used. Read the RFC. The vulnerabilities are usually right there in plain text.
Good hunting.

No comments:
Post a Comment