From c1bceb625cd40c2de87a73493a49b4419f77ab00 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 12 Aug 2026 18:29:36 +0500 Subject: [PATCH] fix: use bounded read for bundle download HTTP responses (#3764) * fix: use bounded read for bundle download HTTP responses The bundle download used unbounded resp.read() to read HTTP responses into memory. A malicious or misconfigured catalog server could return an arbitrarily large payload causing OOM. Replace with read_response_limited() capped at MAX_DOWNLOAD_BYTES (50 MiB), consistent with how other download paths in the codebase enforce bounded reads. Add regression test that monkeypatches MAX_DOWNLOAD_BYTES to 100 bytes and verifies oversized responses are rejected. * fix: remove duplicate import of MAX_DOWNLOAD_BYTES and read_response_limited --- src/specify_cli/commands/bundle/__init__.py | 2 +- tests/contract/test_bundle_cli.py | 30 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/commands/bundle/__init__.py b/src/specify_cli/commands/bundle/__init__.py index 7ccc6cba3..b816e6fd0 100644 --- a/src/specify_cli/commands/bundle/__init__.py +++ b/src/specify_cli/commands/bundle/__init__.py @@ -14,8 +14,8 @@ from pathlib import Path import typer from rich.markup import escape as _escape_markup -from ..._console import console, err_console from ..._download_security import MAX_DOWNLOAD_BYTES, read_response_limited +from ..._console import console, err_console from ...bundler import BundlerError from ...bundler.lib.project import ( active_integration, diff --git a/tests/contract/test_bundle_cli.py b/tests/contract/test_bundle_cli.py index bed2f8964..c458a810b 100644 --- a/tests/contract/test_bundle_cli.py +++ b/tests/contract/test_bundle_cli.py @@ -1010,3 +1010,33 @@ def test_bundle_info_resolves_ghes_browser_release_url(project: Path): payload = json.loads(result.output) assert payload["id"] == "demo-bundle" + + +def test_bundle_download_rejects_oversized_response(project: Path, monkeypatch): + """Bundle download rejects responses exceeding MAX_DOWNLOAD_BYTES.""" + # Monkeypatch to a small limit so the test is fast and low-memory. + monkeypatch.setattr( + "specify_cli.commands.bundle.MAX_DOWNLOAD_BYTES", 100 + ) + + api_asset_url = "https://api.github.com/repos/org/repo/releases/assets/99" + + def fake_open_url(url, timeout=None, extra_headers=None, redirect_validator=None): + # Return a response that exceeds 100 bytes. + return FakeBundleResponse(b"x" * 200, url=api_asset_url) + + catalog = project / "catalog.json" + write_catalog_file( + catalog, + {"demo-bundle": catalog_entry_dict("demo-bundle", download_url=api_asset_url)}, + ) + _make_catalog_config(catalog, project) + + with patch("specify_cli.authentication.http.open_url", side_effect=fake_open_url): + result = runner.invoke(app, ["bundle", "info", "demo-bundle", "--json"]) + + # Must fail with a size-limit error, not an unhandled traceback. + assert result.exit_code == 1 + # Rich may wrap the message across lines; normalise whitespace before checking. + output_flat = " ".join(result.output.split()) + assert "exceeds maximum size of 100 bytes" in output_flat