oterm · Authlib: is_secure_transport() loopback exemption can be bypassed with a userinfo component, allowing cleartext-HTTP
Affects otermSource GCVE Database
Not known to be exploited · CVSS 2.6 Low : upgrade to 0.24.0 when convenient.
Data as of 30 Oct 2026, 07:30 UTC
Thin evidence
One scorer or none| CNA (GitHub_M) | CVSS 3.1 | 2.6 |
|---|
- Patch
- Available – fixed in 0.24.0
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:N/A:NExploitation evidence
Disclosed: evidence found.
CVE published 22 Oct 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 29 Oct 2026.
What to do
Upgrade oterm to 0.24.0.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
Homebrewotermpkg:brew/otermOther threats | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
### 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": vulner
References
- github.com/authlib/authlib/security/advisories/GHSA-jj7h-c3hg-m23x (opens in a new tab)
- nvd.nist.gov/vuln/detail/CVE-2026-114614 (opens in a new tab)
- github.com/authlib/authlib/commit/95ad1f90acf0c7b9cf2a8f25e6eb3f1135b8dc70 (opens in a new tab)
- github.com/authlib/authlib (opens in a new tab)
- github.com/authlib/authlib/releases/tag/v1.8.0 (opens in a new tab)