diff --git a/src/specify_cli/authentication/config.py b/src/specify_cli/authentication/config.py index 968cadc46..8d1faf80c 100644 --- a/src/specify_cli/authentication/config.py +++ b/src/specify_cli/authentication/config.py @@ -196,7 +196,15 @@ def find_entries_for_url( url: str, entries: list[AuthConfigEntry] ) -> list[AuthConfigEntry]: """Return entries whose ``hosts`` match the hostname of *url*.""" - hostname = (urlparse(url).hostname or "").lower() + # A malformed authority (e.g. an unterminated IPv6 bracket "https://[::1") + # makes urlparse/hostname raise ValueError. Treat that the same as a + # host-less URL: no entry can match, so return no matches rather than + # leaking a raw ValueError out of the shared HTTP client (build_request / + # open_url call this before any URL validation). + try: + hostname = (urlparse(url).hostname or "").lower() + except ValueError: + return [] if not hostname: return [] return [ diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 917c698db..e8bc2b678 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -315,6 +315,20 @@ class TestFindEntriesForUrl: def test_empty_url_returns_empty(self): assert find_entries_for_url("", [_github_entry()]) == [] + @pytest.mark.parametrize( + "url", + [ + "https://[::1", # unterminated ipv6 bracket + "https://[not-an-ip]/file", # bracketed non-ip host + ], + ) + def test_malformed_url_returns_empty(self, url): + # A malformed authority makes urlparse/hostname raise ValueError. + # Since no entry can match such a URL, this must return no matches + # (like a host-less URL) rather than leaking a raw ValueError out of + # the shared HTTP client. + assert find_entries_for_url(url, [_github_entry()]) == [] + def test_empty_entries_returns_empty(self): assert find_entries_for_url("https://github.com/org/repo", []) == []