Repository navigation
Description
summary
specify bundle catalog add (add_source in src/specify_cli/bundler/commands_impl/catalog_config.py) persists remote catalog URLs into catalogs.yaml without any HTTPS or host validation, and crashes with a raw ValueError traceback on a malformed IPv6 URL.
The other two catalog-URL validation sites enforce this:
specify_cli.catalogs._validate_catalog_url— HTTPS-only (HTTP for localhost) +hostnamecheck (Host-less catalog URLs (e.g. https://:8080) pass URL validation in base and preset validators #3209/fix(catalogs): reject host-less catalog URLs in base and preset validators #3210)bundler/services/adapters.py:_validate_remote_url— same, host-less gap being fixed in fix(bundler): reject host-less catalog URLs in adapters (use hostname, not netloc) #3333 (Bug: bundler adapters accept host-less catalog URLs (https://:8080), unlike catalogs.py #3332)
add_source is the third copy of this contract and has neither check, so junk enters the persisted config and only fails later at fetch time, far from the command that caused it.
reproduction
from pathlib import Path
import tempfile
from specify_cli.bundler.commands_impl.catalog_config import add_source
d = Path(tempfile.mkdtemp())
add_source(d, "http://evil.example.com/catalog.json", policy="install-allowed", priority=10) # accepted; plain HTTP, non-localhost
add_source(d, "https://:8080", policy="install-allowed", priority=10) # accepted; no host
add_source(d, "https://user@", policy="install-allowed", priority=10) # accepted; no host
add_source(d, "https://[::1/c.json", policy="install-allowed", priority=10) # raw ValueError: Invalid IPv6 URLVia the CLI this is specify bundle catalog add <url>; the first three print ✓ Added catalog ... and write the entry to .specify/catalogs.yaml, the fourth escapes the except BundlerError handler in catalog_add and produces a traceback. remove_source hits the same unguarded urlparse through its _canonicalize_url fallback.
impact
- Plain-HTTP catalog sources are persisted silently — the MITM/downgrade protection the other two sites enforce does not hold for the CLI entry point that actually writes config.
- Host-less URLs defer the failure to fetch time with a less clear error (same class as Bug: bundler adapters accept host-less catalog URLs (https://:8080), unlike catalogs.py #3332).
- Malformed IPv6 input crashes the command instead of a clean
BundlerError.
proposed fix
Mirror the catalogs.py validation in add_source for http(s) schemes (HTTPS-only with localhost exception, check parsed.hostname not netloc), and wrap the urlparse calls so malformed URLs raise BundlerError. Fully additive; file://, builtin:// and local paths unaffected. I have a patch with tests ready and can open a PR.
Activity
marcelsafin commentedon Jul 6, 2026
Opened #3367 with the fix + tests (mirrors the catalogs.py validation, fully additive).
fix(bundler): validate catalog URLs in `catalog add` (HTTPS-only, req…