Skip to content

MAX_FETCH_FILE_SIZE = 0 (no limit) trips the list zip-bomb guard too (#939) - #940

Merged
Ninja-FSE merged 1 commit into
mainfrom
fix/939-max-fetch-file-size-zero-in-the-zip-bomb-guard
Sep 24, 2026
Merged

Ninja-FSE merged 1 commit into
mainfrom
fix/939-max-fetch-file-size-zero-in-the-zip-bomb-guard

Conversation

@Ninja-FSE

Copy link
Copy Markdown
Owner

Closes #939. Sibling of #937/#938, found immediately after that fix shipped.

list_fetch.py reads MAX_FETCH_FILE_SIZE twice - once as the zip-bomb guard on a fetched list's declared total uncompressed size (_validate_zip_members()), once as the running extraction budget (process_fetched_list_zip()) - and neither read treated 0 as "no limit" the way dcc_fetch.py's own admission check does (#302).

Reproduced live right after #938 shipped: a real fetch from SamothMetal refused with zip's declared total uncompressed size exceeds MAX_FETCH_FILE_SIZE (0 bytes) - refusing to extract (zip-bomb guard), the setting explicitly at 0.

Fix: one helper, _fetch_file_size_budget(), resolving 0 to no limit (float('inf')) at the single point both call sites read the setting from - a fix that only patched the sum-check comparison would still fail on the very first byte written, since budget is seeded from the same value.

Test: test_zero_means_no_cap_on_the_declared_total_either. Mutation-checked against both sub-bugs (reverting the helper entirely, and fixing only the sum check while leaving the extraction budget raw) - each fails with the exact message that shape of the bug produces.

Verified live on the operator's own bot before this PR: deployed the branch, and a real fetch from SamothMetal (82,487,470-byte zip, 482,789,465 bytes uncompressed) that previously died at the zip-bomb guard now passes it cleanly and is instead correctly stopped by the unrelated, already-correct MAX_LIST_TEXT_SIZE ceiling (that list genuinely exceeds it).

Full suite 6698 OK.

🤖 Generated with Claude Code

Sibling of #937/#938 (#939). list_fetch.py reads MAX_FETCH_FILE_SIZE twice
as a zip-bomb guard on a fetched list's declared total uncompressed size,
and again as the running extraction budget - neither read treated 0 as no
limit, so a fetched list with the setting at 0 was rejected on the very
first byte, either at the pre-extraction sum check or, if that had been
fixed alone, at the first member's write.

Reproduced live right after #938 shipped: a real fetch from SamothMetal
refused with 'zip's declared total uncompressed size exceeds
MAX_FETCH_FILE_SIZE (0 bytes) - refusing to extract (zip-bomb guard)'.

Fix: one helper, _fetch_file_size_budget(), resolving 0 to no limit
(float('inf')) at the single point both call sites read the setting from.

tests/test_list_fetch.py: test_zero_means_no_cap_on_the_declared_total_either.
Mutation-checked against both sub-bugs: reverting the helper entirely, and
fixing only the sum check while leaving the extraction budget read raw,
each fail the test with the exact message that shape of the bug produces.
@chchatzop

Copy link
Copy Markdown
Collaborator

Review. The bug is real and the helper is the right shape, but there are two things to fix before merging:

1. The new test accidentally deletes two assertions from the test above it. In test_a_declared_total_size_over_the_cap_is_rejected_before_extracting, the last two lines were removed when the new test was inserted:

self.assertNotIn("bigbot", config.fetched_bot_lists)
self.assertFalse(os.path.exists(list_fetch.list_extract_dir("bigbot")))

Those lines are what prove a rejected zip leaves nothing behind (no registry entry, no extract dir). Please put them back.

2. With 0 resolved to float("inf"), lists lose their zip-bomb guard completely. The declared-total check in _validate_zip_members() is the only thing that bounds extraction. _extract_member()'s own docstring says its budget check is inert, because ZipExtFile only ever truncates to the declared size, which is now unbounded. max_list_text_size() is checked only after extraction. So the result:

An operator who sets "no limit" on file downloads isn't asking to switch off zip-bomb protection on list archives. My suggestion: when the setting is 0, the list budget falls back to what a real list archive can hold anyway:

def _fetch_file_size_budget():
    raw = int(getattr(config, "MAX_FETCH_FILE_SIZE", 200 * 1024 * 1024))
    # 0 is "no limit" for FILES (#302). For a LIST archive the zip-bomb guard
    # still needs a bound: every list over max_list_text_size() is rejected
    # after extraction anyway, so this refuses nothing a real list needs.
    return raw if raw > 0 else max_list_text_size() * MAX_LISTS_PER_ARCHIVE

With the defaults that's 128 MB × 8 = 1 GB. Your live SamothMetal case (482 MB uncompressed) still passes the guard and is still stopped by MAX_LIST_TEXT_SIZE, as it is now. It's also worth adding a test that a zip whose declared total is over that fallback is refused with the setting at 0. That case is the zip-bomb guard itself.

Everything else looks good. #937's list_zip_cap > 0 and and dcc_fetch.py's admission checks already treat 0 correctly, and I found no other 0-cap reads.

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.

MAX_FETCH_FILE_SIZE = 0 (no limit) trips the list zip-bomb guard too

2 participants