Skip to content

Authlib coordinated wave: six-leg OAuth/OIDC control-logic pack fixed in 1.9.0

Published October 27, 2026 (15:37–15:39Z), all fixed in Authlib 1.9.0. Authlib is the default OAuth2/OIDC library across a large slice of Python web infrastructure, so every leg here is "everywhere at once" — same framing as this wiki's framework message-body-reader rule. Companion to the June 8 LiteLLM/Authlib batch (empty-signatures fail-open sibling lives there).

The legs

  1. CVE-2026-114615 / GHSA-7vr2-2fgp-9w7p (high, 1.3.1–1.8.0) — incomplete algorithm-confusion fix: HMAC forgery with a prefixed public key. The CVE-2024-37568 fix is a single anchored check, raw_key.startswith(POSSIBLE_UNSAFE_KEYS) at byte 0, while the asymmetric loader (load_pem_public_key) skips anything before -----BEGIN. One leading byte — newline, space, indentation, UTF-8 BOM, or a comment line — makes the guard miss, and the PEM public key becomes the HMAC secret. Attacker holding only the issuer's published public key mints HS256 tokens that jwt.decode() accepts as authentic. Original attack works again unchanged whenever the stored PEM doesn't start at offset 0.
  2. CVE-2026-114617 / GHSA-c5mf-xmp2-59rc (medium, ≤1.8.0) — ID token disables its own nonce validation. parse_id_token() reads nonce_supported from the token being validated; false sets the expected nonce to None, skipping validation. A captured, validly signed victim ID token replays into an attacker session and authenticates the attacker as the victim at implicit/hybrid RPs. Identical code in the sync and async integration paths — one claim inside the signed envelope edits the verifier's own expectations.
  3. CVE-2026-114618 / GHSA-fch4-3c6x-j4gc (medium, <1.9.0) — default RFC 7523 JWT-bearer grant accepts any audience. Default get_audiences() returns empty, so verify_claims() only checks that aud exists. A legitimately issued assertion for any other audience, presented to a different authorization server sharing the same signing key, yields an access token. Cross-service lateral movement inside any shared-issuer estate.
  4. CVE-2026-114619 / GHSA-q686-2qmg-hqc7 (medium, <1.9.0) — implicit grant checks raw scope, not allowed scope. Parent ImplicitGrant filters requested scope through client.get_allowed_scope() into request.scope; the OIDC leg then decides "is this an OpenID request" from the raw client-supplied request.payload.scope. A client allowed profile but not openid requests scope=openid profile and gets a signed ID token after consent.
  5. CVE-2026-114620 / GHSA-x4rv-c3wx-4cr8 (medium, 1.6.0–1.8.x) — JAR request_uri unvalidated SSRF, pre-auth. The RFC 9101 extension registers on before_get_authorization_grant, so it runs before any authentication; the only gate is that client_id names an existing client — a public identifier. The request_uri parameter goes to the get_request_object() hook raw; the pre-registration allowlist (request_uris / require_request_uri_registration) is defined in the library and never consulted, and both docstrings tell integrators to implement the hook as an unrestricted requests.get(request_uri). Deployments that followed the docs own an unauthenticated SSRF primitive.
  6. CVE-2026-114614 / GHSA-jj7h-c3hg-m23x (low, 0.13–1.7.2) — is_secure_transport() loopback exemption bypassed by userinfo. Textual str.startswith("http://localhost:") accepts http://localhost:80@evil.example/cb whose actual host is evil.example. Smuggles cleartext non-loopback URLs past the TLS-required guard at dynamic client registration, RFC 8414 metadata validation, and error-redirect rendering.

Operator rules

  • Byte-0 anchoring audit (leg 1). Any denylist guard implemented as startswith/startswith-family against input that a different parser later skips-whitespace-before-parsing is bypassable with one prefix byte. For any JWS library: fetch the issuer's public JWKS, POST an HS256 token signed with (a) raw PEM, (b) "\n"+PEM, (c) BOM+PEM, (d) comment-line+PEM, header alg=HS256 + kid of the real RSA/EC key. Acceptance on (b)–(d) only = you found the incomplete-fix shape. This is the third member of this wiki's anchored-check family (see the re.match vs full-match Jupyter origin rule — here the asymmetry is guard-vs-loader instead of guard-vs-matcher).
  • Token-claims-as-verifier-config (leg 2). Enumerate every claim the validation code reads out of the token itself to configure its own checks. Any boolean/optional claim inside the signed envelope that can relax validation (nonce_supported:false style) converts a stolen-but-valid token into a cross-session replay. Auth flow matrix: code (unaffected) vs implicit vs hybrid vs direct parse_id_token() calls — test the non-code legs even when the login button spells "authorization code."
  • Existence-vs-equality on identity claims (leg 3). For every token/assertion verifier, ask whether each claim is checked for presence or for value: aud existence-only = cross-audience replay against any sibling service on the same signing key. Probe: mint a legitimate low-priv assertion for audience A, present to sibling service B's token endpoint.
  • Raw-vs-filtered reads (leg 4). After any normalization/filtering step, sweep for code that re-reads the original payload field instead of the filtered value. Decision-table test: client metadata permits profile only; request openid profile; a signed ID token with sub = scope-restriction bypass.
  • Allowlist-defined-but-never-consulted (leg 5). Grep OAuth servers for config knobs that exist in schema/docstrings (request_uris, require_request_uri_registration) and prove the enforcement site never reads them. Then pre-auth SSRF probe: GET /authorize?client_id=<public-id>&request_uri=http://<owned-callback>/ — a fetch at your callback before any login interaction is the finding. Docstring-shaped hooks ("implement as requests.get(url)") are fingerprintable: expect this leg on any JAR deployment.
  • URL authority grammar (leg 6). Per RFC 3986 the authority is [userinfo@]host[:port]. Any prefix/regex origin or scheme check must be probed with userinfo@host shapes: http://localhost:80@evil.example/, https://evil.example\@localhost/, plus this page family's backslash/@/#/? battery. URL-string checks where a URL parser exists = fail by default.

Validation boundaries

Disposable IdP/RP lab only (Authlib's own provider examples run in minutes). Leg 1 uses the lab's own issued public key; leg 3 uses two lab services sharing a signing key; leg 5 fetches an owned callback with no content. Never mint/replay tokens against real issuers, never point request_uri at metadata or internal ranges.

Skipped in this wave (tracked, no page)

Rustls pack (ECH buffered-data exposure, ServerHello KX-record poisoning, Debug-format secret leak, Brotli pre-auth 1 GiB alloc) — TLS-library internals, Debug-format secret leg tracked; snappy-java trio + compress-zstd panic + dnspython/dd-trace/msgpack/qs/undici DoS singles — memory-safety/availability class per parser precedent; MongoDB BSON regex OOB read (dup of Oct 8 86gh enrichment).