Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 85 additions & 2 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,12 +680,12 @@ def test_add_file_after_2107(self):
self.skipTest(f"Linux VFS/XFS kernel bug detected: {mtime_ns=}")

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

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


@requires_zlib()
Expand Down Expand Up @@ -4542,6 +4542,10 @@ def test_create_zipinfo_before_1980(self):
self.assertRaises(ValueError,
zipfile.ZipInfo, 'seventies', (1979, 1, 1, 0, 0, 0))

def test_create_zipinfo_after_2107(self):
self.assertRaises(ValueError,
zipfile.ZipInfo, 'future', (2108, 1, 1, 0, 0, 0))

def test_create_empty_zipinfo_repr(self):
"""Before bpo-26185, repr() on empty ZipInfo object was failing."""
zi = zipfile.ZipInfo(filename="empty")
Expand Down Expand Up @@ -5710,6 +5714,85 @@ def test_from_file(self):
self.assertFalse(zi.is_dir())
self.assertEqual(zi.file_size, os.path.getsize(__file__))

def test_from_file_rejects_timestamp_after_2107(self):
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
st_mtime=2**63, st_size=5)
mtime = (2108, 1, 1, 0, 0, 0, 0, 1, -1)
with mock.patch.object(zipfile.os, 'stat', return_value=st), \
mock.patch.object(zipfile.time, 'localtime', return_value=mtime):
with self.assertRaises(ValueError):
zipfile.ZipInfo.from_file('future')

def test_from_file_clamps_timestamp_after_2107_round_trip(self):
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
st_mtime=2**63, st_size=5)
mtime = (2108, 1, 1, 0, 0, 0, 0, 1, -1)
with mock.patch.object(zipfile.os, 'stat', return_value=st), \
mock.patch.object(zipfile.time, 'localtime', return_value=mtime):
zinfo = zipfile.ZipInfo.from_file(
'future', strict_timestamps=False)

expected = (2107, 12, 31, 23, 59, 58)
self.assertEqual(zinfo.date_time, expected)
archive = io.BytesIO()
with zipfile.ZipFile(archive, 'w') as zf:
zf.writestr(zinfo, b'future')
with zipfile.ZipFile(archive) as zf:
self.assertEqual(zf.getinfo('future').date_time, expected)

def test_from_file_clamps_localtime_overflow(self):
cases = (
(2**63, (2107, 12, 31, 23, 59, 58)),
(2**48, (2107, 12, 31, 23, 59, 58)),
(4386441600, (2107, 12, 31, 23, 59, 58)), # 2109-01-01 UTC
(-2**63, (1980, 1, 1, 0, 0, 0)),
(-2**48, (1980, 1, 1, 0, 0, 0)),
(0, (1980, 1, 1, 0, 0, 0)), # 1970-01-01 UTC
(157766400, (1980, 1, 1, 0, 0, 0)), # 1975-01-01 UTC
(283996799, (1980, 1, 1, 0, 0, 0)), # 1978-12-31 23:59:59 UTC
)
for exception in (OverflowError, OSError):
for timestamp, expected in cases:
with self.subTest(exception=exception, timestamp=timestamp):
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
st_mtime=timestamp, st_size=5)
with mock.patch.object(
zipfile.os, 'stat', return_value=st), \
mock.patch.object(zipfile.time, 'localtime',
side_effect=exception):
with self.assertRaises(exception):
zipfile.ZipInfo.from_file('future')
zinfo = zipfile.ZipInfo.from_file(
'future', strict_timestamps=False)
self.assertEqual(zinfo.date_time, expected)

def test_from_file_preserves_in_range_localtime_errors(self):
timestamps = (
283996800, # 1979-01-01 00:00:00 UTC (conservative lower bound)
315532799, # 1979-12-31 23:59:59 UTC
315532800, # 1980-01-01 00:00:00 UTC
2146003200, # 2038-01-02 00:00:00 UTC (AIX localtime overflow)
2**31, # Beyond the signed 32-bit time_t range
4354819199, # 2107-12-31 23:59:59 UTC
4354819200, # 2108-01-01 00:00:00 UTC
4386441599, # 2108-12-31 23:59:59 UTC
)
for exception in (OverflowError, OSError):
for timestamp in timestamps:
for strict_timestamps in (True, False):
with self.subTest(exception=exception, timestamp=timestamp,
strict_timestamps=strict_timestamps):
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
st_mtime=timestamp, st_size=5)
with mock.patch.object(
zipfile.os, 'stat', return_value=st), \
mock.patch.object(zipfile.time, 'localtime',
side_effect=exception):
with self.assertRaises(exception):
zipfile.ZipInfo.from_file(
'in_range',
strict_timestamps=strict_timestamps)

def test_from_file_pathlike(self):
zi = zipfile.ZipInfo.from_file(FakePath(__file__))
self.assertEqual(posixpath.basename(zi.filename), 'test_core.py')
Expand Down
25 changes: 19 additions & 6 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@ def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):

if date_time[0] < 1980:
raise ValueError('ZIP does not support timestamps before 1980')
if date_time[0] > 2107:
raise ValueError('ZIP does not support timestamps after 2107')

# Standard values:
self.compress_type = ZIP_STORED # Type of compression for the file
Expand Down Expand Up @@ -644,12 +646,23 @@ def from_file(cls, filename, arcname=None, *, strict_timestamps=True):
filename = os.fspath(filename)
st = os.stat(filename)
isdir = stat.S_ISDIR(st.st_mode)
mtime = time.localtime(st.st_mtime)
date_time = mtime[0:6]
if not strict_timestamps and date_time[0] < 1980:
date_time = (1980, 1, 1, 0, 0, 0)
elif not strict_timestamps and date_time[0] > 2107:
date_time = (2107, 12, 31, 23, 59, 59)
try:
mtime = time.localtime(st.st_mtime)
except (OverflowError, OSError):
# A platform limit (such as 2038) need not be a ZIP limit.
# UTC years 1979 through 2108 leave room for local time offsets.
if strict_timestamps or 283996800 <= st.st_mtime < 4386441600:
raise
if st.st_mtime < 283996800:
date_time = (1980, 1, 1, 0, 0, 0)
else:
date_time = (2107, 12, 31, 23, 59, 58)
else:
date_time = mtime[0:6]
if not strict_timestamps and date_time[0] < 1980:
date_time = (1980, 1, 1, 0, 0, 0)
elif not strict_timestamps and date_time[0] > 2107:
date_time = (2107, 12, 31, 23, 59, 58)
# Create ZipInfo instance to store file information
if arcname is None:
arcname = filename
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve :mod:`zipfile` timestamp validation. Reject years after 2107 and
clamp out-of-range filesystem timestamps when ``strict_timestamps`` is false.
Loading