Skip to content

harden: bound HTTP reads and enforce strict redirects#3140

Open
PascalThuet wants to merge 7 commits into
github:mainfrom
PascalThuet:split/bounded-http-reads
Open

harden: bound HTTP reads and enforce strict redirects#3140
PascalThuet wants to merge 7 commits into
github:mainfrom
PascalThuet:split/bounded-http-reads

Conversation

@PascalThuet

@PascalThuet PascalThuet commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Part of splitting #2442 into smaller, dedicated PRs. Rebased onto current main.

What

  • Add shared bounded-response and URL-scheme helpers in src/specify_cli/_download_security.py.
  • Enforce strict redirect safety for both authenticated and unauthenticated requests while preserving the existing redirect validator, GHES handling, auth stripping, and malformed-URL behavior.
  • Bound GitHub release metadata, self-upgrade metadata, and Azure DevOps token response reads.
  • Route the Azure DevOps token POST through the guarded redirect opener.
  • Keep HTTP test fakes cursor-aware and reuse shared helpers where possible.

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 main validation behavior.

Conflict and review resolution

  • Preserved the configurable GitHub metadata limit and redirect validator added on main while switching its implementation to the shared chunked reader.
  • Preserved current GHES shape validation and malformed-redirect handling.
  • Made strict redirect handling unconditional, including the no-token fallback used by catalogs and downloads.
  • Updated the Azure AD network-error test to intercept the guarded opener rather than bare urlopen.
  • Reused the shared opener-routing fixture in affected extension and integration-catalog tests.
  • Removed obsolete duplicate HTTP fake classes rather than carrying parallel helpers.

Validation

  • uvx ruff check src tests — passed.
  • HTTP/security and affected-caller tests — 1,675 passed.
  • Full suite — 4,786 passed, 153 skipped.
  • git diff --check — passed.

Disclosure: Updated on behalf of @PascalThuet by OpenAI Codex (model: GPT-5, autonomous).

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

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() and is_https_or_localhost_http() in a new _download_security module, including size ceilings for downloads vs JSON metadata.
  • Enforce strict redirect validation in the auth redirect handler and thread strict_redirects through open_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

Comment thread src/specify_cli/authentication/azure_devops.py

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.

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 1

Comment thread src/specify_cli/authentication/http.py

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address Copilot feedback

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: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread tests/test_upgrade.py Outdated

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: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines +64 to +69
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 mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address Copilot feedback

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: 17/17 changed files
  • Comments generated: 0 new
  • Review effort level: Low

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address test & lint errors

@PascalThuet
PascalThuet force-pushed the split/bounded-http-reads branch from 46c8cb8 to 4881d99 Compare July 20, 2026 15:28
@PascalThuet

PascalThuet commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Review round summary

Rebase and conflict resolution

Rebased the PR onto the current main branch. The conflict resolution preserves:

  • the newer GHES validation;
  • the configurable metadata size limit;
  • redirect-validator composition;
  • malformed-URL handling.

Duplicate HTTP fakes were omitted in favor of the existing shared, cursor-aware implementations.

Previous CI failure

The historical junie-kilocode failure is obsolete after the integration-registry changes now present on main.

Validation

  • Ruff on the current CI scope: passed
  • Targeted tests: 692 passed, 1 skipped
  • Extension, preset, and workflow tests: 1,391 passed
  • Full suite: 4,447 passed, 112 skipped

Head commit: 4881d99


Posted on behalf of @PascalThuet by Codex (model: GPT-5, autonomous).

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: 17/17 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment thread src/specify_cli/authentication/http.py Outdated
timeout: int = 10,
extra_headers: dict[str, str] | None = None,
redirect_validator: RedirectValidator | None = None,
strict_redirects: bool = False,
Comment on lines +534 to +536
mock_opener = MagicMock()
mock_opener.open.return_value = mock_resp
with patch("urllib.request.build_opener", return_value=mock_opener):
@mnriem

mnriem commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

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)
@PascalThuet
PascalThuet force-pushed the split/bounded-http-reads branch from 4881d99 to 30a7454 Compare July 21, 2026 22:55
@PascalThuet

Copy link
Copy Markdown
Contributor Author

Addressed the latest Copilot review round and re-audited the PR against current main.

Both findings were valid. open_url now always installs the strict redirect handler, including the unauthenticated fallback used by extension, preset, workflow, bundle, and integration-catalog downloads. The newly introduced strict_redirects switch was removed so there is no insecure default path. The Azure AD network-error test now patches build_opener().open() and cannot issue a real request.

The broader audit found four additional tests coupled to bare urlopen (one extension download and three integration-catalog cases). They now reuse the shared opener-routing fixture; no additional production defect was found within the PR scope.

Validation: Ruff passed on src and tests; 1,675 HTTP/security and affected-caller tests passed; full suite: 4,786 passed, 153 skipped; git diff --check passed.

Commit: 30a745439fbac06f50ec7dd04dd87162351467ca
Base: d7699c39f2c3ab7d9c4f3eceff8f29ae1601cc3b

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).

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