diff --git a/src/specify_cli/catalogs.py b/src/specify_cli/catalogs.py index 774aaa51d7..00c875ca27 100644 --- a/src/specify_cli/catalogs.py +++ b/src/specify_cli/catalogs.py @@ -74,6 +74,11 @@ def _validate_catalog_url(cls, url: str) -> None: 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 escape as a raw + # http.client.InvalidURL at fetch time. + _ = 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 2e9706e77d..40c777ff15 100644 --- a/tests/integrations/test_integration_catalog.py +++ b/tests/integrations/test_integration_catalog.py @@ -116,12 +116,15 @@ def test_hostless_url_with_truthy_netloc_rejected(self, url): [ "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)