Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/specify_cli/_github_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,17 @@ def resolve_github_release_asset_api_url(

from specify_cli._download_security import read_response_limited

parsed = urlparse(download_url)
hostname = (parsed.hostname or "").lower()
# Accessing ``.hostname`` (like ``.port`` below) raises ValueError on a
# malformed authority, e.g. an invalid bracketed IPv6 host
# ``https://[not-an-ip]/...``. The function's contract is to return None for
# anything it can't resolve, not to raise, so guard the read. ``download_url``
# is server-controlled here (a catalog ``download_url`` payload), so a
# malformed value must not leak a raw traceback past the caller.
try:
parsed = urlparse(download_url)
hostname = (parsed.hostname or "").lower()
except ValueError:
return None
parts = [unquote(part) for part in parsed.path.strip("/").split("/")]

is_ghes = (
Expand Down
17 changes: 17 additions & 0 deletions tests/test_github_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,23 @@ def open_never(url, timeout=None, extra_headers=None):
assert result is None
assert called == []

def test_returns_none_on_malformed_host(self):
"""A malformed authority (e.g. an invalid bracketed IPv6 host) returns
None, not a ValueError (contract: resolve or return None, never raise)."""
called = []

def open_never(url, timeout=None, extra_headers=None):
called.append(url)
raise AssertionError("open_url_fn must not be called")

result = resolve_github_release_asset_api_url(
"https://[not-an-ip]/o/r/releases/download/v1/ext.zip",
open_never,
github_hosts=("ghes.example",),
)
assert result is None
assert called == []

def test_passthrough_for_unlisted_ghes_api_asset_url(self):
"""A direct GHES /api/v3 asset URL passes through even when the host is
not allowlisted: passthrough issues no API request, and the download
Expand Down