From e32661c4c0974f9e9e8282524c7e7978db5e754c Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Fri, 31 Jul 2026 17:56:44 +0500 Subject: [PATCH 1/2] fix: enforce size limit on individual files in build_bundle() build_bundle() called read_bytes() without any size guard. A single large asset file could exhaust memory. Enforce MAX_ZIP_MEMBER_BYTES (10 MiB) limit before reading. --- src/specify_cli/bundler/services/packager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/specify_cli/bundler/services/packager.py b/src/specify_cli/bundler/services/packager.py index 4e14934e0a..568c7e8141 100644 --- a/src/specify_cli/bundler/services/packager.py +++ b/src/specify_cli/bundler/services/packager.py @@ -14,6 +14,7 @@ from pathlib import Path from .. import BundlerError +from ..._download_security import MAX_ZIP_MEMBER_BYTES from ..lib.yamlio import ensure_within from ..models.manifest import BundleManifest from .validator import validate_manifest @@ -97,6 +98,10 @@ def build_bundle( st = os.fstat(fh.fileno()) mode = 0o755 if st.st_mode & 0o111 else 0o644 info.external_attr = mode << 16 + if st.st_size > MAX_ZIP_MEMBER_BYTES: + raise BundlerError( + f"Bundle file {arcname} exceeds {MAX_ZIP_MEMBER_BYTES}-byte limit" + ) archive.writestr(info, fh.read()) return BuildResult(artifact_path=artifact_path, file_count=len(files)) From 9f59bfb9c0d33c8fa378803c03e0235c014563ea Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 11 Aug 2026 02:38:13 +0500 Subject: [PATCH 2/2] fix: enforce size limit on individual files in build_bundle() Enforce MAX_ZIP_MEMBER_BYTES (10 MiB) limit before reading each file via os.fstat() on the same file descriptor used for reading, avoiding a TOCTOU gap between stat() and read_bytes(). Add regression tests: oversized file (>limit) is rejected, file at exact limit is accepted. Co-authored-by: GitHub Copilot (model: mimo-v2.5-free, supervised) --- tests/unit/test_bundler_packager.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/test_bundler_packager.py b/tests/unit/test_bundler_packager.py index d203f7ffb0..4ea938432a 100644 --- a/tests/unit/test_bundler_packager.py +++ b/tests/unit/test_bundler_packager.py @@ -10,6 +10,7 @@ from specify_cli.bundler import BundlerError from specify_cli.bundler.services.packager import build_bundle +from specify_cli._download_security import MAX_ZIP_MEMBER_BYTES from tests.bundler_helpers import valid_manifest_dict @@ -234,3 +235,28 @@ def test_toctou_stat_read_consistency(tmp_path: Path): assert content == b"\x00\x01\x02\x03" assert modes["assets/data.bin"] == 0o644 assert modes["README.md"] == 0o644 + + +def test_oversized_asset_file_is_rejected(tmp_path: Path): + """A single file exceeding MAX_ZIP_MEMBER_BYTES must be refused, not read + into memory unbounded.""" + bundle = _make_bundle(tmp_path / "b") + oversized = bundle / "assets" / "huge.bin" + oversized.parent.mkdir(parents=True, exist_ok=True) + oversized.write_bytes(b"\x00" * (MAX_ZIP_MEMBER_BYTES + 1)) + + with pytest.raises(BundlerError, match="exceeds.*byte limit"): + build_bundle(bundle, output_dir=tmp_path / "out") + + +def test_asset_at_exact_size_limit_is_accepted(tmp_path: Path): + """A file exactly at MAX_ZIP_MEMBER_BYTES must still be packaged.""" + bundle = _make_bundle(tmp_path / "b") + at_limit = bundle / "assets" / "exact.bin" + at_limit.parent.mkdir(parents=True, exist_ok=True) + at_limit.write_bytes(b"\x00" * MAX_ZIP_MEMBER_BYTES) + + result = build_bundle(bundle, output_dir=tmp_path / "out") + with zipfile.ZipFile(result.artifact_path) as archive: + content = archive.read("assets/exact.bin") + assert len(content) == MAX_ZIP_MEMBER_BYTES