GitHub Security Advisory: GHSA-wvwj-cvrp-7pv5
Package: authlib (PIP)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-347: Improper Verification of Cryptographic Signature
Vulnerable versions: <= 1.6.8
Patched in: 1.6.9

Summary: Authlib JWS JWK Header Injection: Signature Verification Bypass

Details:
## 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

```python
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:

```python
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 `a9e4