---
id: CVE-2026-114614
aliases:
  - GHSA-jj7h-c3hg-m23x
title: >-
  Authlib: is_secure_transport() loopback exemption can be bypassed with a
  userinfo component, allowing cleartext-HTTP URLs to non-loopback…
summary: >-
  Authlib: is_secure_transport() loopback exemption can be bypassed with a
  userinfo component, allowing cleartext-HTTP URLs to non-loopback hosts
severity: low
cvss: 2.6
cvssVector: 'CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:N/A:N'
vendor: authlib
product: authlib
ecosystem: pip
affected:
  - authlib < 1.8.0
patched:
  - authlib 1.8.0
published: '2026-10-27'
updated: '2026-10-27'
sourceUpdated: '2026-10-27T15:47:24.634039221Z'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-jj7h-c3hg-m23x'
references:
  - url: 'https://github.com/authlib/authlib/security/advisories/GHSA-jj7h-c3hg-m23x'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-114614'
  - url: >-
      https://github.com/authlib/authlib/commit/95ad1f90acf0c7b9cf2a8f25e6eb3f1135b8dc70
  - url: 'https://github.com/authlib/authlib'
  - url: 'https://github.com/authlib/authlib/releases/tag/v1.8.0'
  - url: 'https://github.com/advisories/GHSA-jj7h-c3hg-m23x'
  - url: >-
      https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-114614.json
  - url: 'https://access.redhat.com/security/cve/CVE-2026-114614'
  - url: 'https://bugzilla.redhat.com/show_bug.cgi?id=2559389'
  - url: 'https://www.cve.org/CVERecord?id=CVE-2026-114614'
tags:
  - osv
  - pip
  - ghsa
  - csaf
  - vex
  - red-hat
  - cve.org
epss: 0.00177
epssPercentile: 0.07207
cwe:
  - CWE-319
  - CWE-601
  - CWE-693
ingestedAt: '2026-10-22T22:00:50.435Z'
---

## Overview

### Summary
`authlib.common.security.is_secure_transport()` is used throughout Authlib to enforce the OAuth 2.0 / OIDC requirement that endpoint and redirect URIs use TLS, with an RFC 8252 §7.3 exemption for loopback `http://` URIs. The check is implemented as a textual `str.startswith` prefix match, so a URL such as `http://localhost:80@evil.example/cb` — whose actual host is `evil.example` and whose scheme is plain `http` — is accepted as "secure". An attacker who can supply a URL to any of the sinks that rely on this check (OIDC RP-Initiated Logout dynamic client registration, RFC 8414 authorization-server metadata validation, error-redirect rendering) can smuggle a cleartext non-loopback URL past the TLS-required guard. This is a defense-in-depth / spec-conformance failure; severity is low.

### Details
**Affected versions:** `authlib >= 0.13, <= 1.7.2` (all releases containing the loopback prefix exemption; verified against 1.7.2, the current latest)

The vulnerable function, from `authlib/common/security.py` at v1.7.2:

```python
13  def is_secure_transport(uri):
14      """Check if the uri is over ssl."""
15      if os.getenv("AUTHLIB_INSECURE_TRANSPORT"):
16          return True
17
18      uri = uri.lower()
19      return uri.startswith(
20          ("https://", "http://localhost:", "http://127.0.0.1:", "http://[::1]:")
21      )
```

Per RFC 3986 §3.2 the authority component of a URI is `[ userinfo "@" ] host [ ":" port ]`. The URL `http://localhost:80@evil.example/cb` therefore has userinfo `localhost:80` and host `evil.example`. Because the check is a raw prefix match against the full URL string, the `http://localhost:` prefix matches the userinfo portion and the function returns `True` even though the scheme is `http` and the host is an arbitrary external name. The trailing `:` in each allow-list entry — presumably intended to force a `host:port` shape and reject `http://localhost.evil.com` — is also satisfied by the `user:password` separator inside userinfo, so `http://localhost:@evil.example/` matches too. The same construction works against all three loopback prefixes (`http://127.0.0.1:...@evil.example/`, `http://[::1]:...@evil.example/`).

`is_secure_transport()` is consumed at the following locations in v1.7.2, none of which independently reject URLs containing a userinfo component:

