diff --git a/src/specify_cli/catalogs.py b/src/specify_cli/catalogs.py index ec8014127..e4df8eae2 100644 --- a/src/specify_cli/catalogs.py +++ b/src/specify_cli/catalogs.py @@ -71,8 +71,12 @@ class CatalogStackBase: """Validate that a catalog URL uses HTTPS, except localhost HTTP.""" from urllib.parse import urlparse - parsed = urlparse(url) - is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1") + try: + parsed = urlparse(url) + hostname = parsed.hostname + except ValueError: + raise cls._error(f"Catalog URL is malformed: {url}") from None + is_localhost = hostname in ("localhost", "127.0.0.1", "::1") if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost): raise cls._error( f"Catalog URL must use HTTPS (got {parsed.scheme}://). " @@ -81,7 +85,7 @@ class CatalogStackBase: # Check hostname, not netloc: netloc is truthy for host-less URLs like # "https://:8080" or "https://user@", so the host guarantee this error # promises would not actually hold. hostname is None in those cases (#3209). - if not parsed.hostname: + if not hostname: raise cls._error("Catalog URL must be a valid URL with a host.") def _load_catalog_config(self, config_path: Path) -> list[CatalogEntry] | None: diff --git a/tests/integrations/test_integration_catalog.py b/tests/integrations/test_integration_catalog.py index 44f4a1d78..a0323b3ea 100644 --- a/tests/integrations/test_integration_catalog.py +++ b/tests/integrations/test_integration_catalog.py @@ -84,6 +84,20 @@ class TestCatalogURLValidation: with pytest.raises(IntegrationCatalogError, match="valid URL"): IntegrationCatalog._validate_catalog_url(url) + @pytest.mark.parametrize( + "url", + [ + "https://[::1", # unclosed ipv6 bracket + "https://[not-an-ip]/c.json", # bracketed non-ip host + ], + ) + def test_malformed_url_rejected_cleanly(self, url): + # A malformed authority makes urlparse/hostname raise ValueError. The + # validator must turn that into its normal catalog error, not leak a + # raw ValueError to the caller. + with pytest.raises(IntegrationCatalogError, match="malformed"): + IntegrationCatalog._validate_catalog_url(url) + # --------------------------------------------------------------------------- # IntegrationCatalog — active catalogs