Skip to content

fix(presets): harden preset URL installs against unsafe redirects#2911

Open
darion-yaphet wants to merge 4 commits into
github:mainfrom
darion-yaphet:fix/preset-url-hardening
Open

fix(presets): harden preset URL installs against unsafe redirects#2911
darion-yaphet wants to merge 4 commits into
github:mainfrom
darion-yaphet:fix/preset-url-hardening

Conversation

@darion-yaphet

Copy link
Copy Markdown
Contributor

Summary

Hardens specify preset add --from <url> downloads against redirect-based attacks. Extracted from #2826 so the security fix can be reviewed and merged independently of the refactor.

  • Validate redirect targets: the final URL after redirects must satisfy the same policy as the initial URL (HTTPS, or HTTP only for localhost/loopback). A redirect_validator hook is added to authentication.http.open_url so every hop is checked, not just the final response.
  • Reject malformed download URLs: URLs without a valid hostname are rejected before any network I/O.
  • Prevent transport downgrade for credentialed redirects: _StripAuthOnRedirect no longer allows a redirect to carry the request to a weaker transport when credentials were attached.
  • Loopback detection now uses ipaddress.ip_address(...).is_loopback instead of a hardcoded host list.

Relationship to #2826

These commits originally lived on the refactor/split-init-pr6 branch, mixed with the preset command refactor. They are rebased here onto main (applied to __init__.py, where the preset handlers currently live) so they carry no refactor dependency. #2826 will be rebased to drop them and return to a pure code-move.

Test plan

  • New tests for redirect validation, malformed URL rejection, and credentialed downgrade prevention (tests/test_presets.py, tests/test_authentication.py)
  • Full suite: 3717 passed, 45 skipped

darion-yaphet and others added 4 commits June 10, 2026 11:56
Preset URL installs already rejected non-HTTPS source URLs, but the authenticated opener follows redirects. Validate the final response URL before writing the ZIP, preserve GitHub release asset URL resolution after the preset command module split, stream the response to disk, and keep catalog config serialization on safe YAML output.

Constraint: open_url follows redirects, so source URL validation alone does not constrain the downloaded target

Rejected: Keep response.read() for simplicity | large preset downloads should not be buffered entirely in memory

Confidence: high

Scope-risk: narrow

Directive: Keep preset URL policy aligned with workflow installer redirect validation

Tested: uvx ruff check src/specify_cli/__init__.py src/specify_cli/presets/__init__.py src/specify_cli/presets/_commands.py tests/test_presets.py

Tested: uv run pytest tests/test_presets.py -q

Not-tested: Real network redirect integration against a live HTTP server

Co-authored-by: OmX <omx@oh-my-codex.dev>
Preset downloads should fail early when a URL lacks a hostname, even if the scheme is HTTPS. The redirect error now describes any disallowed target instead of implying that only non-HTTPS redirects are blocked.
Preset URL downloads already checked the final URL after urllib followed redirects, but that was too late for authenticated requests because same-host redirects could preserve Authorization during the redirect itself. The authenticated HTTP helper now supports an opt-in redirect validator, and preset downloads use it to reject disallowed redirect targets before following them. The redirect auth handlers also stop preserving credentials across HTTPS to non-HTTPS downgrades as defense in depth.
重定向安全加固为 open_url 新增 redirect_validator 参数,
两处 fake_open_url mock 签名未同步导致 TypeError。
补齐参数后全部 3717 个测试通过。
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.

1 participant