fix(cli): guard lazy .hostname ValueError in extension/preset add --from#3651
Open
Noor-ul-ain001 wants to merge 2 commits into
Open
fix(cli): guard lazy .hostname ValueError in extension/preset add --from#3651Noor-ul-ain001 wants to merge 2 commits into
Noor-ul-ain001 wants to merge 2 commits into
Conversation
`extension add --from <url>` and `preset add --from <url>` validated the URL by reading `parsed.hostname` OUTSIDE their `try/except ValueError` guards. A bracketed-but-invalid IPv6 authority (e.g. "https://[not-an-ip]/x.zip") parses cleanly under urlparse() on Python < 3.14 and only raises ValueError lazily on the first .hostname access. On the interpreters spec-kit supports (>=3.11) that raw ValueError leaked past the CLI, printing an uncaught traceback instead of the clean "Invalid URL" error. (The raise moved eager into urlparse() only in 3.14.) Same bug class as the catalog/download fixes github#3433/github#3435/github#3437/github#3577. - extensions/_commands.py: read parsed.hostname inside the existing try and reuse it for the localhost check. - presets/_commands.py: guard the up-front `urlparse(from_url).hostname` read (preserves the "Invalid URL" message), and harden the nested `_is_allowed_download_url` to take a URL string and parse+read .hostname inside its own try/except -> returns False on malformed input. This also covers the redirect-validator and final-URL (post-redirect) checks, where the URL is server-controlled. Regression tests for each command: a bracketed-non-IP URL, plus a monkeypatched lazy-.hostname raiser that reproduces the pre-3.14 shape independently of the running interpreter (fails with a raw ValueError before the fix, verified via test-the-test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Defensively normalizes URL parsing failures in extension and preset installation paths.
Changes:
- Guards hostname extraction against
ValueError. - Hardens preset redirect URL validation.
- Adds malformed-URL regression tests.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/extensions/_commands.py |
Guards hostname extraction. |
src/specify_cli/presets/_commands.py |
Hardens initial and redirected URL validation. |
tests/test_extensions.py |
Adds extension URL tests. |
tests/test_presets.py |
Adds preset URL tests. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (3)
src/specify_cli/presets/_commands.py:125
- This repeats an incorrect stdlib behavior claim.
.hostnamedoes not lazily validate bracketed IPv6 text in CPython 3.13.0, while 3.11/3.12 and current 3.13/3.14 reject this example during parsing. Keep the useful policy statement without asserting that version history.
# Parse and read .hostname inside the try: a bracketed-but-invalid
# IPv6 authority (e.g. "https://[not-an-ip]/p.zip") parses cleanly
# under urlparse() on Python < 3.14 and only raises ValueError
# lazily on the first .hostname access (eager at urlparse() on
# 3.14+). A malformed URL is simply not an allowed download URL.
tests/test_extensions.py:6640
- The monkeypatch is a synthetic defensive case, not “the Python < 3.14 shape”: the supported CPython implementations do not defer bracket validation to
.hostname. Calling this the exact production path also overstates what this test proves; please label it as simulated defensive coverage.
"""Simulate the Python < 3.14 shape explicitly (independent of the running
interpreter): urlparse() succeeds but .hostname raises ValueError lazily.
This is the exact path the fix guards; it leaks a raw ValueError if
.hostname is read outside the try/except.
tests/test_presets.py:5611
- This monkeypatched object does not reproduce a pre-3.14 CPython behavior: those implementations either reject the bracketed host during parsing or return the extracted hostname without raising. Please describe it as synthetic defensive coverage rather than the exact production failure path.
"""Simulate the Python < 3.14 shape explicitly (independent of the running
interpreter): urlparse() succeeds but .hostname raises ValueError lazily.
This is the exact path the fix guards; it leaks a raw ValueError if
.hostname is read outside the try/except.
- Files reviewed: 4/4 changed files
- Comments generated: 4
- Review effort level: Medium
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| ) as response: | ||
| final_url = response.geturl() if hasattr(response, "geturl") else from_url | ||
| if not _is_allowed_download_url(_urlparse(final_url)): | ||
| if not _is_allowed_download_url(final_url): |
Collaborator
|
Please address Copilot feedback |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two CLI install paths validated a user-supplied
--fromURL by readingparsed.hostnameoutside theirtry/except ValueErrorguard:extension add --from <url>—extensions/_commands.py:434preset add --from <url>—presets/_commands.py(_is_allowed_download_url)A bracketed-but-invalid IPv6 authority (e.g.
https://[not-an-ip]/x.zip) parses cleanly underurlparse()on Python < 3.14 and only raisesValueErrorlazily on the first.hostnameaccess. On the interpreters spec-kit supports (>=3.11), that rawValueErrorleaked past the CLI, printing an uncaught traceback instead of the cleanInvalid URLerror. (The raise moved eager intourlparse()only in 3.14, so the pre-fix code happened to be safe there — but not on 3.11–3.13, i.e. CI.)Same bug class as the maintainer-fixed
#3433/#3435/#3437/#3577(unguardedurlparse().hostnameleaking a rawValueError).Fix
parsed.hostnameinside the existingtryand reuse it for the localhost check.urlparse(from_url).hostnameread (preserving the existingInvalid URLmessage), and harden the nested_is_allowed_download_urlto accept a URL string and parse + read.hostnameinside its owntry/except→ returnsFalseon malformed input. This also covers the redirect-validator and post-redirect final-URL checks, where the URL is server-controlled (a malicious redirect target).Tests
For each command:
--fromURL exits cleanly (exit 1, no traceback);.hostnameraiser reproducing the pre-3.14 shape independently of the running interpreter. Test-the-test: both fail with a rawValueErrorbefore the fix and pass after.Full
tests/test_extensions.py+tests/test_presets.pypass locally (751 passed, 7 pre-existing version/pwsh skips).🤖 Generated with Claude Code