Control: tag 1070133 +patch
Control: tag 1070135 +patch

Here's a debdiff against what's already in 3.11.2-6+deb12u1 in
-proposed-updates

-- 
Steve McIntyre, Cambridge, UK.                                st...@einval.com
< sladen> I actually stayed in a hotel and arrived to find a post-it
          note stuck to the mini-bar saying "Paul: This fridge and
          fittings are the correct way around and do not need altering"
diff -Nru python3.11-3.11.2/debian/changelog python3.11-3.11.2/debian/changelog
--- python3.11-3.11.2/debian/changelog  2024-03-02 20:28:50.000000000 +0000
+++ python3.11-3.11.2/debian/changelog  2024-04-26 16:10:48.000000000 +0100
@@ -1,3 +1,14 @@
+python3.11 (3.11.2-6+deb12u2) bookworm; urgency=medium
+
+  * Apply upstream security fix for CVE-2024-0450
+    Protect zipfile from "quoted-overlap" zipbomb.
+    Closes: #1070133
+  * Apply and tweak upstream security fix for CVE-2023-6597
+    tempfile.TemporaryDirectory: fix symlink bug in cleanup
+    Closes: #1070135
+
+ -- Steve McIntyre <steve.mcint...@pexip.com>  Fri, 26 Apr 2024 16:10:48 +0100
+
 python3.11 (3.11.2-6+deb12u1) bookworm; urgency=medium
 
   [ Anders Kaseorg ]