- `authlib/oidc/rpinitiated/registration.py:50-57` — validation of `post_logout_redirect_uris` during OIDC dynamic client registration for public clients (`token_endpoint_auth_method == "none"`). The preceding `is_valid_url()` check (`authlib/common/urls.py:142-146`) only requires `parsed.scheme and parsed.hostname` and does **not** reject userinfo, so the crafted URL passes both guards. A public client can therefore register `http://localhost:80@attacker.example/cb` as a post-logout redirect target; the OpenID Provider will later 302 the end-user's browser to that cleartext non-loopback URL (carrying `state`).
- `authlib/oauth2/rfc8414/models.py:61,75,100,115,123,248,284` — validation of `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`, `registration_endpoint`, `revocation_endpoint`, `introspection_endpoint` in Authorization Server Metadata / OIDC Discovery documents. A relying party using `AuthorizationServerMetadata.validate()` would accept endpoint values of this form as satisfying the "MUST use https" requirement when they do not.
- `authlib/oidc/rpinitiated/discovery.py:12` — validation of `end_session_endpoint` in provider metadata.
- `authlib/oauth2/rfc6749/errors.py:64` and `authlib/oauth1/rfc5849/errors.py:33` — governs how `redirect_uri` is rendered in error bodies (cosmetic).

The `http://localhost:` prefix was introduced in commit `0225c86b` (first released in v0.13); `http://127.0.0.1:` and `http://[::1]:` were added later in the 1.6.x line. All variants share the same prefix-match weakness.

### PoC
Against the current latest release (1.7.2):

```sh
python3 -m venv venv
./venv/bin/pip install 'authlib==1.7.2'
./venv/bin/python poc.py
```

`poc.py`:

```python
#!/usr/bin/env python3
"""
is_secure_transport() must return True only for https:// URLs, or http://
URLs whose host is a loopback address (localhost / 127.0.0.1 / ::1).

Bypass: http://localhost:80@evil.example/cb has host 'evil.example'
(userinfo = 'localhost:80') per RFC 3986, but matches the prefix
'http://localhost:' and is therefore accepted as secure.
"""
from urllib.parse import urlparse
from authlib.common.security import is_secure_transport

cases = [
    "http://localhost:80@evil.example/cb",
    "http://127.0.0.1:80@evil.example/cb",
    "http://localhost:@evil.example/cb",
]

vulnerable = False
for url in cases:
    parsed = urlparse(url)
    ok = is_secure_transport(url)
    print(f"is_secure_transport({url!r}) = {ok}  "
          f"[actual host = {parsed.hostname!r}, scheme = {parsed.scheme!r}]")
    if ok and parsed.hostname not in ("localhost", "127.0.0.1", "::1") and parsed.scheme != "https":
        vulnerable = True

if vulnerable:
    print("\nVULNERABLE: is_secure_transport() returned True for a plain-HTTP "
          "URL whose host is NOT a loopback address.")
else:
    print("\nNOT VULNERABLE")

assert vulnerable, "expected bypass not observed"
```

Expected output:

```
is_secure_transport('http://localhost:80@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']
is_secure_transport('http://127.0.0.1:80@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']
is_secure_transport('http://localhost:@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']

VULNERABLE: is_secure_transport() returned True for a plain-HTTP URL whose host is NOT a loopback address.
```

### Impact
Deployments are affected if they rely on `is_secure_transport()` — directly or via the Authlib call sites listed above — to reject cleartext non-loopback URLs supplied by an external party. The most realistic path is an OpenID Provider that enables OIDC dynamic client registration and RP-Initiated Logout: a public client can register a `post_logout_redirect_uris` entry of the form `http://localhost:80@attacker.example/…`, which the provider will later use as an open-redirect target over plain HTTP. Relying parties that call `AuthorizationServerMetadata.validate()` on fetched discovery documents are also affected, though exploiting that sink requires the attacker to influence the metadata document. This does **not** bypass the OAuth 2.0 redirect-URI exact-match registration check and does not by itself yield authentication bypass or token theft; it defeats a TLS-required hardening check.

**Suggested fix:** Parse the URI with `urllib.parse.urlsplit()` and return `True` only if `scheme == "https"`, or `scheme == "http"` and `hostname in {"localhost", "127.0.0.1", "::1"}`; additionally reject any URI whose parsed `username`/`password` is non-empty at these validation sites.

## Affected packages

- `authlib < 1.8.0`

## Remediation

Upgrade to a patched release:

- `authlib 1.8.0`

## Vendor advisories

- **Red Hat VEX** · Low · affected: Exploit Intelligence, Lightspeed Core, Migration Toolkit for Applications 8, OpenShift Lightspeed, Red Hat Ansible Automation Platform 2, Red Hat Enterprise Linux command line assistant, … · no fix planned: Exploit Intelligence, Red Hat Quay 3, Lightspeed Core, Migration Toolkit for Applications 8, … · updated 2026-10-23 · [vex](https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-114614.json)
