{"id":"GHSA-jj7h-c3hg-m23x","aliases":["CVE-2026-114614"],"title":"Authlib: is_secure_transport() loopback exemption can be bypassed with a userinfo component, allowing cleartext-HTTP URLs to non-loopback hosts","description":"### Summary\n`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.\n\n### Details\n**Affected versions:** `authlib >= 0.13, <= 1.7.2` (all releases containing the loopback prefix exemption; verified against 1.7.2, the current latest)\n\nThe vulnerable function, from `authlib/common/security.py` at v1.7.2:\n\n```python\n13  def is_secure_transport(uri):\n14      \"\"\"Check if the uri is over ssl.\"\"\"\n15      if os.getenv(\"AUTHLIB_INSECURE_TRANSPORT\"):\n16          return True\n17\n18      uri = uri.lower()\n19      return uri.startswith(\n20          (\"https://\", \"http://localhost:\", \"http://127.0.0.1:\", \"http://[::1]:\")\n21      )\n```\n\nPer 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/`).\n\n`is_secure_transport()` is consumed at the following locations in v1.7.2, none of which independently reject URLs containing a userinfo component:\n\n- `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`).\n- `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.\n- `authlib/oidc/rpinitiated/discovery.py:12` — validation of `end_session_endpoint` in provider metadata.\n- `authlib/oauth2/rfc6749/errors.py:64` and `authlib/oauth1/rfc5849/errors.py:33` — governs how `redirect_uri` is rendered in error bodies (cosmetic).\n\nThe `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.\n\n### PoC\nAgainst the current latest release (1.7.2):\n\n```sh\npython3 -m venv venv\n./venv/bin/pip install 'authlib==1.7.2'\n./venv/bin/python poc.py\n```\n\n`poc.py`:\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nis_secure_transport() must return True only for https:// URLs, or http://\nURLs whose host is a loopback address (localhost / 127.0.0.1 / ::1).\n\nBypass: http://localhost:80@evil.example/cb has host 'evil.example'\n(userinfo = 'localhost:80') per RFC 3986, but matches the prefix\n'http://localhost:' and is therefore accepted as secure.\n\"\"\"\nfrom urllib.parse import urlparse\nfrom authlib.common.security import is_secure_transport\n\ncases = [\n    \"http://localhost:80@evil.example/cb\",\n    \"http://127.0.0.1:80@evil.example/cb\",\n    \"http://localhost:@evil.example/cb\",\n]\n\nvulnerable = False\nfor url in cases:\n    parsed = urlparse(url)\n    ok = is_secure_transport(url)\n    print(f\"is_secure_transport({url!r}) = {ok}  \"\n          f\"[actual host = {parsed.hostname!r}, scheme = {parsed.scheme!r}]\")\n    if ok and parsed.hostname not in (\"localhost\", \"127.0.0.1\", \"::1\") and parsed.scheme != \"https\":\n        vulnerable = True\n\nif vulnerable:\n    print(\"\\nVULNERABLE: is_secure_transport() returned True for a plain-HTTP \"\n          \"URL whose host is NOT a loopback address.\")\nelse:\n    print(\"\\nNOT VULNERABLE\")\n\nassert vulnerable, \"expected bypass not observed\"\n```\n\nExpected output:\n\n```\nis_secure_transport('http://localhost:80@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']\nis_secure_transport('http://127.0.0.1:80@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']\nis_secure_transport('http://localhost:@evil.example/cb') = True  [actual host = 'evil.example', scheme = 'http']\n\nVULNERABLE: is_secure_transport() returned True for a plain-HTTP URL whose host is NOT a loopback address.\n```\n\n### Impact\nDeployments 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.\n\n**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.","severity":"low","score":2.6,"published":"2026-10-27T15:37:48.000Z","modified":"2026-10-27T15:47:24.634Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"PyPI","name":"authlib","ranges":[{"events":[{"introduced":"0"},{"fixed":"1.8.0"}],"type":"ECOSYSTEM"}],"versions":["0.1","0.10","0.11","0.12","0.12.1","0.13","0.14","0.14.1","0.14.2","0.14.3","0.15","0.15.1","0.15.2","0.15.3","0.15.4","0.15.5","0.15.6","0.1rc0","0.2","0.2.1","0.3","0.4","0.4.1","0.5","0.5.1","0.6","0.7","0.8","0.9","1.0.0","1.0.0a1","1.0.0a2","1.0.0b1","1.0.0b2","1.0.0rc1","1.0.1","1.1.0","1.2.0","1.2.1","1.3.0","1.3.1","1.3.2","1.4.0","1.4.1","1.5.0","1.5.1","1.5.2","1.6.0","1.6.1","1.6.10","1.6.11","1.6.12","1.6.2","1.6.3","1.6.4","1.6.5","1.6.6","1.6.7","1.6.8","1.6.9","1.7.0","1.7.1","1.7.2"],"fixed":["1.8.0"]}],"references":["https://github.com/authlib/authlib/security/advisories/GHSA-jj7h-c3hg-m23x","https://nvd.nist.gov/vuln/detail/CVE-2026-114614","https://github.com/authlib/authlib/commit/95ad1f90acf0c7b9cf2a8f25e6eb3f1135b8dc70","https://github.com/authlib/authlib","https://github.com/authlib/authlib/releases/tag/v1.8.0"],"sources":[{"name":"OSV","id":"GHSA-jj7h-c3hg-m23x","modified":"2026-10-27T15:47:24.634039221Z","url":"https://osv.dev/vulnerability/GHSA-jj7h-c3hg-m23x"}],"assessments":[{"source":"OSV","vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:N/A:N","score":2.6}]}