Skip to content

Commit f7d50f8

Browse files
bpo-36993: Improve error reporting for zipfiles with bad zip64 extra data. (pythonGH-14656)
(cherry picked from commit da6ce58) Co-authored-by: Daniel Hillier <daniel.hillier@gmail.com>
1 parent 379b55b commit f7d50f8

3 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lib/test/test_zipfile.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import io
3+
import itertools
34
import os
45
import importlib.util
56
import pathlib
@@ -801,6 +802,227 @@ def test_append(self):
801802
zinfo = zipfp.getinfo("strfile")
802803
self.assertEqual(zinfo.extra, extra)
803804

805+
def make_zip64_file(
806+
self, file_size_64_set=False, file_size_extra=False,
807+
compress_size_64_set=False, compress_size_extra=False,
808+
header_offset_64_set=False, header_offset_extra=False,
809+
):
810+
"""Generate bytes sequence for a zip with (incomplete) zip64 data.
811+
812+
The actual values (not the zip 64 0xffffffff values) stored in the file
813+
are:
814+
file_size: 8
815+
compress_size: 8
816+
header_offset: 0
817+
"""
818+
actual_size = 8
819+
actual_header_offset = 0
820+
local_zip64_fields = []
821+
central_zip64_fields = []
822+
823+
file_size = actual_size
824+
if file_size_64_set:
825+
file_size = 0xffffffff
826+
if file_size_extra:
827+
local_zip64_fields.append(actual_size)
828+
central_zip64_fields.append(actual_size)
829+
file_size = struct.pack("<L", file_size)
830+
831+
compress_size = actual_size
832+
if compress_size_64_set:
833+
compress_size = 0xffffffff
834+
if compress_size_extra:
835+
local_zip64_fields.append(actual_size)
836+
central_zip64_fields.append(actual_size)
837+
compress_size = struct.pack("<L", compress_size)
838+
839+
header_offset = actual_header_offset
840+
if header_offset_64_set:
841+
header_offset = 0xffffffff
842+
if header_offset_extra:
843+
central_zip64_fields.append(actual_header_offset)
844+
header_offset = struct.pack("<L", header_offset)
845+
846+
local_extra = struct.pack(
847+
'<HH' + 'Q'*len(local_zip64_fields),
848+
0x0001,
849+
8*len(local_zip64_fields),
850+
*local_zip64_fields
851+
)
852+
853+
central_extra = struct.pack(
854+
'<HH' + 'Q'*len(central_zip64_fields),
855+
0x0001,
856+
8*len(central_zip64_fields),
857+
*central_zip64_fields
858+
)
859+
860+
central_dir_size = struct.pack('<Q', 58 + 8 * len(central_zip64_fields))
861+
offset_to_central_dir = struct.pack('<Q', 50 + 8 * len(local_zip64_fields))
862+
863+
local_extra_length = struct.pack("<H", 4 + 8 * len(local_zip64_fields))
864+
central_extra_length = struct.pack("<H", 4 + 8 * len(central_zip64_fields))
865+
866+
filename = b"test.txt"
867+
content = b"test1234"
868+
filename_length = struct.pack("<H", len(filename))
869+
zip64_contents = (
870+
# Local file header
871+
b"PK\x03\x04\x14\x00\x00\x00\x00\x00\x00\x00!\x00\x9e%\xf5\xaf"
872+
+ compress_size
873+
+ file_size
874+
+ filename_length
875+
+ local_extra_length
876+
+ filename
877+
+ local_extra
878+
+ content
879+
# Central directory:
880+
+ b"PK\x01\x02-\x03-\x00\x00\x00\x00\x00\x00\x00!\x00\x9e%\xf5\xaf"
881+
+ compress_size
882+
+ file_size
883+
+ filename_length
884+
+ central_extra_length
885+
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x80\x01"
886+
+ header_offset
887+
+ filename
888+
+ central_extra
889+
# Zip64 end of central directory
890+
+ b"PK\x06\x06,\x00\x00\x00\x00\x00\x00\x00-\x00-"
891+
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00"
892+
+ b"\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
893+
+ central_dir_size
894+
+ offset_to_central_dir
895+
# Zip64 end of central directory locator
896+
+ b"PK\x06\x07\x00\x00\x00\x00l\x00\x00\x00\x00\x00\x00\x00\x01"
897+
+ b"\x00\x00\x00"
898+
# end of central directory
899+
+ b"PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00:\x00\x00\x002\x00"
900+
+ b"\x00\x00\x00\x00"
901+
)
902+
return zip64_contents
903+
904+
def test_bad_zip64_extra(self):
905+
"""Missing zip64 extra records raises an exception.
906+
907+
There are 4 fields that the zip64 format handles (the disk number is
908+
not used in this module and so is ignored here). According to the zip
909+
spec:
910+
The order of the fields in the zip64 extended
911+
information record is fixed, but the fields MUST
912+
only appear if the corresponding Local or Central
913+
directory record field is set to 0xFFFF or 0xFFFFFFFF.
914+
915+
If the zip64 extra content doesn't contain enough entries for the
916+
number of fields marked with 0xFFFF or 0xFFFFFFFF, we raise an error.
917+
This test mismatches the length of the zip64 extra field and the number
918+
of fields set to indicate the presence of zip64 data.
919+
"""
920+
# zip64 file size present, no fields in extra, expecting one, equals
921+
# missing file size.
922+
missing_file_size_extra = self.make_zip64_file(
923+
file_size_64_set=True,
924+
)
925+
with self.assertRaises(zipfile.BadZipFile) as e:
926+
zipfile.ZipFile(io.BytesIO(missing_file_size_extra))
927+
self.assertIn('file size', str(e.exception).lower())
928+
929+
# zip64 file size present, zip64 compress size present, one field in
930+
# extra, expecting two, equals missing compress size.
931+
missing_compress_size_extra = self.make_zip64_file(
932+
file_size_64_set=True,
933+
file_size_extra=True,
934+
compress_size_64_set=True,
935+
)
936+
with self.assertRaises(zipfile.BadZipFile) as e:
937+
zipfile.ZipFile(io.BytesIO(missing_compress_size_extra))
938+
self.assertIn('compress size', str(e.exception).lower())
939+
940+
# zip64 compress size present, no fields in extra, expecting one,
941+
# equals missing compress size.
942+
missing_compress_size_extra = self.make_zip64_file(
943+
compress_size_64_set=True,
944+
)
945+
with self.assertRaises(zipfile.BadZipFile) as e:
946+
zipfile.ZipFile(io.BytesIO(missing_compress_size_extra))
947+
self.assertIn('compress size', str(e.exception).lower())
948+
949+
# zip64 file size present, zip64 compress size present, zip64 header
950+
# offset present, two fields in extra, expecting three, equals missing
951+
# header offset
952+
missing_header_offset_extra = self.make_zip64_file(
953+
file_size_64_set=True,
954+
file_size_extra=True,
955+
compress_size_64_set=True,
956+
compress_size_extra=True,
957+
header_offset_64_set=True,
958+
)
959+
with self.assertRaises(zipfile.BadZipFile) as e:
960+
zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
961+
self.assertIn('header offset', str(e.exception).lower())
962+
963+
# zip64 compress size present, zip64 header offset present, one field
964+
# in extra, expecting two, equals missing header offset
965+
missing_header_offset_extra = self.make_zip64_file(
966+
file_size_64_set=False,
967+
compress_size_64_set=True,
968+
compress_size_extra=True,
969+
header_offset_64_set=True,
970+
)
971+
with self.assertRaises(zipfile.BadZipFile) as e:
972+
zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
973+
self.assertIn('header offset', str(e.exception).lower())
974+
975+
# zip64 file size present, zip64 header offset present, one field in
976+
# extra, expecting two, equals missing header offset
977+
missing_header_offset_extra = self.make_zip64_file(
978+
file_size_64_set=True,
979+
file_size_extra=True,
980+
compress_size_64_set=False,
981+
header_offset_64_set=True,
982+
)
983+
with self.assertRaises(zipfile.BadZipFile) as e:
984+
zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
985+
self.assertIn('header offset', str(e.exception).lower())
986+
987+
# zip64 header offset present, no fields in extra, expecting one,
988+
# equals missing header offset
989+
missing_header_offset_extra = self.make_zip64_file(
990+
file_size_64_set=False,
991+
compress_size_64_set=False,
992+
header_offset_64_set=True,
993+
)
994+
with self.assertRaises(zipfile.BadZipFile) as e:
995+
zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
996+
self.assertIn('header offset', str(e.exception).lower())
997+
998+
def test_generated_valid_zip64_extra(self):
999+
# These values are what is set in the make_zip64_file method.
1000+
expected_file_size = 8
1001+
expected_compress_size = 8
1002+
expected_header_offset = 0
1003+
expected_content = b"test1234"
1004+
1005+
# Loop through the various valid combinations of zip64 masks
1006+
# present and extra fields present.
1007+
params = (
1008+
{"file_size_64_set": True, "file_size_extra": True},
1009+
{"compress_size_64_set": True, "compress_size_extra": True},
1010+
{"header_offset_64_set": True, "header_offset_extra": True},
1011+
)
1012+
1013+
for r in range(1, len(params) + 1):
1014+
for combo in itertools.combinations(params, r):
1015+
kwargs = {}
1016+
for c in combo:
1017+
kwargs.update(c)
1018+
with zipfile.ZipFile(io.BytesIO(self.make_zip64_file(**kwargs))) as zf:
1019+
zinfo = zf.infolist()[0]
1020+
self.assertEqual(zinfo.file_size, expected_file_size)
1021+
self.assertEqual(zinfo.compress_size, expected_compress_size)
1022+
self.assertEqual(zinfo.header_offset, expected_header_offset)
1023+
self.assertEqual(zf.read(zinfo), expected_content)
1024+
1025+
8041026
@requires_zlib
8051027
class DeflateTestZip64InSmallFiles(AbstractTestZip64InSmallFiles,
8061028
unittest.TestCase):

Lib/zipfile.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,14 +477,26 @@ def _decodeExtra(self):
477477

478478
# ZIP64 extension (large files and/or large archives)
479479
if self.file_size in (0xffffffffffffffff, 0xffffffff):
480+
if len(counts) <= idx:
481+
raise BadZipFile(
482+
"Corrupt zip64 extra field. File size not found."
483+
)
480484
self.file_size = counts[idx]
481485
idx += 1
482486

483487
if self.compress_size == 0xFFFFFFFF:
488+
if len(counts) <= idx:
489+
raise BadZipFile(
490+
"Corrupt zip64 extra field. Compress size not found."
491+
)
484492
self.compress_size = counts[idx]
485493
idx += 1
486494

487495
if self.header_offset == 0xffffffff:
496+
if len(counts) <= idx:
497+
raise BadZipFile(
498+
"Corrupt zip64 extra field. Header offset not found."
499+
)
488500
old = self.header_offset
489501
self.header_offset = counts[idx]
490502
idx+=1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve error reporting for corrupt zip files with bad zip64 extra data. Patch
2+
by Daniel Hillier.

0 commit comments

Comments
 (0)