July 18, 2026
Don’t Trust the Token — The Art of JWT Attacks
One token. Every door open.
By Muhab A.
8 min read
Every time you log into a website, you stay authenticated while browsing. But have you ever wondered… how does the server know it's still you?
Before we dive into JWT, let's first go back to how authentication traditionally worked.
When you log in to a website, the server verifies your credentials, creates a Session, stores it, and sends your browser a Session ID, usually inside a cookie.
With every request, the browser automatically sends this Session ID back to the server, allowing it to retrieve your session and verify that you're authenticated.
This model is called Stateful Authentication because the server must store and manage session data for every user.
As applications grew, this approach became harder to scale. Multiple servers require shared session storage and modern APIs prefer Bearer Tokens over cookies.
This is where JSON Web Tokens (JWT) come in, introducing a Stateless Authentication model.
What is a JWT?
- A standardized format for sending cryptographically signed JSON data between systems. Used for authentication, authorization, and session management. Unlike traditional sessions, the server doesn't store user state; instead, the required information is carried inside the token itself.
- A JWT always consists of three parts separated by dots (.):
Header.Payload.Signature(Looks like:xxxxx.yyyyy.zzzzz)
- Header (The Metadata)
Acts as the token's ID card. It tells the server how to process and verify the token.
typ(Type): Usually set toJWT.alg(Algorithm): The hashing/signing algorithm being used (e.g.,HS256,RS256, ornone).kid(Key ID): An optional parameter indicating which specific key the server should use to verify the signature.
{
"alg": "HS256",
"typ": "JWT",
"kid": "key_1"
}{
"alg": "HS256",
"typ": "JWT",
"kid": "key_1"
}This JSON is Base64URL encoded to form the first part of the token (
xxxxx).
- Payload (The Claims)
The core of the token where the actual user data (claims) is stored.
- Registered Claims: Standardized fields for interoperability.
iss(Issuer): Who created the token.sub(Subject): Who the token is about (often the User ID).exp(Expiration Time): Unix timestamp when the token expires.iat(Issued At): When the token was created.- Public/Private Claims: Custom application data like user roles or identifiers.
{
"sub": "1234567890",
"name": "Muhab",
"role": "admin",
"exp": 1718582400
}{
"sub": "1234567890",
"name": "Muhab",
"role": "admin",
"exp": 1718582400
}This JSON is also Base64URL encoded to form the second part of the token (
yyyyy).
- Signature (The Security Seal)
The signature is what makes the JWT secure. It guarantees two things:
- Authenticity: The token was actually issued by the trusted server.
- Integrity: The Header and Payload were not tampered with during transit.
How it is generated:
The server takes the encoded Header, the encoded Payload, joins them with a dot, and hashes them using the algorithm specified in the Header (alg) along with a Secret Key that only the server knows.
Signature =
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret_key)Signature =
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret_key)The resulting hash is Base64URL encoded to form the third and final part of the token(
zzzzz).
→ How it prevents tampering:
If an attacker decodes the token, changes "role": "user" to "role": "admin", and sends it back:
- The server receives the token and recalculates the signature using its own secret key.
- Because the attacker modified the payload but didn't have the secret key to generate a valid new signature, the server's calculated signature will not match the attacker's provided signature.
- The server immediately rejects the token.
→ CRITICAL SECURITY NOTE: Base64URL is NOT encryption; it is just encoding. Anyone who intercepts the token can easily decode and read the Payload. Never put sensitive data (like passwords or financial info) inside a JWT payload.
At first glance, this seems secure. If an attacker changes anything inside the payload, the signature becomes invalid and the server rejects the token.
- So… where do JWT vulnerabilities actually come from?
Where Does the Trust Break?
JWT itself isn't the vulnerability.
The vulnerability appears when the server blindly trusts data that comes from the client.
A JWT contains several fields that are completely controlled by the user before the server verifies them, such as:
algkidjku
If the server makes security decisions based on these values without proper validation, an attacker may be able to forge tokens, bypass authentication, or even execute server-side attacks.
So instead of attacking the cryptography itself, most JWT attacks target the implementation.
Let's look at the most common ones.
1. Signature Not Verified
The first thing I try when testing a JWT is surprisingly simple:
What happens if I modify the payload without touching the signature?
For example, changing:
{
"role": "user"
}{
"role": "user"
}to:
{
"role": "admin"
}{
"role": "admin"
}If the application still accepts the token, it usually means the signature isn't being verified at all.
This often happens when the application uses
decode()instead ofverify().
- While
decode()only reads the token,verify()actually validates its cryptographic signature.
Impact
If the signature isn't verified, I can modify any claim inside the payload and impersonate other users or escalate my privileges without knowing the secret key.
2. The alg:none Attack
If the signature is being verified correctly, the next question is:
Can I influence how the server verifies it?
Since the verification algorithm is specified by the alg field inside the JWT header, I always check whether I can modify it.
For example, changing:
{
"alg": "HS256"
}{
"alg": "HS256"
}to:
{
"alg": "none"
}{
"alg": "none"
}and removing the signature completely.
Some older or misconfigured JWT libraries blindly trusted the algorithm supplied by the client. As a result, they skipped signature verification entirely and accepted the unsigned token.
3. Weak Secret Keys
Suppose the application correctly verifies the signature and rejects alg:none.
The next thing I want to know is:
How strong is the signing secret?
- If the application uses HS256, the signature relies on a shared secret between the client and the server.
Weak or predictable secrets such as:
secret
password
123456
jwt_secretsecret
password
123456
jwt_secretcan often be recovered through an offline brute-force attack using tools like Hashcat.
Once the correct secret is found, I can generate completely valid JWTs with any claims I want, making privilege escalation or user impersonation trivial.
4. Trusting User-Controlled Headers
At this point, the signature is verified and the secret isn't weak.
So our attention shifts to another part of the JWT: The Header.
The header isn't just metadata — it's entirely controlled by the client.
Whenever I inspect a JWT, one of the first things I look for is unusual header parameters such as:
kidjwkjku
Each of these influences how the server verifies the token.
If the server blindly trusts any of them, I may be able to make it verify my own token instead of the legitimate one. and this opens the door to several powerful attacks:
1. JWK Injection
Can I provide my own public key inside the JWT?
Some apps support the jwk header parameter and automatically use the embedded key to verify the token.
If the server accepts any user-supplied JWK, I can generate my own RSA key pair, modify the payload, embed my public key in the header, sign the token with my private key, and the server may accept it as legitimate:
2. JKU Injection
If the application doesn't accept an embedded JWK, the next thing I check is the jku header.
Instead of containing the key itself, jku tells the server where to download it from.
If I can control this URL, I simply host my own JWK Set, point the jku parameter to my server, sign the token with my private key, and wait to see whether the application fetches and trusts my key.
3. Kid Injection
Normally, it identifies which verification key the server should use.
But if it's used insecurely — for example, concatenated into a file path or a database query — it can become an injection point.
Depending on the implementation, this may lead to attacks such as:
- Path Traversal
- SQL Injection
- Loading attacker-controlled keys
Once I can influence the verification key, forging valid JWTs becomes possible.
5. Algorithm Confusion
Suppose none of the previous attacks worked.
The app verifies signatures correctly, uses a strong secret (or RSA), and doesn't trust user-controlled headers. So the next question becomes:
Can I trick the server into verifying the token using a completely different algorithm?
This is exactly what the Algorithm Confusion attack attempts to do.
Some vulnerable implementations support multiple algorithms but blindly trust the alg value supplied by the client.
By changing the algorithm from RS256 to HS256, it's sometimes possible to make the server use its public RSA key as an HMAC secret, allowing a forged token to pass signature verification.
But why does this work?
Under normal circumstances, an application using RS256 verifies JWTs with its public key, while only the server owns the corresponding private key used for signing.
However, if the server blindly trusts the
algvalue supplied by the token, an attacker can change it from RS256 to HS256.
The vulnerable JWT library will then treat the server's public key as an HMAC secret instead of an RSA public key. Since public keys are meant to be public, the attacker already has everything needed to sign arbitrary tokens.
The result? A completely forged JWT that the server considers valid.
Exploitation
- Obtain the server's public key.
- Change the algorithm from RS256 to HS256.
- Modify the payload (e.g., role → admin).
- Sign the token using the public key as the HMAC secret.
- Send the forged JWT.
For a deeper dive into the exploitation steps, check this link.
Useful JWT Tools
- Burp Suite + JWT Editor → The go-to tool for testing JWTs (decode, sign, JWK/JKU injection, Algorithm Confusion, alg:none).
- jwt.io → Decode, inspect, and verify JWTs for quick analysis.
- Hashcat → Offline brute-forcing of weak HS256 secret keys.
- jwt-tool → Automates many JWT attacks, including alg:none, weak secrets, kid, jwk, jku, and Algorithm Confusion.
- sig2n → PortSwigger tool for deriving RSA public keys and performing Algorithm Confusion attacks.
- curl → Retrieve JWKS endpoints (e.g., /.well-known/jwks.json) and test related functionality.
- jq → Pretty-print and analyze JWKS/JSON responses from the command line.
JWT Testing Methodology
When you encounter a JWT during a penetration test, don't immediately jump into exploitation.
→ Start by asking a few simple questions:
1. Which algorithm is being used? (HS256 or RS256?)
2. Can I modify the payload without breaking authentication?
3. Does the server accept alg:none?
4. Is the secret weak enough to brute-force?
5. Are there any interesting header parameters (kid, jwk, jku)?
6. Is the public key exposed?
7. Is the token still valid after logout?
8. Does the payload contain sensitive information?1. Which algorithm is being used? (HS256 or RS256?)
2. Can I modify the payload without breaking authentication?
3. Does the server accept alg:none?
4. Is the secret weak enough to brute-force?
5. Are there any interesting header parameters (kid, jwk, jku)?
6. Is the public key exposed?
7. Is the token still valid after logout?
8. Does the payload contain sensitive information?JWT Pentest Checklist
□ Decode the token □ Identify the algorithm
□ Test signature verification □ Test alg:none
□ Brute-force HS256 secrets □ Inspect kid / jwk / jku
□ Check JWKS endpoint □ Test Algorithm Confusion
□ Inspect sensitive claims □ Test token expiration
□ Test logout/revocation□ Decode the token □ Identify the algorithm
□ Test signature verification □ Test alg:none
□ Brute-force HS256 secrets □ Inspect kid / jwk / jku
□ Check JWKS endpoint □ Test Algorithm Confusion
□ Inspect sensitive claims □ Test token expiration
□ Test logout/revocationPrevention
Most of the attacks covered in this writeup don't exploit a flaw in JWT itself — they exploit how developers implement it.
- Always verify JWT signatures — never rely on decode() alone.
- Explicitly reject tokens using alg: none.
- Whitelist the allowed signing algorithms instead of trusting the alg header.
- Never trust user-controlled header parameters (kid, jku, jwk) without strict validation.
- Restrict jku to trusted domains only.
- Validate and sanitize the kid parameter to prevent path traversal and SQL injection.
- Use long, cryptographically random secrets for HMAC algorithms (HS256, HS384, HS512).
- Store private keys securely and rotate signing keys when necessary.
- Keep JWT expiration times short and validate exp, nbf, and iat claims.
- Validate claims such as iss, aud, and sub before trusting the token.
- Implement proper token revocation for logout or credential changes.
- Never place sensitive information inside the JWT payload, as it can be easily decoded.
- Do not accept JWTs without verifying the expected issuer (iss) and audience (aud).
JWT often gives the impression that it's secure simply because it is signed.
Most successful JWT attacks don't break the cryptography — they exploit the implementation.
Applications end up trusting data that attackers are allowed to control, turning a secure design into an insecure one.
JWT doesn't fail because attackers break cryptography.
It fails because applications trust what should never be trusted.
Never trust the token. Verify it.