From 7df06ad73669028ce64ec4a9bb7ecf9bf9b48c8d Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:20:53 +0800 Subject: [PATCH] fix(zipfile): reject negative local file header offsets --- Lib/test/test_zipfile/test_core.py | 35 +++++++++++++++++++ Lib/zipfile/__init__.py | 2 ++ ...6-10-06-14-17-00.gh-issue-91087.qZ3rTp.rst | 4 +++ 3 files changed, 41 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-06-14-17-00.gh-issue-91087.qZ3rTp.rst diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 708db4d4387df5..d4602c00176725 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -4319,6 +4319,41 @@ def test_negative_central_directory_offset_raises_BadZipFile(self): f = io.BytesIO(buffer) self.assertRaises(zipfile.BadZipFile, zipfile.ZipFile, f) + def test_negative_file_header_offset_raises_BadZipFile(self): + data = io.BytesIO() + with zipfile.ZipFile(data, "w") as zipf: + zipf.writestr("bad.txt", b"corrupt header") + zipf.writestr("good.txt", b"intact contents") + # Removing the first byte leaves the central directory intact, but + # makes the first member's local file header offset negative. + damaged = data.getvalue()[1:] + for file in get_files(self): + with self.subTest(type=type(file)): + if isinstance(file, str): + with open(file, "wb") as fp: + fp.write(damaged) + else: + file.write(damaged) + file.seek(0) + with zipfile.ZipFile(file) as zipf: + for name in ("bad.txt", zipf.getinfo("bad.txt")): + with self.assertRaises(zipfile.BadZipFile): + zipf.open(name) + self.assertEqual(zipf.testzip(), "bad.txt") + self.assertEqual(zipf.read("good.txt"), b"intact contents") + + def test_read_file_header_raises_OSError(self): + data = io.BytesIO() + with zipfile.ZipFile(data, "w") as zipf: + zipf.writestr("foo.txt", b"contents") + data.seek(0) + with zipfile.ZipFile(data) as zipf: + with mock.patch.object(data, "seek", side_effect=OSError("seek failed")): + with self.assertRaisesRegex(OSError, "seek failed"): + zipf.read("foo.txt") + with self.assertRaisesRegex(OSError, "seek failed"): + zipf.testzip() + def test_closed_zip_raises_ValueError(self): """Verify that testzip() doesn't swallow inappropriate exceptions.""" data = io.BytesIO() diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index d817bdd8769e7d..e9d5dd79b870d3 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2243,6 +2243,8 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False): "Close the writing handle before trying to read.") # Open for reading: + if zinfo.header_offset < 0: + raise BadZipFile("Bad offset for file header") self._fileRefCnt += 1 zef_file = _SharedFile(self.fp, zinfo.header_offset, self._fpclose, self._lock, lambda: self._writing) diff --git a/Misc/NEWS.d/next/Library/2026-10-06-14-17-00.gh-issue-91087.qZ3rTp.rst b/Misc/NEWS.d/next/Library/2026-10-06-14-17-00.gh-issue-91087.qZ3rTp.rst new file mode 100644 index 00000000000000..ece397a0ab2fdd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-06-14-17-00.gh-issue-91087.qZ3rTp.rst @@ -0,0 +1,4 @@ +:meth:`zipfile.ZipFile.open` now raises :exc:`~zipfile.BadZipFile` for negative +file header offsets in corrupted archives. :meth:`~zipfile.ZipFile.testzip` +reports the affected member instead of raising an :exc:`OSError` or +:exc:`ValueError`.