diff --git a/src/specify_cli/catalogs.py b/src/specify_cli/catalogs.py index 774aaa51d..323bb4f74 100644 --- a/src/specify_cli/catalogs.py +++ b/src/specify_cli/catalogs.py @@ -74,6 +74,13 @@ class CatalogStackBase: try: parsed = urlparse(url) hostname = parsed.hostname + # Accessing ``port`` performs urllib's syntax/range validation; + # ``hostname`` alone does not, so a non-numeric or out-of-range + # port would otherwise pass validation here and only fail later, + # at fetch time, as an error this module does not translate -- + # a raw http.client.InvalidURL for a non-numeric port, and a + # socket-layer failure for one that is merely out of range. + _ = parsed.port except ValueError: raise cls._error(f"Catalog URL is malformed: {url}") from None is_localhost = hostname in ("localhost", "127.0.0.1", "::1") diff --git a/tests/integrations/test_integration_catalog.py b/tests/integrations/test_integration_catalog.py index 4688c6a21..e8a9029db 100644 --- a/tests/integrations/test_integration_catalog.py +++ b/tests/integrations/test_integration_catalog.py @@ -116,12 +116,15 @@ class TestCatalogURLValidation: [ "https://[::1", # unclosed ipv6 bracket "https://[not-an-ip]/c.json", # bracketed non-ip host + "https://example.com:notaport/c.json", # non-numeric port + "https://example.com:65536/c.json", # out-of-range port ], ) 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. + # A malformed authority makes urlparse/hostname raise ValueError, and a + # bad port makes ``parsed.port`` raise it. The validator must turn that + # into its normal catalog error, not leak a raw ValueError to the caller + # (or, for a bad port, accept the URL and fail later at fetch time). with pytest.raises(IntegrationCatalogError, match="malformed"): IntegrationCatalog._validate_catalog_url(url)