diff -Nru python3.11-3.11.2/debian/patches/CVE-2023-6597.patch 
python3.11-3.11.2/debian/patches/CVE-2023-6597.patch
--- python3.11-3.11.2/debian/patches/CVE-2023-6597.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.11-3.11.2/debian/patches/CVE-2023-6597.patch        2024-04-26 
16:10:48.000000000 +0100
@@ -0,0 +1,202 @@
+commit 5585334d772b253a01a6730e8202ffb1607c3d25
+Author: Serhiy Storchaka <storch...@gmail.com>
+Date:   Thu Dec 7 18:37:10 2023 +0200
+
+    [3.11] gh-91133: tempfile.TemporaryDirectory: fix symlink bug in cleanup 
(GH-99930) (GH-112839)
+    
+    (cherry picked from commit 81c16cd94ec38d61aa478b9a452436dc3b1b524d)
+    
+    Co-authored-by: Søren Løvborg <sor...@unity3d.com>
+
+diff --git a/Lib/tempfile.py b/Lib/tempfile.py
+index aace11fa7b..f59a63a7b4 100644
+--- a/Lib/tempfile.py
++++ b/Lib/tempfile.py
+@@ -270,6 +270,22 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
+     raise FileExistsError(_errno.EEXIST,
+                           "No usable temporary file name found")
+ 
++def _dont_follow_symlinks(func, path, *args):
++    # Pass follow_symlinks=False, unless not supported on this platform.
++    if func in _os.supports_follow_symlinks:
++        func(path, *args, follow_symlinks=False)
++    elif _os.name == 'nt' or not _os.path.islink(path):
++        func(path, *args)
++
++def _resetperms(path):
++    try:
++        chflags = _os.chflags
++    except AttributeError:
++        pass
++    else:
++        _dont_follow_symlinks(chflags, path, 0)
++    _dont_follow_symlinks(_os.chmod, path, 0o700)
++
+ 
+ # User visible interfaces.
+ 
+@@ -863,17 +879,10 @@ def __init__(self, suffix=None, prefix=None, dir=None,
+     def _rmtree(cls, name, ignore_errors=False):
+         def onerror(func, path, exc_info):
+             if issubclass(exc_info[0], PermissionError):
+-                def resetperms(path):
+-                    try:
+-                        _os.chflags(path, 0)
+-                    except AttributeError:
+-                        pass
+-                    _os.chmod(path, 0o700)
+-
+                 try:
+                     if path != name:
+-                        resetperms(_os.path.dirname(path))
+-                    resetperms(path)
++                        _resetperms(_os.path.dirname(path))
++                    _resetperms(path)
+ 
+                     try:
+                         _os.unlink(path)
+diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py
+index 1242ec7e3c..675edc8de9 100644
+--- a/Lib/test/test_tempfile.py
++++ b/Lib/test/test_tempfile.py
+@@ -1565,6 +1565,103 @@ def test_cleanup_with_symlink_to_a_directory(self):
+                          "were deleted")
+         d2.cleanup()
+ 
++    @os_helper.skip_unless_symlink
++    def test_cleanup_with_symlink_modes(self):
++        # cleanup() should not follow symlinks when fixing mode bits (#91133)
++        with self.do_create(recurse=0) as d2:
++            file1 = os.path.join(d2, 'file1')
++            open(file1, 'wb').close()
++            dir1 = os.path.join(d2, 'dir1')
++            os.mkdir(dir1)
++            for mode in range(8):
++                mode <<= 6
++                with self.subTest(mode=format(mode, '03o')):
++                    def test(target, target_is_directory):
++                        d1 = self.do_create(recurse=0)
++                        symlink = os.path.join(d1.name, 'symlink')
++                        os.symlink(target, symlink,
++                                target_is_directory=target_is_directory)
++                        try:
++                            os.chmod(symlink, mode, follow_symlinks=False)
++                        except NotImplementedError:
++                            pass
++                        try:
++                            os.chmod(symlink, mode)
++                        except FileNotFoundError:
++                            pass
++                        os.chmod(d1.name, mode)
++                        d1.cleanup()
++                        self.assertFalse(os.path.exists(d1.name))
++
++                    with self.subTest('nonexisting file'):
++                        test('nonexisting', target_is_directory=False)
++                    with self.subTest('nonexisting dir'):
++                        test('nonexisting', target_is_directory=True)
++
++                    with self.subTest('existing file'):
++                        os.chmod(file1, mode)
++                        old_mode = os.stat(file1).st_mode
++                        test(file1, target_is_directory=False)
++                        new_mode = os.stat(file1).st_mode
++                        self.assertEqual(new_mode, old_mode,
++                                         '%03o != %03o' % (new_mode, 
old_mode))
++
++                    with self.subTest('existing dir'):
++                        os.chmod(dir1, mode)
++                        old_mode = os.stat(dir1).st_mode
++                        test(dir1, target_is_directory=True)
++                        new_mode = os.stat(dir1).st_mode
++                        self.assertEqual(new_mode, old_mode,
++                                         '%03o != %03o' % (new_mode, 
old_mode))
++
++    @unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
++    @os_helper.skip_unless_symlink
++    def test_cleanup_with_symlink_flags(self):
++        # cleanup() should not follow symlinks when fixing flags (#91133)
++        flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
++        self.check_flags(flags)
++
++        with self.do_create(recurse=0) as d2:
++            file1 = os.path.join(d2, 'file1')
++            open(file1, 'wb').close()
++            dir1 = os.path.join(d2, 'dir1')
++            os.mkdir(dir1)
++            def test(target, target_is_directory):
++                d1 = self.do_create(recurse=0)
++                symlink = os.path.join(d1.name, 'symlink')
++                os.symlink(target, symlink,
++                           target_is_directory=target_is_directory)
++                try:
++                    os.chflags(symlink, flags, follow_symlinks=False)
++                except NotImplementedError:
++                    pass
++                try:
++                    os.chflags(symlink, flags)
++                except FileNotFoundError:
++                    pass
++                os.chflags(d1.name, flags)
++                d1.cleanup()
++                self.assertFalse(os.path.exists(d1.name))
++
++            with self.subTest('nonexisting file'):
++                test('nonexisting', target_is_directory=False)
++            with self.subTest('nonexisting dir'):
++                test('nonexisting', target_is_directory=True)
++
++            with self.subTest('existing file'):
++                os.chflags(file1, flags)
++                old_flags = os.stat(file1).st_flags
++                test(file1, target_is_directory=False)
++                new_flags = os.stat(file1).st_flags
++                self.assertEqual(new_flags, old_flags)
++
++            with self.subTest('existing dir'):
++                os.chflags(dir1, flags)
++                old_flags = os.stat(dir1).st_flags
++                test(dir1, target_is_directory=True)
++                new_flags = os.stat(dir1).st_flags
++                self.assertEqual(new_flags, old_flags)
++
+     @support.cpython_only
+     def test_del_on_collection(self):
+         # A TemporaryDirectory is deleted when garbage collected
+@@ -1726,9 +1726,27 @@ class TestTemporaryDirectory(BaseTestCase):
+                     d.cleanup()
+                 self.assertFalse(os.path.exists(d.name))
+ 
+-    @unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.lchflags')
++    def check_flags(self, flags):
++        # skip the test if these flags are not supported (ex: FreeBSD 13)
++        filename = os_helper.TESTFN
++        try:
++            open(filename, "w").close()
++            try:
++                os.chflags(filename, flags)
++            except OSError as exc:
++                # "OSError: [Errno 45] Operation not supported"
++                self.skipTest(f"chflags() doesn't support flags "
++                              f"{flags:#b}: {exc}")
++            else:
++                os.chflags(filename, 0)
++        finally:
++            os_helper.unlink(filename)
++
++    @unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
+     def test_flags(self):
+         flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
++        self.check_flags(flags)
++
+         d = self.do_create(recurse=3, dirs=2, files=2)
+         with d:
+             # Change files and directories flags recursively.
+diff --git 
a/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst 
b/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst
+new file mode 100644
+index 0000000000..7991048fc4
+--- /dev/null
++++ b/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst
+@@ -0,0 +1,2 @@
++Fix a bug in :class:`tempfile.TemporaryDirectory` cleanup, which now no longer
++dereferences symlinks when working around file system permission errors.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2024-0450.patch 
python3.11-3.11.2/debian/patches/CVE-2024-0450.patch
--- python3.11-3.11.2/debian/patches/CVE-2024-0450.patch        1970-01-01 
01:00:00.000000000 +0100
+++ python3.11-3.11.2/debian/patches/CVE-2024-0450.patch        2024-04-26 
16:10:48.000000000 +0100
@@ -0,0 +1,136 @@
+commit a956e510f6336d5ae111ba429a61c3ade30a7549
+Author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
+Date:   Thu Jan 11 10:24:47 2024 +0100
+
+    [3.11] gh-109858: Protect zipfile from "quoted-overlap" zipbomb 
(GH-110016) (GH-113913)
+    
+    Raise BadZipFile when try to read an entry that overlaps with other entry 
or
+    central directory.
+    (cherry picked from commit 66363b9a7b9fe7c99eba3a185b74c5fdbf842eba)
+    
+    Co-authored-by: Serhiy Storchaka <storch...@gmail.com>
+
+diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
+index c8e0159765..9354ab74fa 100644
+--- a/Lib/test/test_zipfile.py
++++ b/Lib/test/test_zipfile.py
+@@ -2216,6 +2216,66 @@ def test_decompress_without_3rd_party_library(self):
+             with zipfile.ZipFile(zip_file) as zf:
+                 self.assertRaises(RuntimeError, zf.extract, 'a.txt')
+ 
++    @requires_zlib()
++    def test_full_overlap(self):
++        data = (
++            b'PK\x03\x04\x14\x00\x00\x00\x08\x00\xa0lH\x05\xe2\x1e'
++            b'8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00\x00\x00a\xed'
++            b'\xc0\x81\x08\x00\x00\x00\xc00\xd6\xfbK\\d\x0b`P'
++            b'K\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0lH\x05\xe2'
++            b'\x1e8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00\x00\x00\x00'
++            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00aPK'
++            b'\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0lH\x05\xe2\x1e'
++            b'8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00\x00\x00\x00\x00'
++            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00bPK\x05'
++            b'\x06\x00\x00\x00\x00\x02\x00\x02\x00^\x00\x00\x00/\x00\x00'
++            b'\x00\x00\x00'
++        )
++        with zipfile.ZipFile(io.BytesIO(data), 'r') as zipf:
++            self.assertEqual(zipf.namelist(), ['a', 'b'])
++            zi = zipf.getinfo('a')
++            self.assertEqual(zi.header_offset, 0)
++            self.assertEqual(zi.compress_size, 16)
++            self.assertEqual(zi.file_size, 1033)
++            zi = zipf.getinfo('b')
++            self.assertEqual(zi.header_offset, 0)
++            self.assertEqual(zi.compress_size, 16)
++            self.assertEqual(zi.file_size, 1033)
++            self.assertEqual(len(zipf.read('a')), 1033)
++            with self.assertRaisesRegex(zipfile.BadZipFile, 'File 
name.*differ'):
++                zipf.read('b')
++
++    @requires_zlib()
++    def test_quoted_overlap(self):
++        data = (
++            b'PK\x03\x04\x14\x00\x00\x00\x08\x00\xa0lH\x05Y\xfc'
++            b'8\x044\x00\x00\x00(\x04\x00\x00\x01\x00\x00\x00a\x00'
++            b'\x1f\x00\xe0\xffPK\x03\x04\x14\x00\x00\x00\x08\x00\xa0l'
++            b'H\x05\xe2\x1e8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00'
++            b'\x00\x00b\xed\xc0\x81\x08\x00\x00\x00\xc00\xd6\xfbK\\'
++            b'd\x0b`PK\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0'
++            b'lH\x05Y\xfc8\x044\x00\x00\x00(\x04\x00\x00\x01'
++            
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
++            b'\x00aPK\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0l'
++            b'H\x05\xe2\x1e8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00'
++            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x00\x00'
++            b'bPK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00^\x00\x00'
++            b'\x00S\x00\x00\x00\x00\x00'
++        )
++        with zipfile.ZipFile(io.BytesIO(data), 'r') as zipf:
++            self.assertEqual(zipf.namelist(), ['a', 'b'])
++            zi = zipf.getinfo('a')
++            self.assertEqual(zi.header_offset, 0)
++            self.assertEqual(zi.compress_size, 52)
++            self.assertEqual(zi.file_size, 1064)
++            zi = zipf.getinfo('b')
++            self.assertEqual(zi.header_offset, 36)
++            self.assertEqual(zi.compress_size, 16)
++            self.assertEqual(zi.file_size, 1033)
++            with self.assertRaisesRegex(zipfile.BadZipFile, 'Overlapped 
entries'):
++                zipf.read('a')
++            self.assertEqual(len(zipf.read('b')), 1033)
++
+     def tearDown(self):
+         unlink(TESTFN)
+         unlink(TESTFN2)
+diff --git a/Lib/zipfile.py b/Lib/zipfile.py
+index 6189db5e3e..058d7163ea 100644
+--- a/Lib/zipfile.py
++++ b/Lib/zipfile.py
+@@ -367,6 +367,7 @@ class ZipInfo (object):
+         'compress_size',
+         'file_size',
+         '_raw_time',
++        '_end_offset',
+     )
+ 
+     def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
+@@ -408,6 +409,7 @@ def __init__(self, filename="NoName", 
date_time=(1980,1,1,0,0,0)):
+         self.external_attr = 0          # External file attributes
+         self.compress_size = 0          # Size of the compressed file
+         self.file_size = 0              # Size of the uncompressed file
++        self._end_offset = None         # Start of the next local header or 
central directory
+         # Other attributes are set by class ZipFile:
+         # header_offset         Byte offset to the file header
+         # CRC                   CRC-32 of the uncompressed file
+@@ -1437,6 +1439,12 @@ def _RealGetContents(self):
+             if self.debug > 2:
+                 print("total", total)
+ 
++        end_offset = self.start_dir
++        for zinfo in sorted(self.filelist,
++                            key=lambda zinfo: zinfo.header_offset,
++                            reverse=True):
++            zinfo._end_offset = end_offset
++            end_offset = zinfo.header_offset
+ 
+     def namelist(self):
+         """Return a list of file names in the archive."""
+@@ -1590,6 +1598,10 @@ def open(self, name, mode="r", pwd=None, *, 
force_zip64=False):
+                     'File name in directory %r and header %r differ.'
+                     % (zinfo.orig_filename, fname))
+ 
++            if (zinfo._end_offset is not None and
++                zef_file.tell() + zinfo.compress_size > zinfo._end_offset):
++                raise BadZipFile(f"Overlapped entries: 
{zinfo.orig_filename!r} (possible zip bomb)")
++
+             # check for encrypted flag & handle password
+             is_encrypted = zinfo.flag_bits & _MASK_ENCRYPTED
+             if is_encrypted:
+diff --git 
a/Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst 
b/Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst
+new file mode 100644
+index 0000000000..be279caffc
+--- /dev/null
++++ b/Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst
+@@ -0,0 +1,3 @@
++Protect :mod:`zipfile` from "quoted-overlap" zipbomb. It now raises
++BadZipFile when try to read an entry that overlaps with other entry or
++central directory.
diff -Nru python3.11-3.11.2/debian/patches/series 
python3.11-3.11.2/debian/patches/series
--- python3.11-3.11.2/debian/patches/series     2024-03-02 20:28:50.000000000 
+0000
+++ python3.11-3.11.2/debian/patches/series     2024-04-26 16:10:48.000000000 
+0100
@@ -40,3 +40,5 @@
 ntpath-import.diff
 shutdown-deadlock.diff
 frame_dealloc-crash.diff
+CVE-2024-0450.patch
+CVE-2023-6597.patch

Reply via email to