VAITP Dataset

← Back to the dataset

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.

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 ::