harden: bound HTTP reads and enforce strict redirects#3140
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens Spec Kit’s outbound HTTP handling by introducing bounded response reads for in-memory JSON parsing and enforcing strict redirect safety (HTTPS-only, with a narrow localhost HTTP exception) to reduce memory-DoS and credential-leak/downgrade risks.
Changes:
- Add
read_response_limited()andis_https_or_localhost_http()in a new_download_securitymodule, including size ceilings for downloads vs JSON metadata. - Enforce strict redirect validation in the auth redirect handler and thread
strict_redirectsthroughopen_url()for unauthenticated fallback behavior. - Update GitHub/Azure DevOps HTTP codepaths to use bounded JSON reads; adjust tests/fakes to model
HTTPResponse.read(size)cursor semantics.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/_download_security.py |
Introduces shared bounded-read + URL scheme predicate utilities and size constants. |
src/specify_cli/authentication/http.py |
Adds strict redirect validation and a strict_redirects option to open_url(). |
src/specify_cli/_version.py |
Uses bounded reads for the “latest release” JSON and enables strict redirects. |
src/specify_cli/_github_http.py |
Uses bounded reads for GitHub release metadata JSON resolution. |
src/specify_cli/authentication/azure_devops.py |
Routes token POST via strict-redirect opener and bounds token JSON reads. |
tests/test_download_security.py |
Adds focused unit tests for the new bounded-read and scheme predicate helpers. |
tests/http_helpers.py |
Makes read() mocks stream-like and adds an autouse fixture to route opener .open() through urlopen() for tests. |
tests/test_upgrade.py |
Adds a regression test pinning _fetch_latest_release_tag() to bounded reads and strict redirects; updates fixtures/imports. |
tests/test_authentication.py |
Updates Azure token tests for opener-based fetching; adds redirect downgrade rejection coverage. |
tests/test_github_http.py |
Updates mocks for bounded reads; adds redirect auth-preservation test across GitHub-owned hosts. |
tests/test_extensions.py |
Updates GitHub release download mocks to stream-like read() behavior. |
tests/test_presets.py |
Updates GitHub release download mocks to stream-like read() behavior. |
tests/test_workflows.py |
Updates GitHub release download fakes to implement read(size) cursor semantics. |
tests/self_upgrade_helpers.py |
Exposes the opener-routing fixture for self-upgrade test modules. |
tests/test_self_upgrade_detection.py |
Imports the opener-routing fixture so existing urlopen patches remain effective. |
tests/test_self_upgrade_execution.py |
Imports the opener-routing fixture so existing urlopen patches remain effective. |
tests/test_self_upgrade_verification.py |
Imports the opener-routing fixture so existing urlopen patches remain effective. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 17/17 changed files
- Comments generated: 1
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
885f8fc to
f2c3bc1
Compare
| def _validate_strict_redirect(_old_url: str, new_url: str) -> None: | ||
| if not is_https_or_localhost_http(new_url): | ||
| raise urllib.error.URLError( | ||
| "unsafe redirect: target must use HTTPS with a hostname, " | ||
| "or HTTP for localhost (127.0.0.1, ::1)" | ||
| ) |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
mnriem
left a comment
There was a problem hiding this comment.
Please address test & lint errors
46c8cb8 to
4881d99
Compare
Review round summaryRebase and conflict resolutionRebased the PR onto the current main branch. The conflict resolution preserves:
Duplicate HTTP fakes were omitted in favor of the existing shared, cursor-aware implementations. Previous CI failureThe historical junie-kilocode failure is obsolete after the integration-registry changes now present on main. Validation
Head commit: 4881d99 Posted on behalf of @PascalThuet by Codex (model: GPT-5, autonomous). |
| timeout: int = 10, | ||
| extra_headers: dict[str, str] | None = None, | ||
| redirect_validator: RedirectValidator | None = None, | ||
| strict_redirects: bool = False, |
| mock_opener = MagicMock() | ||
| mock_opener.open.return_value = mock_resp | ||
| with patch("urllib.request.build_opener", return_value=mock_opener): |
|
Please address Copilot feedback |
Add a shared _download_security module (read_response_limited, is_https_or_localhost_http, size constants) and route the GitHub release and Azure DevOps token network reads through bounded reads so an oversized response can't exhaust memory. Add a strict_redirects mode to authentication.open_url: the redirect handler now rejects any redirect whose target isn't HTTPS (or HTTP to localhost), composing with the existing per-hop redirect_validator and auth-stripping. The Azure DevOps token POST is routed through that handler so a 307/308 cannot forward the client_secret body to a non-HTTPS host. Assisted-by: Codex (model: GPT-5, autonomous)
Assisted-by: Codex (model: GPT-5, autonomous)
Assisted-by: Codex (model: GPT-5, autonomous)
Assisted-by: Codex (model: GPT-5, autonomous)
Assisted-by: Codex (model: GPT-5, autonomous)
Assisted-by: Codex (model: GPT-5, autonomous)
Assisted-by: Codex (model: GPT-5, autonomous)
4881d99 to
30a7454
Compare
|
Addressed the latest Copilot review round and re-audited the PR against current Both findings were valid. The broader audit found four additional tests coupled to bare Validation: Ruff passed on Commit: The review threads remain open for the reviewer or PR author to resolve. Posted on behalf of @PascalThuet by OpenAI Codex (model: GPT-5, autonomous). |
Part of splitting #2442 into smaller, dedicated PRs. Rebased onto current
main.What
src/specify_cli/_download_security.py.Why
Unbounded remote JSON reads expose a memory-denial-of-service surface. Unsafe redirect downgrades can also expose credentials or silently weaken transport. The change closes both paths while preserving the existing happy path and current
mainvalidation behavior.Conflict and review resolution
mainwhile switching its implementation to the shared chunked reader.urlopen.Validation
uvx ruff check src tests— passed.git diff --check— passed.Disclosure: Updated on behalf of @PascalThuet by OpenAI Codex (model: GPT-5, autonomous).