Skip to content

Commit 885bfbd

Browse files
committed
gh-154667: Validate zipfile timestamps consistently
Signed-off-by: Taeknology <20297177+Taeknology@users.noreply.github.com>
1 parent 5afbb60 commit 885bfbd

3 files changed

Lines changed: 69 additions & 8 deletions

File tree

Lib/test/test_zipfile/test_core.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,12 +676,12 @@ def test_add_file_after_2107(self):
676676
self.skipTest(f"Linux VFS/XFS kernel bug detected: {mtime_ns=}")
677677

678678
with zipfile.ZipFile(TESTFN2, "w") as zipfp:
679-
self.assertRaises(struct.error, zipfp.write, TESTFN)
679+
self.assertRaises(ValueError, zipfp.write, TESTFN)
680680

681681
with zipfile.ZipFile(TESTFN2, "w", strict_timestamps=False) as zipfp:
682682
zipfp.write(TESTFN)
683683
zinfo = zipfp.getinfo(TESTFN)
684-
self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 59))
684+
self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 58))
685685

686686

687687
@requires_zlib()
@@ -4476,6 +4476,10 @@ def test_create_zipinfo_before_1980(self):
44764476
self.assertRaises(ValueError,
44774477
zipfile.ZipInfo, 'seventies', (1979, 1, 1, 0, 0, 0))
44784478

4479+
def test_create_zipinfo_after_2107(self):
4480+
self.assertRaises(ValueError,
4481+
zipfile.ZipInfo, 'future', (2108, 1, 1, 0, 0, 0))
4482+
44794483
def test_create_empty_zipinfo_repr(self):
44804484
"""Before bpo-26185, repr() on empty ZipInfo object was failing."""
44814485
zi = zipfile.ZipInfo(filename="empty")
@@ -5528,6 +5532,50 @@ def test_from_file(self):
55285532
self.assertFalse(zi.is_dir())
55295533
self.assertEqual(zi.file_size, os.path.getsize(__file__))
55305534

5535+
def test_from_file_rejects_timestamp_after_2107(self):
5536+
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
5537+
st_mtime=2**63, st_size=5)
5538+
mtime = (2108, 1, 1, 0, 0, 0, 0, 1, -1)
5539+
with mock.patch.object(zipfile.os, 'stat', return_value=st), \
5540+
mock.patch.object(zipfile.time, 'localtime', return_value=mtime):
5541+
with self.assertRaises(ValueError):
5542+
zipfile.ZipInfo.from_file('future')
5543+
5544+
def test_from_file_clamps_timestamp_after_2107_round_trip(self):
5545+
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
5546+
st_mtime=2**63, st_size=5)
5547+
mtime = (2108, 1, 1, 0, 0, 0, 0, 1, -1)
5548+
with mock.patch.object(zipfile.os, 'stat', return_value=st), \
5549+
mock.patch.object(zipfile.time, 'localtime', return_value=mtime):
5550+
zinfo = zipfile.ZipInfo.from_file(
5551+
'future', strict_timestamps=False)
5552+
5553+
expected = (2107, 12, 31, 23, 59, 58)
5554+
self.assertEqual(zinfo.date_time, expected)
5555+
archive = io.BytesIO()
5556+
with zipfile.ZipFile(archive, 'w') as zf:
5557+
zf.writestr(zinfo, b'future')
5558+
with zipfile.ZipFile(archive) as zf:
5559+
self.assertEqual(zf.getinfo('future').date_time, expected)
5560+
5561+
def test_from_file_clamps_localtime_overflow(self):
5562+
cases = (
5563+
(2**63, (2107, 12, 31, 23, 59, 58)),
5564+
(-2**63, (1980, 1, 1, 0, 0, 0)),
5565+
)
5566+
for exception in (OverflowError, OSError):
5567+
for timestamp, expected in cases:
5568+
with self.subTest(exception=exception, timestamp=timestamp):
5569+
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
5570+
st_mtime=timestamp, st_size=5)
5571+
with mock.patch.object(
5572+
zipfile.os, 'stat', return_value=st), \
5573+
mock.patch.object(zipfile.time, 'localtime',
5574+
side_effect=exception):
5575+
zinfo = zipfile.ZipInfo.from_file(
5576+
'future', strict_timestamps=False)
5577+
self.assertEqual(zinfo.date_time, expected)
5578+
55315579
def test_from_file_pathlike(self):
55325580
zi = zipfile.ZipInfo.from_file(FakePath(__file__))
55335581
self.assertEqual(posixpath.basename(zi.filename), 'test_core.py')

Lib/zipfile/__init__.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,8 @@ def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
467467

468468
if date_time[0] < 1980:
469469
raise ValueError('ZIP does not support timestamps before 1980')
470+
if date_time[0] > 2107:
471+
raise ValueError('ZIP does not support timestamps after 2107')
470472

471473
# Standard values:
472474
self.compress_type = ZIP_STORED # Type of compression for the file
@@ -643,12 +645,21 @@ def from_file(cls, filename, arcname=None, *, strict_timestamps=True):
643645
filename = os.fspath(filename)
644646
st = os.stat(filename)
645647
isdir = stat.S_ISDIR(st.st_mode)
646-
mtime = time.localtime(st.st_mtime)
647-
date_time = mtime[0:6]
648-
if not strict_timestamps and date_time[0] < 1980:
649-
date_time = (1980, 1, 1, 0, 0, 0)
650-
elif not strict_timestamps and date_time[0] > 2107:
651-
date_time = (2107, 12, 31, 23, 59, 59)
648+
try:
649+
mtime = time.localtime(st.st_mtime)
650+
except (OverflowError, OSError):
651+
if strict_timestamps:
652+
raise
653+
if st.st_mtime < 0:
654+
date_time = (1980, 1, 1, 0, 0, 0)
655+
else:
656+
date_time = (2107, 12, 31, 23, 59, 58)
657+
else:
658+
date_time = mtime[0:6]
659+
if not strict_timestamps and date_time[0] < 1980:
660+
date_time = (1980, 1, 1, 0, 0, 0)
661+
elif not strict_timestamps and date_time[0] > 2107:
662+
date_time = (2107, 12, 31, 23, 59, 58)
652663
# Create ZipInfo instance to store file information
653664
if arcname is None:
654665
arcname = filename
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve :mod:`zipfile` timestamp validation. Reject years after 2107 and
2+
clamp out-of-range filesystem timestamps when ``strict_timestamps`` is false.

0 commit comments

Comments
 (0)