summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeonidas Da Silva Barbosa <[email protected]>2025-11-06 10:44:16 -0300
committergit-ubuntu importer <[email protected]>2025-11-24 14:22:28 +0000
commitd93ce35cfee42c22e87053c493216bb2696879df (patch)
tree3d974115ce9fcaecf217eb1c1e4f496331636d63
parent201279d890e23dcac5f72604fc363ff69535b583 (diff)
Imported using git-ubuntu import.
Notes
Notes: * SECURITY UPDATE: Possible payload obfuscation - debian/patches/CVE-2025-8291.patch: check consistency of the zip64 end of central dir record in Lib/zipfile.py, Lib/test/test_zipfile.py. - CVE-2025-8291 * SECURITY UPDATE: Performance degradation - debian/patches/CVE-2025-6075.patch: fix quadratic complexity in os.path.expandvars() in Lib/ntpatch.py, Lib/posixpath.py, Lib/test/test_genericpatch.py, Lib/test/test_npath.py. - CVE-2025-6075
-rw-r--r--debian/changelog15
-rw-r--r--debian/patches/CVE-2025-6075-12.patch351
-rw-r--r--debian/patches/CVE-2025-8291.patch284
-rw-r--r--debian/patches/series2
4 files changed, 652 insertions, 0 deletions
diff --git a/debian/changelog b/debian/changelog
index a77ec77c..68eb4544 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,18 @@
+python3.12 (3.12.3-1ubuntu0.9) noble-security; urgency=medium
+
+ * SECURITY UPDATE: Possible payload obfuscation
+ - debian/patches/CVE-2025-8291.patch: check consistency of
+ the zip64 end of central dir record in Lib/zipfile.py,
+ Lib/test/test_zipfile.py.
+ - CVE-2025-8291
+ * SECURITY UPDATE: Performance degradation
+ - debian/patches/CVE-2025-6075.patch: fix quadratic complexity
+ in os.path.expandvars() in Lib/ntpatch.py, Lib/posixpath.py,
+ Lib/test/test_genericpatch.py, Lib/test/test_npath.py.
+ - CVE-2025-6075
+
+ -- Leonidas Da Silva Barbosa <[email protected]> Thu, 06 Nov 2025 10:44:16 -0300
+
python3.12 (3.12.3-1ubuntu0.8) noble-security; urgency=medium
* SECURITY UPDATE: Regular expression denial of service.
diff --git a/debian/patches/CVE-2025-6075-12.patch b/debian/patches/CVE-2025-6075-12.patch
new file mode 100644
index 00000000..9acf2f6a
--- /dev/null
+++ b/debian/patches/CVE-2025-6075-12.patch
@@ -0,0 +1,351 @@
+From c8a5f3435c342964e0a432cc9fb448b7dbecd1ba Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?=C5=81ukasz=20Langa?= <[email protected]>
+Date: Fri, 31 Oct 2025 17:50:42 +0100
+Subject: [PATCH] [3.12] gh-136065: Fix quadratic complexity in
+ os.path.expandvars() (GH-134952) (GH-140847)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+(cherry picked from commit f029e8db626ddc6e3a3beea4eff511a71aaceb5c)
+
+Co-authored-by: Serhiy Storchaka <[email protected]>
+Co-authored-by: Ɓukasz Langa <[email protected]>
+---
+ Lib/ntpath.py | 126 ++++++------------
+ Lib/posixpath.py | 43 +++---
+ Lib/test/test_genericpath.py | 14 ++
+ Lib/test/test_ntpath.py | 23 +++-
+ ...-05-30-22-33-27.gh-issue-136065.bu337o.rst | 1 +
+ 5 files changed, 94 insertions(+), 113 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
+
+--- python3.12-3.12.3.orig/Lib/ntpath.py
++++ python3.12-3.12.3/Lib/ntpath.py
+@@ -409,17 +409,23 @@ def expanduser(path):
+ # XXX With COMMAND.COM you can use any characters in a variable name,
+ # XXX except '^|<>='.
+
++_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)"
++_varsub = None
++_varsubb = None
++
+ def expandvars(path):
+ """Expand shell variables of the forms $var, ${var} and %var%.
+
+ Unknown variables are left unchanged."""
+ path = os.fspath(path)
++ global _varsub, _varsubb
+ if isinstance(path, bytes):
+ if b'$' not in path and b'%' not in path:
+ return path
+- import string
+- varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
+- quote = b'\''
++ if not _varsubb:
++ import re
++ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
++ sub = _varsubb
+ percent = b'%'
+ brace = b'{'
+ rbrace = b'}'
+@@ -428,94 +434,44 @@ def expandvars(path):
+ else:
+ if '$' not in path and '%' not in path:
+ return path
+- import string
+- varchars = string.ascii_letters + string.digits + '_-'
+- quote = '\''
++ if not _varsub:
++ import re
++ _varsub = re.compile(_varpattern, re.ASCII).sub
++ sub = _varsub
+ percent = '%'
+ brace = '{'
+ rbrace = '}'
+ dollar = '$'
+ environ = os.environ
+- res = path[:0]
+- index = 0
+- pathlen = len(path)
+- while index < pathlen:
+- c = path[index:index+1]
+- if c == quote: # no expansion within single quotes
+- path = path[index + 1:]
+- pathlen = len(path)
+- try:
+- index = path.index(c)
+- res += c + path[:index + 1]
+- except ValueError:
+- res += c + path
+- index = pathlen - 1
+- elif c == percent: # variable or '%'
+- if path[index + 1:index + 2] == percent:
+- res += c
+- index += 1
+- else:
+- path = path[index+1:]
+- pathlen = len(path)
+- try:
+- index = path.index(percent)
+- except ValueError:
+- res += percent + path
+- index = pathlen - 1
+- else:
+- var = path[:index]
+- try:
+- if environ is None:
+- value = os.fsencode(os.environ[os.fsdecode(var)])
+- else:
+- value = environ[var]
+- except KeyError:
+- value = percent + var + percent
+- res += value
+- elif c == dollar: # variable or '$$'
+- if path[index + 1:index + 2] == dollar:
+- res += c
+- index += 1
+- elif path[index + 1:index + 2] == brace:
+- path = path[index+2:]
+- pathlen = len(path)
+- try:
+- index = path.index(rbrace)
+- except ValueError:
+- res += dollar + brace + path
+- index = pathlen - 1
+- else:
+- var = path[:index]
+- try:
+- if environ is None:
+- value = os.fsencode(os.environ[os.fsdecode(var)])
+- else:
+- value = environ[var]
+- except KeyError:
+- value = dollar + brace + var + rbrace
+- res += value
+- else:
+- var = path[:0]
+- index += 1
+- c = path[index:index + 1]
+- while c and c in varchars:
+- var += c
+- index += 1
+- c = path[index:index + 1]
+- try:
+- if environ is None:
+- value = os.fsencode(os.environ[os.fsdecode(var)])
+- else:
+- value = environ[var]
+- except KeyError:
+- value = dollar + var
+- res += value
+- if c:
+- index -= 1
++
++ def repl(m):
++ lastindex = m.lastindex
++ if lastindex is None:
++ return m[0]
++ name = m[lastindex]
++ if lastindex == 1:
++ if name == percent:
++ return name
++ if not name.endswith(percent):
++ return m[0]
++ name = name[:-1]
+ else:
+- res += c
+- index += 1
+- return res
++ if name == dollar:
++ return name
++ if name.startswith(brace):
++ if not name.endswith(rbrace):
++ return m[0]
++ name = name[1:-1]
++
++ try:
++ if environ is None:
++ return os.fsencode(os.environ[os.fsdecode(name)])
++ else:
++ return environ[name]
++ except KeyError:
++ return m[0]
++
++ return sub(repl, path)
+
+
+ # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
+--- python3.12-3.12.3.orig/Lib/posixpath.py
++++ python3.12-3.12.3/Lib/posixpath.py
+@@ -314,42 +314,41 @@ def expanduser(path):
+ # This expands the forms $variable and ${variable} only.
+ # Non-existent variables are left unchanged.
+
+-_varprog = None
+-_varprogb = None
++_varpattern = r'\$(\w+|\{[^}]*\}?)'
++_varsub = None
++_varsubb = None
+
+ def expandvars(path):
+ """Expand shell variables of form $var and ${var}. Unknown variables
+ are left unchanged."""
+ path = os.fspath(path)
+- global _varprog, _varprogb
++ global _varsub, _varsubb
+ if isinstance(path, bytes):
+ if b'$' not in path:
+ return path
+- if not _varprogb:
++ if not _varsubb:
+ import re
+- _varprogb = re.compile(br'\$(\w+|\{[^}]*\})', re.ASCII)
+- search = _varprogb.search
++ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
++ sub = _varsubb
+ start = b'{'
+ end = b'}'
+ environ = getattr(os, 'environb', None)
+ else:
+ if '$' not in path:
+ return path
+- if not _varprog:
++ if not _varsub:
+ import re
+- _varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII)
+- search = _varprog.search
++ _varsub = re.compile(_varpattern, re.ASCII).sub
++ sub = _varsub
+ start = '{'
+ end = '}'
+ environ = os.environ
+- i = 0
+- while True:
+- m = search(path, i)
+- if not m:
+- break
+- i, j = m.span(0)
+- name = m.group(1)
+- if name.startswith(start) and name.endswith(end):
++
++ def repl(m):
++ name = m[1]
++ if name.startswith(start):
++ if not name.endswith(end):
++ return m[0]
+ name = name[1:-1]
+ try:
+ if environ is None:
+@@ -357,13 +356,11 @@ def expandvars(path):
+ else:
+ value = environ[name]
+ except KeyError:
+- i = j
++ return m[0]
+ else:
+- tail = path[j:]
+- path = path[:i] + value
+- i = len(path)
+- path += tail
+- return path
++ return value
++
++ return sub(repl, path)
+
+
+ # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B.
+--- python3.12-3.12.3.orig/Lib/test/test_genericpath.py
++++ python3.12-3.12.3/Lib/test/test_genericpath.py
+@@ -7,6 +7,7 @@ import os
+ import sys
+ import unittest
+ import warnings
++from test import support
+ from test.support import is_emscripten
+ from test.support import os_helper
+ from test.support import warnings_helper
+@@ -434,6 +435,19 @@ class CommonTest(GenericTest):
+ os.fsencode('$bar%s bar' % nonascii))
+ check(b'$spam}bar', os.fsencode('%s}bar' % nonascii))
+
++ @support.requires_resource('cpu')
++ def test_expandvars_large(self):
++ expandvars = self.pathmodule.expandvars
++ with os_helper.EnvironmentVarGuard() as env:
++ env.clear()
++ env["A"] = "B"
++ n = 100_000
++ self.assertEqual(expandvars('$A'*n), 'B'*n)
++ self.assertEqual(expandvars('${A}'*n), 'B'*n)
++ self.assertEqual(expandvars('$A!'*n), 'B!'*n)
++ self.assertEqual(expandvars('${A}A'*n), 'BA'*n)
++ self.assertEqual(expandvars('${'*10*n), '${'*10*n)
++
+ def test_abspath(self):
+ self.assertIn("foo", self.pathmodule.abspath("foo"))
+ with warnings.catch_warnings():
+--- python3.12-3.12.3.orig/Lib/test/test_ntpath.py
++++ python3.12-3.12.3/Lib/test/test_ntpath.py
+@@ -7,8 +7,8 @@ import sys
+ import unittest
+ import warnings
+ from ntpath import ALLOW_MISSING
+-from test.support import cpython_only, os_helper
+-from test.support import TestFailed, is_emscripten
++from test import support
++from test.support import os_helper, is_emscripten
+ from test.support.os_helper import FakePath
+ from test import test_genericpath
+ from tempfile import TemporaryFile
+@@ -58,7 +58,7 @@ def tester(fn, wantResult):
+ fn = fn.replace("\\", "\\\\")
+ gotResult = eval(fn)
+ if wantResult != gotResult and _norm(wantResult) != _norm(gotResult):
+- raise TestFailed("%s should return: %s but returned: %s" \
++ raise support.TestFailed("%s should return: %s but returned: %s" \
+ %(str(fn), str(wantResult), str(gotResult)))
+
+ # then with bytes
+@@ -74,7 +74,7 @@ def tester(fn, wantResult):
+ warnings.simplefilter("ignore", DeprecationWarning)
+ gotResult = eval(fn)
+ if _norm(wantResult) != _norm(gotResult):
+- raise TestFailed("%s should return: %s but returned: %s" \
++ raise support.TestFailed("%s should return: %s but returned: %s" \
+ %(str(fn), str(wantResult), repr(gotResult)))
+
+
+@@ -927,6 +927,19 @@ class TestNtpath(NtpathTestCase):
+ check('%spam%bar', '%sbar' % nonascii)
+ check('%{}%bar'.format(nonascii), 'ham%sbar' % nonascii)
+
++ @support.requires_resource('cpu')
++ def test_expandvars_large(self):
++ expandvars = ntpath.expandvars
++ with os_helper.EnvironmentVarGuard() as env:
++ env.clear()
++ env["A"] = "B"
++ n = 100_000
++ self.assertEqual(expandvars('%A%'*n), 'B'*n)
++ self.assertEqual(expandvars('%A%A'*n), 'BA'*n)
++ self.assertEqual(expandvars("''"*n + '%%'), "''"*n + '%')
++ self.assertEqual(expandvars("%%"*n), "%"*n)
++ self.assertEqual(expandvars("$$"*n), "$"*n)
++
+ def test_expanduser(self):
+ tester('ntpath.expanduser("test")', 'test')
+
+@@ -1228,7 +1241,7 @@ class TestNtpath(NtpathTestCase):
+ self.assertTrue(os.path.exists(r"\\.\CON"))
+
+ @unittest.skipIf(sys.platform != 'win32', "Fast paths are only for win32")
+- @cpython_only
++ @support.cpython_only
+ def test_fast_paths_in_use(self):
+ # There are fast paths of these functions implemented in posixmodule.c.
+ # Confirm that they are being used, and not the Python fallbacks in
+--- /dev/null
++++ python3.12-3.12.3/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
+@@ -0,0 +1 @@
++Fix quadratic complexity in :func:`os.path.expandvars`.
diff --git a/debian/patches/CVE-2025-8291.patch b/debian/patches/CVE-2025-8291.patch
new file mode 100644
index 00000000..2ed526b0
--- /dev/null
+++ b/debian/patches/CVE-2025-8291.patch
@@ -0,0 +1,284 @@
+From 162997bb70e067668c039700141770687bc8f267 Mon Sep 17 00:00:00 2001
+From: Serhiy Storchaka <[email protected]>
+Date: Tue, 7 Oct 2025 20:15:26 +0300
+Subject: [PATCH] gh-139700: Check consistency of the zip64 end of central
+ directory record (GH-139702)
+
+Support records with "zip64 extensible data" if there are no bytes
+prepended to the ZIP file.
+--- python3.12-3.12.3.orig/Lib/test/test_zipfile/test_core.py
++++ python3.12-3.12.3/Lib/test/test_zipfile/test_core.py
+@@ -884,6 +884,8 @@ class StoredTestZip64InSmallFiles(Abstra
+ self, file_size_64_set=False, file_size_extra=False,
+ compress_size_64_set=False, compress_size_extra=False,
+ header_offset_64_set=False, header_offset_extra=False,
++ extensible_data=b'',
++ end_of_central_dir_size=None, offset_to_end_of_central_dir=None,
+ ):
+ """Generate bytes sequence for a zip with (incomplete) zip64 data.
+
+@@ -937,6 +939,12 @@ class StoredTestZip64InSmallFiles(Abstra
+
+ central_dir_size = struct.pack('<Q', 58 + 8 * len(central_zip64_fields))
+ offset_to_central_dir = struct.pack('<Q', 50 + 8 * len(local_zip64_fields))
++ if end_of_central_dir_size is None:
++ end_of_central_dir_size = 44 + len(extensible_data)
++ if offset_to_end_of_central_dir is None:
++ offset_to_end_of_central_dir = (108
++ + 8 * len(local_zip64_fields)
++ + 8 * len(central_zip64_fields))
+
+ local_extra_length = struct.pack("<H", 4 + 8 * len(local_zip64_fields))
+ central_extra_length = struct.pack("<H", 4 + 8 * len(central_zip64_fields))
+@@ -965,14 +973,17 @@ class StoredTestZip64InSmallFiles(Abstra
+ + filename
+ + central_extra
+ # Zip64 end of central directory
+- + b"PK\x06\x06,\x00\x00\x00\x00\x00\x00\x00-\x00-"
+- + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00"
++ + b"PK\x06\x06"
++ + struct.pack('<Q', end_of_central_dir_size)
++ + b"-\x00-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00"
+ + b"\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
+ + central_dir_size
+ + offset_to_central_dir
++ + extensible_data
+ # Zip64 end of central directory locator
+- + b"PK\x06\x07\x00\x00\x00\x00l\x00\x00\x00\x00\x00\x00\x00\x01"
+- + b"\x00\x00\x00"
++ + b"PK\x06\x07\x00\x00\x00\x00"
++ + struct.pack('<Q', offset_to_end_of_central_dir)
++ + b"\x01\x00\x00\x00"
+ # end of central directory
+ + b"PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00:\x00\x00\x002\x00"
+ + b"\x00\x00\x00\x00"
+@@ -1003,6 +1014,7 @@ class StoredTestZip64InSmallFiles(Abstra
+ with self.assertRaises(zipfile.BadZipFile) as e:
+ zipfile.ZipFile(io.BytesIO(missing_file_size_extra))
+ self.assertIn('file size', str(e.exception).lower())
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_file_size_extra)))
+
+ # zip64 file size present, zip64 compress size present, one field in
+ # extra, expecting two, equals missing compress size.
+@@ -1014,6 +1026,7 @@ class StoredTestZip64InSmallFiles(Abstra
+ with self.assertRaises(zipfile.BadZipFile) as e:
+ zipfile.ZipFile(io.BytesIO(missing_compress_size_extra))
+ self.assertIn('compress size', str(e.exception).lower())
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_compress_size_extra)))
+
+ # zip64 compress size present, no fields in extra, expecting one,
+ # equals missing compress size.
+@@ -1023,6 +1036,7 @@ class StoredTestZip64InSmallFiles(Abstra
+ with self.assertRaises(zipfile.BadZipFile) as e:
+ zipfile.ZipFile(io.BytesIO(missing_compress_size_extra))
+ self.assertIn('compress size', str(e.exception).lower())
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_compress_size_extra)))
+
+ # zip64 file size present, zip64 compress size present, zip64 header
+ # offset present, two fields in extra, expecting three, equals missing
+@@ -1037,6 +1051,7 @@ class StoredTestZip64InSmallFiles(Abstra
+ with self.assertRaises(zipfile.BadZipFile) as e:
+ zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
+ self.assertIn('header offset', str(e.exception).lower())
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra)))
+
+ # zip64 compress size present, zip64 header offset present, one field
+ # in extra, expecting two, equals missing header offset
+@@ -1049,6 +1064,7 @@ class StoredTestZip64InSmallFiles(Abstra
+ with self.assertRaises(zipfile.BadZipFile) as e:
+ zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
+ self.assertIn('header offset', str(e.exception).lower())
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra)))
+
+ # zip64 file size present, zip64 header offset present, one field in
+ # extra, expecting two, equals missing header offset
+@@ -1061,6 +1077,7 @@ class StoredTestZip64InSmallFiles(Abstra
+ with self.assertRaises(zipfile.BadZipFile) as e:
+ zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
+ self.assertIn('header offset', str(e.exception).lower())
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra)))
+
+ # zip64 header offset present, no fields in extra, expecting one,
+ # equals missing header offset
+@@ -1072,6 +1089,63 @@ class StoredTestZip64InSmallFiles(Abstra
+ with self.assertRaises(zipfile.BadZipFile) as e:
+ zipfile.ZipFile(io.BytesIO(missing_header_offset_extra))
+ self.assertIn('header offset', str(e.exception).lower())
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra)))
++
++ def test_bad_zip64_end_of_central_dir(self):
++ zipdata = self.make_zip64_file(end_of_central_dir_size=0)
++ with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*record'):
++ zipfile.ZipFile(io.BytesIO(zipdata))
++ self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata)))
++
++ zipdata = self.make_zip64_file(end_of_central_dir_size=100)
++ with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*record'):
++ zipfile.ZipFile(io.BytesIO(zipdata))
++ self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata)))
++
++ zipdata = self.make_zip64_file(offset_to_end_of_central_dir=0)
++ with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*record'):
++ zipfile.ZipFile(io.BytesIO(zipdata))
++ self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata)))
++
++ zipdata = self.make_zip64_file(offset_to_end_of_central_dir=1000)
++ with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*locator'):
++ zipfile.ZipFile(io.BytesIO(zipdata))
++ self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata)))
++
++ def test_zip64_end_of_central_dir_record_not_found(self):
++ zipdata = self.make_zip64_file()
++ zipdata = zipdata.replace(b"PK\x06\x06", b'\x00'*4)
++ with self.assertRaisesRegex(zipfile.BadZipFile, 'record not found'):
++ zipfile.ZipFile(io.BytesIO(zipdata))
++ self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata)))
++
++ zipdata = self.make_zip64_file(
++ extensible_data=b'\xca\xfe\x04\x00\x00\x00data')
++ zipdata = zipdata.replace(b"PK\x06\x06", b'\x00'*4)
++ with self.assertRaisesRegex(zipfile.BadZipFile, 'record not found'):
++ zipfile.ZipFile(io.BytesIO(zipdata))
++ self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata)))
++
++ def test_zip64_extensible_data(self):
++ # These values are what is set in the make_zip64_file method.
++ expected_file_size = 8
++ expected_compress_size = 8
++ expected_header_offset = 0
++ expected_content = b"test1234"
++
++ zipdata = self.make_zip64_file(
++ extensible_data=b'\xca\xfe\x04\x00\x00\x00data')
++ with zipfile.ZipFile(io.BytesIO(zipdata)) as zf:
++ zinfo = zf.infolist()[0]
++ self.assertEqual(zinfo.file_size, expected_file_size)
++ self.assertEqual(zinfo.compress_size, expected_compress_size)
++ self.assertEqual(zinfo.header_offset, expected_header_offset)
++ self.assertEqual(zf.read(zinfo), expected_content)
++ self.assertTrue(zipfile.is_zipfile(io.BytesIO(zipdata)))
++
++ with self.assertRaisesRegex(zipfile.BadZipFile, 'record not found'):
++ zipfile.ZipFile(io.BytesIO(b'prepended' + zipdata))
++ self.assertFalse(zipfile.is_zipfile(io.BytesIO(b'prepended' + zipdata)))
+
+ def test_generated_valid_zip64_extra(self):
+ # These values are what is set in the make_zip64_file method.
+--- python3.12-3.12.3.orig/Lib/zipfile/__init__.py
++++ python3.12-3.12.3/Lib/zipfile/__init__.py
+@@ -231,7 +231,7 @@ def is_zipfile(filename):
+ else:
+ with open(filename, "rb") as fp:
+ result = _check_zipfile(fp)
+- except OSError:
++ except (OSError, BadZipFile):
+ pass
+ return result
+
+@@ -239,16 +239,15 @@ def _EndRecData64(fpin, offset, endrec):
+ """
+ Read the ZIP64 end-of-archive records and use that to update endrec
+ """
+- try:
+- fpin.seek(offset - sizeEndCentDir64Locator, 2)
+- except OSError:
+- # If the seek fails, the file is not large enough to contain a ZIP64
++ offset -= sizeEndCentDir64Locator
++ if offset < 0:
++ # The file is not large enough to contain a ZIP64
+ # end-of-archive record, so just return the end record we were given.
+ return endrec
+-
++ fpin.seek(offset)
+ data = fpin.read(sizeEndCentDir64Locator)
+ if len(data) != sizeEndCentDir64Locator:
+- return endrec
++ raise OSError("Unknown I/O error")
+ sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data)
+ if sig != stringEndArchive64Locator:
+ return endrec
+@@ -256,16 +255,33 @@ def _EndRecData64(fpin, offset, endrec):
+ if diskno != 0 or disks > 1:
+ raise BadZipFile("zipfiles that span multiple disks are not supported")
+
+- # Assume no 'zip64 extensible data'
+- fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2)
++ offset -= sizeEndCentDir64
++ if reloff > offset:
++ raise BadZipFile("Corrupt zip64 end of central directory locator")
++ # First, check the assumption that there is no prepended data.
++ fpin.seek(reloff)
++ extrasz = offset - reloff
+ data = fpin.read(sizeEndCentDir64)
+ if len(data) != sizeEndCentDir64:
+- return endrec
++ raise OSError("Unknown I/O error")
++ if not data.startswith(stringEndArchive64) and reloff != offset:
++ # Since we already have seen the Zip64 EOCD Locator, it's
++ # possible we got here because there is prepended data.
++ # Assume no 'zip64 extensible data'
++ fpin.seek(offset)
++ extrasz = 0
++ data = fpin.read(sizeEndCentDir64)
++ if len(data) != sizeEndCentDir64:
++ raise OSError("Unknown I/O error")
++ if not data.startswith(stringEndArchive64):
++ raise BadZipFile("Zip64 end of central directory record not found")
++
+ sig, sz, create_version, read_version, disk_num, disk_dir, \
+ dircount, dircount2, dirsize, diroffset = \
+ struct.unpack(structEndArchive64, data)
+- if sig != stringEndArchive64:
+- return endrec
++ if (diroffset + dirsize != reloff or
++ sz + 12 != sizeEndCentDir64 + extrasz):
++ raise BadZipFile("Corrupt zip64 end of central directory record")
+
+ # Update the original endrec using data from the ZIP64 record
+ endrec[_ECD_SIGNATURE] = sig
+@@ -275,6 +291,7 @@ def _EndRecData64(fpin, offset, endrec):
+ endrec[_ECD_ENTRIES_TOTAL] = dircount2
+ endrec[_ECD_SIZE] = dirsize
+ endrec[_ECD_OFFSET] = diroffset
++ endrec[_ECD_LOCATION] = offset - extrasz
+ return endrec
+
+
+@@ -308,7 +325,7 @@ def _EndRecData(fpin):
+ endrec.append(filesize - sizeEndCentDir)
+
+ # Try to read the "Zip64 end of central directory" structure
+- return _EndRecData64(fpin, -sizeEndCentDir, endrec)
++ return _EndRecData64(fpin, filesize - sizeEndCentDir, endrec)
+
+ # Either this is not a ZIP file, or it is a ZIP file with an archive
+ # comment. Search the end of the file for the "end of central directory"
+@@ -332,8 +349,7 @@ def _EndRecData(fpin):
+ endrec.append(maxCommentStart + start)
+
+ # Try to read the "Zip64 end of central directory" structure
+- return _EndRecData64(fpin, maxCommentStart + start - filesize,
+- endrec)
++ return _EndRecData64(fpin, maxCommentStart + start, endrec)
+
+ # Unable to find a valid end of central directory structure
+ return None
+@@ -1422,9 +1438,6 @@ class ZipFile:
+
+ # "concat" is zero, unless zip was concatenated to another file
+ concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
+- if endrec[_ECD_SIGNATURE] == stringEndArchive64:
+- # If Zip64 extension structures are present, account for them
+- concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator)
+
+ if self.debug > 2:
+ inferred = concat + offset_cd
+@@ -2034,7 +2047,7 @@ class ZipFile:
+ " would require ZIP64 extensions")
+ zip64endrec = struct.pack(
+ structEndArchive64, stringEndArchive64,
+- 44, 45, 45, 0, 0, centDirCount, centDirCount,
++ sizeEndCentDir64 - 12, 45, 45, 0, 0, centDirCount, centDirCount,
+ centDirSize, centDirOffset)
+ self.fp.write(zip64endrec)
+
diff --git a/debian/patches/series b/debian/patches/series
index e8b405cf..b813419d 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -48,3 +48,5 @@ CVE-2025-4516.patch
CVE-202x-12718-4138-4x3x-4517.patch
CVE-2025-6069.patch
CVE-2025-8194.patch
+CVE-2025-6075-12.patch
+CVE-2025-8291.patch