2222import pickle
2323import sys
2424import tempfile
25+ import zipfile
2526
2627sys .path .insert (0 , str (pathlib .Path (__file__ ).resolve ().parent .parent ))
2728
@@ -226,6 +227,52 @@ def __reduce__(self):
226227 )
227228
228229
230+ def test_zip_member_resource_limits ():
231+ """ZIP scanning must bound decompression and report limit hits as suspicious."""
232+ old_member_limit = modelhawk ._MAX_ARCHIVE_MEMBER_BYTES
233+ old_total_limit = modelhawk ._MAX_ARCHIVE_TOTAL_BYTES
234+ old_count_limit = modelhawk ._MAX_ARCHIVE_MEMBERS
235+ try :
236+ modelhawk ._MAX_ARCHIVE_MEMBER_BYTES = 32
237+ modelhawk ._MAX_ARCHIVE_TOTAL_BYTES = 48
238+ modelhawk ._MAX_ARCHIVE_MEMBERS = 100
239+ with tempfile .TemporaryDirectory () as d :
240+ oversized = os .path .join (d , "oversized.zip" )
241+ with zipfile .ZipFile (oversized , "w" , compression = zipfile .ZIP_DEFLATED ) as z :
242+ z .writestr ("huge.pt" , b"A" * 128 )
243+ report = modelhawk .scan_file (oversized )
244+ assert report .verdict == "MEDIUM" , (
245+ f"oversized ZIP member must be suspicious, got { report .verdict } "
246+ )
247+ assert any ("archive-limit:member-size" in s .label for s in report .streams )
248+
249+ aggregate = os .path .join (d , "aggregate.zip" )
250+ with zipfile .ZipFile (aggregate , "w" , compression = zipfile .ZIP_STORED ) as z :
251+ z .writestr ("first.bin" , b"A" * 24 )
252+ z .writestr ("second.bin" , b"B" * 32 )
253+ report = modelhawk .scan_file (aggregate )
254+ assert report .verdict == "MEDIUM" , (
255+ f"aggregate ZIP limit must be suspicious, got { report .verdict } "
256+ )
257+ assert any ("archive-limit:total-size" in s .label for s in report .streams )
258+
259+ modelhawk ._MAX_ARCHIVE_TOTAL_BYTES = 1024
260+ modelhawk ._MAX_ARCHIVE_MEMBERS = 1
261+ count_limited = os .path .join (d , "count-limited.zip" )
262+ with zipfile .ZipFile (count_limited , "w" , compression = zipfile .ZIP_STORED ) as z :
263+ z .writestr ("one.bin" , b"A" )
264+ z .writestr ("two.bin" , b"B" )
265+ report = modelhawk .scan_file (count_limited )
266+ assert report .verdict == "MEDIUM" , (
267+ f"archive member-count limit must be suspicious, got { report .verdict } "
268+ )
269+ assert any ("archive-limit:member-count" in s .label for s in report .streams )
270+ finally :
271+ modelhawk ._MAX_ARCHIVE_MEMBER_BYTES = old_member_limit
272+ modelhawk ._MAX_ARCHIVE_TOTAL_BYTES = old_total_limit
273+ modelhawk ._MAX_ARCHIVE_MEMBERS = old_count_limit
274+
275+
229276if __name__ == "__main__" :
230277 tests = [
231278 test_detection_and_no_false_positives ,
@@ -237,6 +284,7 @@ def __reduce__(self):
237284 test_npy_float_array_safe ,
238285 test_npy_header_bounds_and_versions ,
239286 test_npy_real_numpy_object_array ,
287+ test_zip_member_resource_limits ,
240288 ]
241289 failed = 0
242290 for fn in tests :
0 commit comments