Skip to content

Commit 1736f07

Browse files
fix(bundler): raise BundlerError, not raw ValueError, on a malformed catalog URL (#3433)
adapters._validate_remote_url reads parsed.hostname, which raises ValueError on a malformed authority (e.g. an unclosed ipv6 bracket https://[::1). the function's contract is to raise BundlerError for any bad url - every other reject path does - so the raw ValueError leaked to the caller and crashed the fetch instead of failing cleanly. wrap the parse and convert to BundlerError. bundler sibling of #3369, which fixed the cli extension/preset/workflow add paths but not this validator. added a regression test that fails pre-fix.
1 parent c8ce488 commit 1736f07

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

src/specify_cli/bundler/services/adapters.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,18 @@ def _validate_remote_url(source_id: str, url: str) -> None:
6868
Mirrors ``specify_cli.catalogs`` URL validation to avoid MITM/downgrade
6969
issues before any network call.
7070
"""
71-
parsed = urlparse(url)
72-
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
71+
# A malformed authority (e.g. an unclosed IPv6 bracket ``https://[::1``)
72+
# makes urlparse / hostname access raise ValueError. This function's
73+
# contract is to raise BundlerError for a bad URL, so surface that as a
74+
# clean error rather than leaking a raw ValueError to the caller.
75+
try:
76+
parsed = urlparse(url)
77+
hostname = parsed.hostname
78+
except ValueError:
79+
raise BundlerError(
80+
f"Catalog '{source_id}' URL is malformed: {url}"
81+
) from None
82+
is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
7383
if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
7484
raise BundlerError(
7585
f"Catalog '{source_id}' URL must use HTTPS (got {parsed.scheme}://). "
@@ -79,7 +89,7 @@ def _validate_remote_url(source_id: str, url: str) -> None:
7989
# "https://:8080" or "https://user@...", so requiring netloc would let
8090
# those through even though they carry no host. hostname is None in those
8191
# cases. Mirrors the fix in ``specify_cli.catalogs`` (#3210).
82-
if not parsed.hostname:
92+
if not hostname:
8393
raise BundlerError(
8494
f"Catalog '{source_id}' URL must be a valid URL with a host: {url}"
8595
)

tests/unit/test_bundler_adapters.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,21 @@ def test_validate_remote_url_rejects_host_less_urls(url):
9696
def test_validate_remote_url_accepts_normal_https_url():
9797
# Sanity: a real host with a port still passes.
9898
adapters._validate_remote_url("team", "https://example.com:8080/c.json")
99+
100+
101+
@pytest.mark.parametrize(
102+
"url",
103+
[
104+
"https://[::1", # unclosed IPv6 bracket
105+
"https://[not-an-ip]/c.json",
106+
],
107+
)
108+
def test_validate_remote_url_rejects_malformed_url_cleanly(url):
109+
"""A malformed URL must raise BundlerError, not a raw ValueError.
110+
111+
``urlparse``/``hostname`` raise ``ValueError`` on a malformed authority
112+
(e.g. an unclosed IPv6 bracket). The validator's contract is to raise
113+
BundlerError for any bad URL, so the raw ValueError must not escape to the
114+
caller. Bundler sibling of #3369."""
115+
with pytest.raises(BundlerError):
116+
adapters._validate_remote_url("team", url)

0 commit comments

Comments
 (0)