Skip to content

fix(extensions): re-validate catalog URL after redirects (HTTPS parity/security)#3524

Open
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/extensions-catalog-redirect-revalidate
Open

fix(extensions): re-validate catalog URL after redirects (HTTPS parity/security)#3524
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/extensions-catalog-redirect-revalidate

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

What

ExtensionCatalog._fetch_single_catalog fetched the catalog JSON and trusted it without re-validating the post-redirect URL — the same gap as in the presets catalog fetcher:

with self._open_url(entry.url, timeout=10) as response:
    catalog_data = json.loads(response.read())   # no response.geturl() check

_open_url follows redirects (stripping auth only on an HTTPS→HTTP downgrade), so an https:// catalog entry.url that 30x-redirects to http://attacker/catalog.json is still fetched and trusted. The payload supplies each extension's download_url and sha256, so an attacker-controlled redirected payload defeats sha256 verification and can drive installation of an arbitrary archive.

Fix

Re-validate response.geturl() after the redirect via _validate_catalog_url, mirroring integrations/catalog.py, workflows/catalog.py, bundler/services/adapters.py, and the presets catalog fetcher. This is the extensions sibling of the presets fix.

Test

test_fetch_single_catalog_revalidates_redirected_url: an HTTPS URL whose response.geturl() reports http:// is rejected with ExtensionError (fails before: no revalidation). Existing fetch-test mocks were completed to report geturl() like a real urllib response.

uv run pytest tests/test_extensions.py   # 354 passed, 3 skipped
uvx ruff check  # clean

🤖 Written with the assistance of Claude Code (AI). Bug self-found; fix/tests verified locally (fail-before / pass-after).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds post-redirect URL validation to prevent trusting extension catalogs redirected to insecure HTTP endpoints.

Changes:

  • Validates the final catalog URL before reading its payload.
  • Adds redirect-security coverage and updates response mocks.
Show a summary per file
File Description
src/specify_cli/extensions/__init__.py Re-validates redirected catalog URLs.
tests/test_extensions.py Tests HTTPS-to-HTTP rejection and updates mocks.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/specify_cli/extensions/__init__.py Outdated
jawwad-ali and others added 2 commits July 17, 2026 15:49
ExtensionCatalog._fetch_single_catalog opened the catalog URL and trusted the
payload without re-validating response.geturl() after redirects. _open_url
follows redirects (stripping auth only on an HTTPS->HTTP downgrade), so an
https:// catalog entry that 30x-redirects to http://attacker/... was still
fetched and trusted. The payload supplies each extension's download_url +
sha256, so a redirected payload can drive install of an arbitrary archive that
passes sha256 verification.

Add the post-redirect geturl() re-validation via _validate_catalog_url,
mirroring integrations/catalog.py, presets, workflows/catalog.py, and bundler
adapters. Sibling of the same fix in the presets catalog fetcher.

Test: an HTTPS URL whose response.geturl() reports http:// is rejected
(ExtensionError). Completed existing fetch-test mocks that predated this
behavior to report geturl() like a real urllib response.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hop + guard legacy fetch_catalog

- Correct the comment: _StripAuthOnRedirect strips auth not only on an
  HTTPS->HTTP downgrade but also whenever the redirect leaves the configured
  trusted hosts. The comment now describes both cases.
- Parity with the presets fix: validate EVERY redirect hop (not just the
  terminal URL) so an https -> http -> attacker-https chain can't slip a
  redirected payload past the final-URL check. _open_url forwards a
  redirect_validator to open_url; _fetch_single_catalog passes
  _validate_catalog_url through it while keeping the final geturl() check.
- Give the legacy public fetch_catalog() single-catalog path the same
  redirect_validator + final geturl() validation (it previously parsed the body
  with no redirect check).

Tests: an intermediate http hop is rejected, and the legacy fetch_catalog()
rejects an HTTPS->http redirected payload (both fail before). Full
test_extensions.py (356) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali force-pushed the fix/extensions-catalog-redirect-revalidate branch from 00794af to 8b7fdac Compare July 17, 2026 10:53
@mnriem
mnriem requested a review from Copilot July 17, 2026 19:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread tests/test_extensions.py
Comment on lines +3720 to +3722
with patch.object(catalog, "_open_url", return_value=mock_response):
with pytest.raises(ExtensionError, match="HTTPS"):
catalog.fetch_catalog(force_refresh=True)
@mnriem

mnriem commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants