Skip to content

Commit 9f59bfb

Browse files
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)
1 parent e32661c commit 9f59bfb

1 file changed

Lines changed: 26 additions & 0 deletions

File tree

tests/unit/test_bundler_packager.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from specify_cli.bundler import BundlerError
1212
from specify_cli.bundler.services.packager import build_bundle
13+
from specify_cli._download_security import MAX_ZIP_MEMBER_BYTES
1314
from tests.bundler_helpers import valid_manifest_dict
1415

1516

@@ -234,3 +235,28 @@ def test_toctou_stat_read_consistency(tmp_path: Path):
234235
assert content == b"\x00\x01\x02\x03"
235236
assert modes["assets/data.bin"] == 0o644
236237
assert modes["README.md"] == 0o644
238+
239+
240+
def test_oversized_asset_file_is_rejected(tmp_path: Path):
241+
"""A single file exceeding MAX_ZIP_MEMBER_BYTES must be refused, not read
242+
into memory unbounded."""
243+
bundle = _make_bundle(tmp_path / "b")
244+
oversized = bundle / "assets" / "huge.bin"
245+
oversized.parent.mkdir(parents=True, exist_ok=True)
246+
oversized.write_bytes(b"\x00" * (MAX_ZIP_MEMBER_BYTES + 1))
247+
248+
with pytest.raises(BundlerError, match="exceeds.*byte limit"):
249+
build_bundle(bundle, output_dir=tmp_path / "out")
250+
251+
252+
def test_asset_at_exact_size_limit_is_accepted(tmp_path: Path):
253+
"""A file exactly at MAX_ZIP_MEMBER_BYTES must still be packaged."""
254+
bundle = _make_bundle(tmp_path / "b")
255+
at_limit = bundle / "assets" / "exact.bin"
256+
at_limit.parent.mkdir(parents=True, exist_ok=True)
257+
at_limit.write_bytes(b"\x00" * MAX_ZIP_MEMBER_BYTES)
258+
259+
result = build_bundle(bundle, output_dir=tmp_path / "out")
260+
with zipfile.ZipFile(result.artifact_path) as archive:
261+
content = archive.read("assets/exact.bin")
262+
assert len(content) == MAX_ZIP_MEMBER_BYTES

0 commit comments

Comments
 (0)