Skip to content

Commit d864fbc

Browse files
jjollymcepl
authored andcommitted
00427: ZipExtFile tell and seek, CVE-2024-0450
Backport of seek and tell methods for ZipExtFile makes it possible to backport the fix for CVE-2024-0450. Combines: python@066df4f python@066df4f
1 parent ae7d9cf commit d864fbc

File tree

4 files changed

+109
-3
lines changed

4 files changed

+109
-3
lines changed

Doc/library/zipfile.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,9 @@ ZipFile Objects
230230
With *mode* ``'r'`` the file-like object
231231
(``ZipExtFile``) is read-only and provides the following methods:
232232
:meth:`~io.BufferedIOBase.read`, :meth:`~io.IOBase.readline`,
233-
:meth:`~io.IOBase.readlines`, :meth:`__iter__`,
234-
:meth:`~iterator.__next__`. These objects can operate independently of
235-
the ZipFile.
233+
:meth:`~io.IOBase.readlines`, :meth:`~io.IOBase.seek`,
234+
:meth:`~io.IOBase.tell`, :meth:`__iter__`, :meth:`~iterator.__next__`.
235+
These objects can operate independently of the ZipFile.
236236

237237
With ``mode='w'``, a writable file handle is returned, which supports the
238238
:meth:`~io.BufferedIOBase.write` method. While a writable file handle is open,

Lib/test/test_zipfile.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,6 +1611,40 @@ def test_open_conflicting_handles(self):
16111611
self.assertEqual(zipf.read('baz'), msg3)
16121612
self.assertEqual(zipf.namelist(), ['foo', 'bar', 'baz'])
16131613

1614+
def test_seek_tell(self):
1615+
# Test seek functionality
1616+
txt = b"Where's Bruce?"
1617+
bloc = txt.find(b"Bruce")
1618+
# Check seek on a file
1619+
with zipfile.ZipFile(TESTFN, "w") as zipf:
1620+
zipf.writestr("foo.txt", txt)
1621+
with zipfile.ZipFile(TESTFN, "r") as zipf:
1622+
with zipf.open("foo.txt", "r") as fp:
1623+
fp.seek(bloc, os.SEEK_SET)
1624+
self.assertEqual(fp.tell(), bloc)
1625+
fp.seek(-bloc, os.SEEK_CUR)
1626+
self.assertEqual(fp.tell(), 0)
1627+
fp.seek(bloc, os.SEEK_CUR)
1628+
self.assertEqual(fp.tell(), bloc)
1629+
self.assertEqual(fp.read(5), txt[bloc:bloc+5])
1630+
fp.seek(0, os.SEEK_END)
1631+
self.assertEqual(fp.tell(), len(txt))
1632+
# Check seek on memory file
1633+
data = io.BytesIO()
1634+
with zipfile.ZipFile(data, mode="w") as zipf:
1635+
zipf.writestr("foo.txt", txt)
1636+
with zipfile.ZipFile(data, mode="r") as zipf:
1637+
with zipf.open("foo.txt", "r") as fp:
1638+
fp.seek(bloc, os.SEEK_SET)
1639+
self.assertEqual(fp.tell(), bloc)
1640+
fp.seek(-bloc, os.SEEK_CUR)
1641+
self.assertEqual(fp.tell(), 0)
1642+
fp.seek(bloc, os.SEEK_CUR)
1643+
self.assertEqual(fp.tell(), bloc)
1644+
self.assertEqual(fp.read(5), txt[bloc:bloc+5])
1645+
fp.seek(0, os.SEEK_END)
1646+
self.assertEqual(fp.tell(), len(txt))
1647+
16141648
@requires_zlib
16151649
def test_full_overlap(self):
16161650
data = (

Lib/zipfile.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,9 @@ class ZipExtFile(io.BufferedIOBase):
784784
# Read from compressed files in 4k blocks.
785785
MIN_READ_SIZE = 4096
786786

787+
# Chunk size to read during seek
788+
MAX_SEEK_READ = 1 << 24
789+
787790
def __init__(self, fileobj, mode, zipinfo, decrypter=None,
788791
close_fileobj=False):
789792
self._fileobj = fileobj
@@ -816,6 +819,17 @@ def __init__(self, fileobj, mode, zipinfo, decrypter=None,
816819
else:
817820
self._expected_crc = None
818821

822+
self._seekable = False
823+
try:
824+
if fileobj.seekable():
825+
self._orig_compress_start = fileobj.tell()
826+
self._orig_compress_size = zipinfo.compress_size
827+
self._orig_file_size = zipinfo.file_size
828+
self._orig_start_crc = self._running_crc
829+
self._seekable = True
830+
except AttributeError:
831+
pass
832+
819833
def __repr__(self):
820834
result = ['<%s.%s' % (self.__class__.__module__,
821835
self.__class__.__qualname__)]
@@ -1001,6 +1015,62 @@ def close(self):
10011015
finally:
10021016
super().close()
10031017

1018+
def seekable(self):
1019+
return self._seekable
1020+
1021+
def seek(self, offset, whence=0):
1022+
if not self._seekable:
1023+
raise io.UnsupportedOperation("underlying stream is not seekable")
1024+
curr_pos = self.tell()
1025+
if whence == 0: # Seek from start of file
1026+
new_pos = offset
1027+
elif whence == 1: # Seek from current position
1028+
new_pos = curr_pos + offset
1029+
elif whence == 2: # Seek from EOF
1030+
new_pos = self._orig_file_size + offset
1031+
else:
1032+
raise ValueError("whence must be os.SEEK_SET (0), "
1033+
"os.SEEK_CUR (1), or os.SEEK_END (2)")
1034+
1035+
if new_pos > self._orig_file_size:
1036+
new_pos = self._orig_file_size
1037+
1038+
if new_pos < 0:
1039+
new_pos = 0
1040+
1041+
read_offset = new_pos - curr_pos
1042+
buff_offset = read_offset + self._offset
1043+
1044+
if buff_offset >= 0 and buff_offset < len(self._readbuffer):
1045+
# Just move the _offset index if the new position is in the _readbuffer
1046+
self._offset = buff_offset
1047+
read_offset = 0
1048+
elif read_offset < 0:
1049+
# Position is before the current position. Reset the ZipExtFile
1050+
1051+
self._fileobj.seek(self._orig_compress_start)
1052+
self._running_crc = self._orig_start_crc
1053+
self._compress_left = self._orig_compress_size
1054+
self._left = self._orig_file_size
1055+
self._readbuffer = b''
1056+
self._offset = 0
1057+
self._decompressor = zipfile._get_decompressor(self._compress_type)
1058+
self._eof = False
1059+
read_offset = new_pos
1060+
1061+
while read_offset > 0:
1062+
read_len = min(self.MAX_SEEK_READ, read_offset)
1063+
self.read(read_len)
1064+
read_offset -= read_len
1065+
1066+
return self.tell()
1067+
1068+
def tell(self):
1069+
if not self._seekable:
1070+
raise io.UnsupportedOperation("underlying stream is not seekable")
1071+
filepos = self._orig_file_size - self._left - len(self._readbuffer) + self._offset
1072+
return filepos
1073+
10041074

10051075
class _ZipWriteFile(io.BufferedIOBase):
10061076
def __init__(self, zf, zinfo, zip64):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added seek and tell to the ZipExtFile class. This only works if the file
2+
object used to open the zipfile is seekable.

0 commit comments

Comments
 (0)