harden: secure extension and preset archive downloads#3141
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens extension/preset/catalog download and extraction paths by introducing bounded reads, optional SHA-256 verification, and safer ZIP handling, then wiring those protections into the extension/preset install flows and associated tests.
Changes:
- Add
_download_securityhelpers for strict URL validation, bounded HTTP reads, SHA-256 verification, bounded ZIP-member reads, and safe ZIP extraction (zip-slip/zip-bomb/symlink defenses). - Update extension and preset download/install flows to use bounded reads, checksum verification (when
sha256is present), andsafe_extract_zip. - Update auth redirect handling and tests to reflect strict redirect enforcement and stream-like
read(size)semantics.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/_download_security.py |
New security primitives: bounded reads, ZIP extraction/member limits, checksum verification, scheme/host predicate. |
src/specify_cli/authentication/http.py |
Strict redirect validation integrated into redirect handler and exposed via open_url(..., strict_redirects=...). |
src/specify_cli/authentication/azure_devops.py |
Bounded token-response reads and strict redirect protection for client-credentials POST. |
src/specify_cli/_version.py |
Latest-release metadata fetch now uses bounded reads and strict redirects. |
src/specify_cli/_github_http.py |
GitHub release metadata resolution now uses bounded reads to avoid unbounded JSON loads. |
src/specify_cli/catalogs.py |
Catalog URL validation rejects hostless URLs before scheme checks. |
src/specify_cli/extensions/__init__.py |
Extension ZIP installs use safe_extract_zip; catalog and ZIP downloads are bounded and optionally checksum-verified. |
src/specify_cli/extensions/_commands.py |
Extension manifest pre-read from ZIP is now bounded (read_zip_member_limited). |
src/specify_cli/presets/__init__.py |
Preset ZIP installs use safe_extract_zip; catalog and ZIP downloads are bounded and optionally checksum-verified. |
src/specify_cli/presets/_commands.py |
Shared URL predicate for download/redirect validation; preset ZIP downloads use bounded reads and strict redirects for GitHub metadata resolution. |
tests/test_download_security.py |
New tests covering bounded reads, checksum verification, safe ZIP extraction, and bounded ZIP member reads. |
tests/test_authentication.py |
Tests updated for strict redirect rejection and bounded read semantics; Azure AD token tests patch opener behavior. |
tests/test_github_http.py |
Tests updated for stream-like reads; add multi-hop GitHub redirect auth-preservation test. |
tests/http_helpers.py |
Stream-backed read() mocks and an autouse fixture to route build_opener().open() through patched urlopen. |
tests/test_upgrade.py |
Add regression test asserting _fetch_latest_release_tag reads via read_response_limited(max_bytes=...). |
tests/self_upgrade_helpers.py |
Re-export the opener-routing fixture for self-upgrade test modules. |
tests/test_self_upgrade_detection.py |
Enable opener-routing fixture to keep existing urlopen patches effective. |
tests/test_self_upgrade_execution.py |
Enable opener-routing fixture to keep existing urlopen patches effective. |
tests/test_self_upgrade_verification.py |
Enable opener-routing fixture to keep existing urlopen patches effective. |
tests/test_extensions.py |
Stream-like read(size) mocks and one assertion update for host-before-scheme URL validation. |
tests/test_presets.py |
Stream-like read(size) mocks and open_url fake signature updated for strict_redirects. |
tests/integrations/test_integration_catalog.py |
One assertion updated for host-before-scheme URL validation. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 22/22 changed files
- Comments generated: 2
|
Converting to draft as #3140 has to land first |
Adopt the shared download-security primitives from github#3140 across extension and preset catalog, package, direct-URL, and ZIP-install flows: - bound catalog, package, and inline manifest reads; - verify catalog SHA-256 values when present; - replace path-only extraction with bounded traversal/symlink-safe extraction; - validate malformed hosts and ports before opening download URLs; - handle normalized trailing-backslash directory entries consistently. Redirect enforcement and checksum verification remain owned by the shared helpers already on main; this commit wires them into extension and preset behavior. Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
Preflight ZIP central directories before ZipFile allocates them, bound both declared and actual payload sizes, and reject ambiguous or non-portable archive paths before extraction. Keep extension update manifest selection consistent with extraction, reject unsafe catalog-derived output filenames and malformed URL types, and escape untrusted values in download errors. Add regression coverage for parser differentials, collisions, platform-specific filenames, bounded call sites, and failure ordering. Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
99773ba to
b52570e
Compare
|
Rebased this PR onto current The refreshed diff removes the duplicated #3140 history, retains the shared redirect/checksum behavior from Validation on the published tree: full Python 3.12 suite Posted on behalf of @PascalThuet by OpenAI Codex (model: GPT-5). |
| for name in namelist: | ||
| normalized_name = name.replace("\\", "/") | ||
| parts = normalized_name.split("/") | ||
| path_key = portable_zip_path_key(normalized_name) |
| reserved_stem = part.partition(".")[0].partition(":")[0].rstrip(" ") | ||
| if ( | ||
| len(part.encode("utf-8")) > MAX_ZIP_COMPONENT_BYTES | ||
| or any(ord(character) < 32 for character in part) |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback. Also what about workflow, workflow steps and bundle downloads? Do they need this too?
Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
Rebased onto current
main(4d3a428). #3140 and #3671 are merged, so this PR now reuses their shared URL, redirect, bounded-read, and checksum primitives instead of carrying the #3140 commit.What
ZipFilematerializes them, including declared/actual entry counts and a central-directory size cap.Security and compatibility boundaries
Validation
5291 passed, 166 skipped.915 passed.uvx ruff@0.15.0 check src tests: clean.git diff --check origin/main: clean.Deliberately out of scope
Authorship disclosure: updated on behalf of @PascalThuet by OpenAI Codex (model: GPT-5).