CVE-2026-28498
Authlib OIDC hash validation bypass via fail-open on unknown algorithm.
- CVSS 8.2
- CWE-354
- Cryptographic
- Remote
Authlib is a Python library which builds OAuth and OpenID Connect servers. Prior to version 1.6.9, a library-level vulnerability was identified in the Authlib Python library concerning the validation of OpenID Connect (OIDC) ID Tokens. Specifically, the internal hash verification logic (_verify_hash) responsible for validating the at_hash (Access Token Hash) and c_hash (Authorization Code Hash) claims exhibits a fail-open behavior when encountering an unsupported or unknown cryptographic algorithm. This flaw allows an attacker to bypass mandatory integrity protections by supplying a forged ID Token with a deliberately unrecognized alg header parameter. The library intercepts the unsupported state and silently returns True (validation passed), inherently violating fundamental cryptographic design principles and direct OIDC specifications. This issue has been patched in version 1.6.9.
- CWE
- CWE-354
- CVSS base score
- 8.2
- Published
- 2026-03-16
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Cryptographic
- Subcategory
- Cryptographic Implementation Error
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- Authlib
- Fixed by upgrading
- Yes
Solution
Upgrade Authlib to version 1.6.9 or later.
Vulnerable code sample
import hashlib
from base64 import urlsafe_b64encode
# A list of supported JWS algorithms, mimicking Authlib's internal list.
# The vulnerability lies in how algorithms *not* in this list are handled.
JWS_ALGORITHMS = [
'HS256', 'HS384', 'HS512',
'RS256', 'RS384', 'RS512',
'ES256', 'ES384', 'ES512',
'PS256', 'PS384', 'PS512',
]
def to_bytes(s, encoding='utf-8'):
"""Helper to ensure string is in bytes."""
if isinstance(s, bytes):
return s
return str(s).encode(encoding)
class VulnerableOIDCValidator:
"""
A minimal class to represent the vulnerable logic in Authlib versions
prior to the fix for CVE-2024-28498. The CVE in the prompt has a typo.
"""
def _verify_hash(self, alg: str, h: bytes, s: str) -> bool:
"""
This is a replica of the vulnerable _verify_hash method from
Authlib's oidc/core/claims.py before it was patched.
The vulnerability is the "fail-open" behavior: if an unsupported
algorithm is provided, the function silently returns True instead of
raising an error or returning False.
"""
# VULNERABLE LOGIC: If 'alg' is unknown, validation passes.
if alg not in JWS_ALGORITHMS:
# An attacker can use an algorithm like 'none' or any invented
# string to bypass this check entirely.
# The function should have raised an error or returned False.
return True
# This part of the code is correct but is bypassed by the flaw.
digest_alg = getattr(hashlib, 'sha' + alg[2:])
digest_size = digest_alg().digest_size
left_most_size = digest_size // 2
value = to_bytes(s)
hashed_value = digest_alg(value).digest()
# The provided hash `h` is compared with the computed hash.
return h == urlsafe_b64encode(hashed_value[:left_most_size]).rstrip(b'=')
def validate_id_token_hashes(self, id_token: dict, access_token: str):
"""
Simulates the validation of at_hash (Access Token hash) from an ID Token.
"""
id_token_header_alg = id_token.get('header', {}).get('alg')
at_hash = id_token.get('at_hash')
print(f"--- Validating with ID Token signed with alg='{id_token_header_alg}' ---")
# Validate at_hash
if at_hash:
print(f"Validating at_hash: '{at_hash.decode()}'...")
at_hash_valid = self._verify_hash(id_token_header_alg, at_hash, access_token)
print(f"Result: {'PASSED' if at_hash_valid else 'FAILED'}")
if not at_hash_valid:
return False
print("---------------------------------------------------\n")
return True
# --- DEMONSTRATION OF THE VULNERABILITY ---
validator = VulnerableOIDCValidator()
# SCENARIO 1: A legitimate ID Token with a valid at_hash.
# The `alg` used for the ID Token signature is 'RS256'.
# The at_hash is correctly calculated for the given access token.
print("SCENARIO 1: Legitimate validation with a supported algorithm.")
legit_access_token = "YjJiMDg1ZTQ1Zjc2NTYyZGM3N2YxZTQ0YTg4MGE3YmU"
# Correct at_hash for the token above, using RS256's SHA-256
#
# How to calculate:
# 1. SHA-256 hash of the access token:
# hashlib.sha256(b"YjJiMDg1ZTQ1Zjc2NTYyZGM3N2YxZTQ0YTg4MGE3YmU").digest()
# 2. Take the left-most 128 bits (16 bytes).
# 3. Base64url encode the result.
#
legit_at_hash = b'77nfz2-77F1_x3b3aSCp_g'
legit_id_token = {
'header': {'alg': 'RS256'},
'at_hash': legit_at_hash,
}
validator.validate_id_token_hashes(legit_id_token, legit_access_token)
# SCENARIO 2: An attacker crafts a forged ID Token.
# They provide a completely invalid `at_hash`.
# To bypass validation, they set the ID Token's signing algorithm ('alg')
# to an unsupported value like 'none' or any other unknown string.
print("SCENARIO 2: Attack using an unsupported algorithm to bypass validation.")
forged_access_token = "this_is_a_stolen_or_unrelated_access_token"
forged_id_token = {
# The attacker sets the 'alg' to something the library doesn't support.
'header': {'alg': 'none'},
# The hash is completely fake and does not match the token.
'at_hash': b'attacker_provided_junk_hash',
}
# Due to the vulnerability, the _verify_hash function will return True
# for the at_hash check because 'none' is not in JWS_ALGORITHMS.
# This allows the forged ID Token to be accepted as valid.
validation_result = validator.validate_id_token_hashes(
forged_id_token,
forged_access_token
)
if validation_result:
print("VULNERABILITY CONFIRMED: The forged ID Token was accepted as valid.")
else:
print("Vulnerability not present. The forged ID Token was rejected.")Patched code sample
The following Python code demonstrates both the vulnerable logic and the subsequent fix for the described vulnerability. The vulnerable function `_verify_hash_vulnerable` replicates the "fail-open" behavior, while the `_verify_hash_fixed` function shows the corrected "fail-closed" logic.
```python
import hashlib
from base64 import urlsafe_b64encode, urlsafe_b64decode
# --- Helper data and functions for demonstration ---
# Allowed JWS algorithms for hash verification, mapping to hashlib functions.
# This simulates the internal list used by Authlib.
JWS_ALGORITHMS = {
'HS256': 'sha256',
'HS384': 'sha384',
'HS512': 'sha512',
'RS256': 'sha256',
'RS384': 'sha384',
'RS512': 'sha512',
}
# A simple custom exception to represent an invalid claim error.
class InvalidClaimError(Exception):
pass
def to_bytes(s, encoding='utf-8'):
"""Helper to ensure a string is bytes."""
if isinstance(s, bytes):
return s
return s.encode(encoding)
# --- Vulnerable and Fixed Function Definitions ---
def _verify_hash_vulnerable(alg, value, hash_value):
"""
Represents the vulnerable logic prior to the fix.
It "fails open" by returning True for any unsupported algorithm.
"""
if alg not in JWS_ALGORITHMS:
# VULNERABILITY: Silently returns True for any unknown algorithm,
# bypassing the actual hash integrity check.
return True
# This part of the logic is only reached for supported algorithms
digest_alg_name = JWS_ALGORITHMS[alg]
digest_alg = getattr(hashlib, digest_alg_name)
digest_value = digest_alg(value.encode('ascii')).digest()
left_half = digest_value[:int(len(digest_value) / 2)]
# Padding is added for urlsafe_b64decode, which can be strict
return left_half == urlsafe_b64decode(to_bytes(hash_value) + b'==')
def _verify_hash_fixed(alg, value, hash_value):
"""
Represents the fixed logic, as implemented in the patched version.
It "fails closed" by raising an error for unsupported algorithms.
"""
if alg not in JWS_ALGORITHMS:
# FIX: Explicitly raises an error, rejecting the token instead of
# silently accepting it. This is a secure "fail-closed" approach.
raise InvalidClaimError(f'Unsupported hash algorithm "{alg}"')
# This part of the logic is only reached for supported algorithms
digest_alg_name = JWS_ALGORITHMS[alg]
digest_alg = getattr(hashlib, digest_alg_name)
digest_value = digest_alg(value.encode('ascii')).digest()
left_half = digest_value[:int(len(digest_value) / 2)]
# Padding is added for urlsafe_b64decode, which can be strict
return left_half == urlsafe_b64decode(to_bytes(hash_value) + b'==')
# --- Demonstration of the Vulnerability and Fix ---
if __name__ == "__main__":
# Dummy data representing an access token and a forged hash
access_token_value = "YmFkLWFjY2Vzcy10b2tlbi12YWx1ZQ"
forged_hash_value = "forged_hash_that_does_not_match"
# The attacker specifies an algorithm that is not supported by the library
attack_alg = "unsupported_alg_123"
print("--- Demonstrating the Vulnerability and Fix ---")
print(f"Access Token: '{access_token_value}'")
print(f"Forged Hash: '{forged_hash_value}'")
print(f"Algorithm provided by attacker: '{attack_alg}'\n")
# 1. Test the VULNERABLE function
print("[1] Running VULNERABLE code...")
try:
is_valid = _verify_hash_vulnerable(
attack_alg, access_token_value, forged_hash_value
)
print(f"Result: {is_valid}")
if is_valid:
print("VULNERABILITY CONFIRMED: The forged hash was accepted because the "
"unsupported algorithm caused the check to 'fail open'.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
print("-" * 50)
# 2. Test the FIXED function
print("[2] Running FIXED code...")
try:
_verify_hash_fixed(
attack_alg, access_token_value, forged_hash_value
)
except InvalidClaimError as e:
print("Result: Caught the expected exception.")
print(f"FIX CONFIRMED: The unsupported algorithm was correctly rejected "
f"with an error: '{e}'")
except Exception as e:
print(f"An unexpected error occurred: {e}")Payload
{
"header": {
"alg": "unsupported_alg",
"typ": "JWT"
},
"payload": {
"iss": "https://malicious-issuer.com",
"sub": "victim_user",
"aud": "vulnerable_client_id",
"exp": 2147483647,
"iat": 1640995200,
"at_hash": "forged_hash_that_will_not_be_verified"
}
}
Cite this entry
@misc{vaitp:cve202628498,
title = {{Authlib OIDC hash validation bypass via fail-open on unknown algorithm.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-28498},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-28498/}}
}
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 ::
