Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:python3.11
User: [email protected]
Usertags: pu

Dear Release team,

this is a follow-up update to the +deb12u7 update already accepted in
bookwork-proposed-updates a few weeks ago. It fixes low severity CVEs,
which have been backported to the upstream 3.11 branch for the most
part. It will bring the bookworm package in-line with the trixie package
+deb13u2 (currently in trixie-p-u) in terms of CVE fixes.

The debdiff also includes a patch to fix the autopkgtest.

[ CVE details ]

The following patches:

* CVE-2025-13462
* CVE-2026-2297
* CVE-2026-4224
* CVE-2026-4519
* CVE-2026-6100

have been cherry-picked from the upstream 3.11 branch.

CVE-2026-3644 is not yet merged upstream, but it will be included in the
next 3.11 point release, according to:
https://github.com/python/cpython/pull/146026#issuecomment-4418137974

CVE-2026-6019 wasn't merged upstream because the automatic backport
failed, and upstream didn't bother (very low severity), however it turns
out that automatic backport will pass after CVE-2026-3644 is applied, so
it's possible that it will also be backported upstream, cf.
https://github.com/python/cpython/pull/148848#issuecomment-4299364914
(and other comments below).

For this reason I believe it's Ok to be a bit ahead of upstream for
these 2 CVE fixes. Note that they both come with unit tests.

[ Autopkgtest fix ]

The debdiff also contains a fix so that the autopkgtest succeeds again.
In short, autopkgtest has been failing for almost a year, since the
upload of expat/2.5.0-1+deb12u2. It makes it difficult to detect
regressions.

I have found a long discussion upstream on the matter, and from my
reading, upstream recommends downstream who patched expat to skip the
failing tests. This is what I did here. I elaborated a bit more in the
message of the patch itself.

[ Other info ]

The debusine workflow looks good:
https://debusine.debian.net/debian/developers/work-request/688225/

I checked the failures in reverse autopkgtests and they are unrelated.

Git commits are available at:
https://salsa.debian.org/arnaudr/python3.11/-/tree/wip/deb12u8?ref_type=heads

IMPORTANT! The debdiff attached is relative to the version
3.11.2-6+deb12u7 that is already in bookworm-p-u.

Thanks,

Arnaud
diff -Nru python3.11-3.11.2/debian/changelog python3.11-3.11.2/debian/changelog
--- python3.11-3.11.2/debian/changelog  2026-04-08 08:58:00.000000000 +0700
+++ python3.11-3.11.2/debian/changelog  2026-05-12 12:17:27.000000000 +0700
@@ -1,3 +1,25 @@
+python3.11 (3.11.2-6+deb12u8) bookworm; urgency=medium
+
+  * Non-maintainer upload.
+  * Apply upstream patches for the following CVEs:
+    - CVE-2025-13462: Incorrect parsing of TarInfo header when GNU long name
+      and type AREGTYPE are combined
+    - CVE-2026-2297: SourcelessFileLoader does not use io.open_code()
+    - CVE-2026-3644: Reject control characters in more places in
+      http.cookies.Morsel (follow-up of patch for CVE-2026-0672)
+    - CVE-2026-4224: pyexpat.c: Unbounded C recursion in conv_content_model
+      causes crash
+    - CVE-2026-4519: Reject leading dashes in webbrowser.open()
+    - CVE-2026-6019: SimpleCookie.js_output is vulnerable to HTML injection
+      (Closes: #1135116)
+    - CVE-2026-6100: Possible UAF in {LZMA,BZ2}Decompressor
+  * Add patch to skip some failing XML tests. Failure is due to the fact that
+    we build / tests against expat/2.5.0-1+deb12u2, which was patched for
+    CVE-2023-52425, and that broke some tests. See the patch itself for more
+    details.
+
+ -- Arnaud Rebillout <[email protected]>  Tue, 12 May 2026 12:17:27 +0700
+
 python3.11 (3.11.2-6+deb12u7) bookworm; urgency=medium
 
   * Non-maintainer upload.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-13462.patch 
python3.11-3.11.2/debian/patches/CVE-2025-13462.patch
--- python3.11-3.11.2/debian/patches/CVE-2025-13462.patch       1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2025-13462.patch       2026-05-12 
12:03:12.000000000 +0700
@@ -0,0 +1,140 @@
+From 9a23b753552afa28e3a2f4d8863572fc66479406 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Thu, 30 Apr 2026 23:18:47 +0200
+Subject: [PATCH] [3.11] gh-141707: Skip TarInfo DIRTYPE normalization during
+ GNU long name handling (#145815)
+
+gh-141707: Skip TarInfo DIRTYPE normalization during GNU long name handling
+(cherry picked from commit 42d754e34c06e57ad6b8e7f92f32af679912d8ab)
+
+Co-authored-by: Seth Michael Larson <[email protected]>
+Co-authored-by: Eashwar Ranganathan <[email protected]>
+Origin: upstream, 
https://github.com/python/cpython/commit/9a23b753552afa28e3a2f4d8863572fc66479406
+---
+ Lib/tarfile.py                                | 29 ++++++++++++++++---
+ Lib/test/test_tarfile.py                      | 19 ++++++++++++
+ Misc/ACKS                                     |  1 +
+ ...-11-18-06-35-53.gh-issue-141707.DBmQIy.rst |  2 ++
+ 4 files changed, 47 insertions(+), 4 deletions(-)
+ create mode 100644 
Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst
+
+diff --git a/Lib/tarfile.py b/Lib/tarfile.py
+index c04c576ea22d2d..e2d9f9e6c61b31 100755
+--- a/Lib/tarfile.py
++++ b/Lib/tarfile.py
+@@ -1058,6 +1058,20 @@ def _create_pax_generic_header(cls, pax_headers, type, 
encoding):
+     @classmethod
+     def frombuf(cls, buf, encoding, errors):
+         """Construct a TarInfo object from a 512 byte bytes object.
++
++        To support the old v7 tar format AREGTYPE headers are
++        transformed to DIRTYPE headers if their name ends in '/'.
++        """
++        return cls._frombuf(buf, encoding, errors)
++
++    @classmethod
++    def _frombuf(cls, buf, encoding, errors, *, dircheck=True):
++        """Construct a TarInfo object from a 512 byte bytes object.
++
++        If ``dircheck`` is set to ``True`` then ``AREGTYPE`` headers will
++        be normalized to ``DIRTYPE`` if the name ends in a trailing slash.
++        ``dircheck`` must be set to ``False`` if this function is called
++        on a follow-up header such as ``GNUTYPE_LONGNAME``.
+         """
+         if len(buf) == 0:
+             raise EmptyHeaderError("empty header")
+@@ -1088,7 +1102,7 @@ def frombuf(cls, buf, encoding, errors):
+ 
+         # Old V7 tar format represents a directory as a regular
+         # file with a trailing slash.
+-        if obj.type == AREGTYPE and obj.name.endswith("/"):
++        if dircheck and obj.type == AREGTYPE and obj.name.endswith("/"):
+             obj.type = DIRTYPE
+ 
+         # The old GNU sparse format occupies some of the unused
+@@ -1123,8 +1137,15 @@ def fromtarfile(cls, tarfile):
+         """Return the next TarInfo object from TarFile object
+            tarfile.
+         """
++        return cls._fromtarfile(tarfile)
++
++    @classmethod
++    def _fromtarfile(cls, tarfile, *, dircheck=True):
++        """
++        See dircheck documentation in _frombuf().
++        """
+         buf = tarfile.fileobj.read(BLOCKSIZE)
+-        obj = cls.frombuf(buf, tarfile.encoding, tarfile.errors)
++        obj = cls._frombuf(buf, tarfile.encoding, tarfile.errors, 
dircheck=dircheck)
+         obj.offset = tarfile.fileobj.tell() - BLOCKSIZE
+         return obj._proc_member(tarfile)
+ 
+@@ -1182,7 +1203,7 @@ def _proc_gnulong(self, tarfile):
+ 
+         # Fetch the next header and process it.
+         try:
+-            next = self.fromtarfile(tarfile)
++            next = self._fromtarfile(tarfile, dircheck=False)
+         except HeaderError as e:
+             raise SubsequentHeaderError(str(e)) from None
+ 
+@@ -1317,7 +1338,7 @@ def _proc_pax(self, tarfile):
+ 
+         # Fetch the next header.
+         try:
+-            next = self.fromtarfile(tarfile)
++            next = self._fromtarfile(tarfile, dircheck=False)
+         except HeaderError as e:
+             raise SubsequentHeaderError(str(e)) from None
+ 
+diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
+index 366aac781df1e7..11066c005629c2 100644
+--- a/Lib/test/test_tarfile.py
++++ b/Lib/test/test_tarfile.py
+@@ -1054,6 +1054,25 @@ def test_longname_directory(self):
+                 self.assertIsNotNone(tar.getmember(longdir))
+                 self.assertIsNotNone(tar.getmember(longdir.removesuffix('/')))
+ 
++    def test_longname_file_not_directory(self):
++        # Test reading a longname file and ensure it is not handled as a 
directory
++        # Issue #141707
++        buf = io.BytesIO()
++        with tarfile.open(mode='w', fileobj=buf, format=self.format) as tar:
++            ti = tarfile.TarInfo()
++            ti.type = tarfile.AREGTYPE
++            ti.name = ('a' * 99) + '/' + ('b' * 3)
++            tar.addfile(ti)
++
++            expected = {t.name: t.type for t in tar.getmembers()}
++
++        buf.seek(0)
++        with tarfile.open(mode='r', fileobj=buf) as tar:
++            actual = {t.name: t.type for t in tar.getmembers()}
++
++        self.assertEqual(expected, actual)
++
++
+ class GNUReadTest(LongnameTest, ReadTest, unittest.TestCase):
+ 
+     subdir = "gnu"
+diff --git a/Misc/ACKS b/Misc/ACKS
+index 89474408a6bbd4..1c0f5d7f782fd3 100644
+--- a/Misc/ACKS
++++ b/Misc/ACKS
+@@ -1448,6 +1448,7 @@ Dhushyanth Ramasamy
+ Ashwin Ramaswami
+ Jeff Ramnani
+ Bayard Randel
++Eashwar Ranganathan
+ Varpu Rantala
+ Brodie Rao
+ Rémi Rampin
+diff --git 
a/Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst 
b/Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst
+new file mode 100644
+index 00000000000000..1f5b8ed90b8a90
+--- /dev/null
++++ b/Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst
+@@ -0,0 +1,2 @@
++Don't change :class:`tarfile.TarInfo` type from ``AREGTYPE`` to ``DIRTYPE`` 
when parsing
++GNU long name or link headers.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-2297.patch 
python3.11-3.11.2/debian/patches/CVE-2026-2297.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-2297.patch        1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-2297.patch        2026-05-12 
12:03:12.000000000 +0700
@@ -0,0 +1,45 @@
+From 69ddd9bb2cc4bd69b1565647c18659c6a789ccd9 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Thu, 30 Apr 2026 23:18:42 +0200
+Subject: [PATCH] [3.11] gh-145506: Fixes CVE-2026-2297 by ensuring
+ SourcelessFileLoader uses io.open_code (GH-145507) (#145515)
+
+* gh-145506: Fixes CVE-2026-2297 by ensuring SourcelessFileLoader uses 
io.open_code (GH-145507)
+(cherry picked from commit a51b1b512de1d56b3714b65628a2eae2b07e535e)
+
+Co-authored-by: Steve Dower <[email protected]>
+
+* Fix docs reference
+
+---------
+
+Co-authored-by: Steve Dower <[email protected]>
+Origin: upstream, 
https://github.com/python/cpython/commit/69ddd9bb2cc4bd69b1565647c18659c6a789ccd9
+---
+ Lib/importlib/_bootstrap_external.py                            | 2 +-
+ .../Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst     | 2 ++
+ 2 files changed, 3 insertions(+), 1 deletion(-)
+ create mode 100644 
Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst
+
+diff --git a/Lib/importlib/_bootstrap_external.py 
b/Lib/importlib/_bootstrap_external.py
+index e53f6acf38fc64..588da3c7ad1517 100644
+--- a/Lib/importlib/_bootstrap_external.py
++++ b/Lib/importlib/_bootstrap_external.py
+@@ -1126,7 +1126,7 @@ def get_filename(self, fullname):
+ 
+     def get_data(self, path):
+         """Return the data from path as raw bytes."""
+-        if isinstance(self, (SourceLoader, ExtensionFileLoader)):
++        if isinstance(self, (SourceLoader, SourcelessFileLoader, 
ExtensionFileLoader)):
+             with _io.open_code(str(path)) as file:
+                 return file.read()
+         else:
+diff --git 
a/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst 
b/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst
+new file mode 100644
+index 00000000000000..edeb9e640c2732
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst
+@@ -0,0 +1,2 @@
++Fixes CVE-2026-2297 by ensuring that ``SourcelessFileLoader`` uses
++:func:`io.open_code` when opening ``.pyc`` files.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-3644.patch 
python3.11-3.11.2/debian/patches/CVE-2026-3644.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-3644.patch        1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-3644.patch        2026-05-12 
12:03:43.000000000 +0700
@@ -0,0 +1,150 @@
+From 37b75af7264fcffb95135618bef0937dd0ae61b8 Mon Sep 17 00:00:00 2001
+From: Stan Ulbrych <[email protected]>
+Date: Mon, 16 Mar 2026 13:43:43 +0000
+Subject: [PATCH] gh-145599, CVE 2026-3644: Reject control characters in
+ `http.cookies.Morsel.update()` (GH-145600)
+
+Reject control characters in `http.cookies.Morsel.update()` and 
`http.cookies.BaseCookie.js_output`.
+(cherry picked from commit 57e88c1cf95e1481b94ae57abe1010469d47a6b4)
+
+Co-authored-by: Stan Ulbrych 
<[email protected]>
+Co-authored-by: Victor Stinner <[email protected]>
+Co-authored-by: Victor Stinner <[email protected]>
+---
+ Lib/http/cookies.py                           | 24 ++++++++++--
+ Lib/test/test_http_cookies.py                 | 38 +++++++++++++++++++
+ ...-03-06-17-03-38.gh-issue-145599.kchwZV.rst |  4 ++
+ 3 files changed, 62 insertions(+), 4 deletions(-)
+ create mode 100644 
Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst
+
+diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
+index 5cfa7a8072c7f7..6b36ffa9f89bb1 100644
+--- a/Lib/http/cookies.py
++++ b/Lib/http/cookies.py
+@@ -335,9 +335,16 @@ def update(self, values):
+             key = key.lower()
+             if key not in self._reserved:
+                 raise CookieError("Invalid attribute %r" % (key,))
++            if _has_control_character(key, val):
++                raise CookieError("Control characters are not allowed in "
++                                  f"cookies {key!r} {val!r}")
+             data[key] = val
+         dict.update(self, data)
+ 
++    def __ior__(self, values):
++        self.update(values)
++        return self
++
+     def isReservedKey(self, K):
+         return K.lower() in self._reserved
+ 
+@@ -363,9 +370,15 @@ def __getstate__(self):
+         }
+ 
+     def __setstate__(self, state):
+-        self._key = state['key']
+-        self._value = state['value']
+-        self._coded_value = state['coded_value']
++        key = state['key']
++        value = state['value']
++        coded_value = state['coded_value']
++        if _has_control_character(key, value, coded_value):
++            raise CookieError("Control characters are not allowed in cookies "
++                              f"{key!r} {value!r} {coded_value!r}")
++        self._key = key
++        self._value = value
++        self._coded_value = coded_value
+ 
+     def output(self, attrs=None, header="Set-Cookie:"):
+         return "%s %s" % (header, self.OutputString(attrs))
+@@ -377,13 +390,16 @@ def __repr__(self):
+ 
+     def js_output(self, attrs=None):
+         # Print javascript
++        output_string = self.OutputString(attrs)
++        if _has_control_character(output_string):
++            raise CookieError("Control characters are not allowed in cookies")
+         return """
+         <script type="text/javascript">
+         <!-- begin hiding
+         document.cookie = \"%s\";
+         // end hiding -->
+         </script>
+-        """ % (self.OutputString(attrs).replace('"', r'\"'))
++        """ % (output_string.replace('"', r'\"'))
+ 
+     def OutputString(self, attrs=None):
+         # Build up our result
+diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
+index 2438c57ef40458..f9a846f8faa91b 100644
+--- a/Lib/test/test_http_cookies.py
++++ b/Lib/test/test_http_cookies.py
+@@ -527,6 +527,14 @@ def test_control_characters(self):
+             with self.assertRaises(cookies.CookieError):
+                 morsel["path"] = c0
+ 
++            # .__setstate__()
++            with self.assertRaises(cookies.CookieError):
++                morsel.__setstate__({'key': c0, 'value': 'val', 
'coded_value': 'coded'})
++            with self.assertRaises(cookies.CookieError):
++                morsel.__setstate__({'key': 'key', 'value': c0, 
'coded_value': 'coded'})
++            with self.assertRaises(cookies.CookieError):
++                morsel.__setstate__({'key': 'key', 'value': 'val', 
'coded_value': c0})
++
+             # .setdefault()
+             with self.assertRaises(cookies.CookieError):
+                 morsel.setdefault("path", c0)
+@@ -541,6 +549,18 @@ def test_control_characters(self):
+             with self.assertRaises(cookies.CookieError):
+                 morsel.set("path", "val", c0)
+ 
++            # .update()
++            with self.assertRaises(cookies.CookieError):
++                morsel.update({"path": c0})
++            with self.assertRaises(cookies.CookieError):
++                morsel.update({c0: "val"})
++
++            # .__ior__()
++            with self.assertRaises(cookies.CookieError):
++                morsel |= {"path": c0}
++            with self.assertRaises(cookies.CookieError):
++                morsel |= {c0: "val"}
++
+     def test_control_characters_output(self):
+         # Tests that even if the internals of Morsel are modified
+         # that a call to .output() has control character safeguards.
+@@ -561,6 +581,24 @@ def test_control_characters_output(self):
+             with self.assertRaises(cookies.CookieError):
+                 cookie.output()
+ 
++        # Tests that .js_output() also has control character safeguards.
++        for c0 in support.control_characters_c0():
++            morsel = cookies.Morsel()
++            morsel.set("key", "value", "coded-value")
++            morsel._key = c0  # Override private variable.
++            cookie = cookies.SimpleCookie()
++            cookie["cookie"] = morsel
++            with self.assertRaises(cookies.CookieError):
++                cookie.js_output()
++
++            morsel = cookies.Morsel()
++            morsel.set("key", "value", "coded-value")
++            morsel._coded_value = c0  # Override private variable.
++            cookie = cookies.SimpleCookie()
++            cookie["cookie"] = morsel
++            with self.assertRaises(cookies.CookieError):
++                cookie.js_output()
++
+ 
+ def load_tests(loader, tests, pattern):
+     tests.addTest(doctest.DocTestSuite(cookies))
+diff --git 
a/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst 
b/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst
+new file mode 100644
+index 00000000000000..e53a932d12fcdc
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst
+@@ -0,0 +1,4 @@
++Reject control characters in :class:`http.cookies.Morsel`
++:meth:`~http.cookies.Morsel.update` and
++:meth:`~http.cookies.BaseCookie.js_output`.
++This addresses :cve:`2026-3644`.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-4224.patch 
python3.11-3.11.2/debian/patches/CVE-2026-4224.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-4224.patch        1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-4224.patch        2026-05-12 
12:03:12.000000000 +0700
@@ -0,0 +1,121 @@
+From 642865ddf4b232da1f3b1f7abcfa3254c4bfe785 Mon Sep 17 00:00:00 2001
+From: Stan Ulbrych <[email protected]>
+Date: Wed, 8 Apr 2026 11:27:39 +0100
+Subject: [PATCH] [3.11] gh-145986: Avoid unbound C recursion in
+ `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987) (#146000)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+* [3.11] gh-145986: Avoid unbound C recursion in `conv_content_model` in 
`pyexpat.c` (CVE 2026-4224) (GH-145987)
+
+Fix C stack overflow (CVE-2026-4224) when an Expat parser
+with a registered `ElementDeclHandler` parses inline DTD
+containing deeply nested content model.
+
+---------
+(cherry picked from commit eb0e8be3a7e11b87d198a2c3af1ed0eccf532768)
+(cherry picked from commit e5caf45faac)
+
+Co-authored-by: Stan Ulbrych 
<[email protected]>
+Co-authored-by: Bénédikt Tran <[email protected]>
+
+* Update 
Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
+
+---------
+
+Co-authored-by: Bénédikt Tran <[email protected]>
+Origin: backport, 
https://github.com/python/cpython/commit/642865ddf4b232da1f3b1f7abcfa3254c4bfe785
+---
+ Lib/test/test_pyexpat.py                       | 18 ++++++++++++++++++
+ ...6-03-14-17-31-39.gh-issue-145986.ifSSr8.rst |  4 ++++
+ Modules/pyexpat.c                              |  9 ++++++++-
+ 3 files changed, 30 insertions(+), 1 deletion(-)
+ create mode 100644 
Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
+
+diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
+index 9aa2fcedadf637..8afce3ffe134f5 100644
+--- a/Lib/test/test_pyexpat.py
++++ b/Lib/test/test_pyexpat.py
+@@ -8,6 +8,7 @@ import sys
+ import sysconfig
+ import unittest
+ import traceback
++from test import support
+ 
+ from xml.parsers import expat
+ from xml.parsers.expat import errors
+@@ -647,6 +648,24 @@ def test_change_size_2(self):
+         parser.Parse(xml2, True)
+         self.assertEqual(self.n, 4)
+ 
++class ElementDeclHandlerTest(unittest.TestCase):
++    def test_deeply_nested_content_model(self):
++        # This should raise a RecursionError and not crash.
++        # See https://github.com/python/cpython/issues/145986.
++        N = 500_000
++        data = (
++            b'<!DOCTYPE root [\n<!ELEMENT root '
++            + b'(a, ' * N + b'a' + b')' * N
++            + b'>\n]>\n<root/>\n'
++        )
++
++        parser = expat.ParserCreate()
++        parser.ElementDeclHandler = lambda _1, _2: None
++        with support.infinite_recursion():
++            with self.assertRaises(RecursionError):
++                parser.Parse(data)
++
++
+ class MalformedInputTest(unittest.TestCase):
+     def test1(self):
+         xml = b"\0\r\n"
+diff --git 
a/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst 
b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
+new file mode 100644
+index 00000000000000..cb9dbadb72d976
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
+@@ -0,0 +1,4 @@
++:mod:`xml.parsers.expat`: Fixed a crash caused by unbounded C recursion when
++converting deeply nested XML content models with
++:meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`.
++This addresses `CVE-2026-4224 
<https://www.cve.org/CVERecord?id=CVE-2026-4224>`_.
+diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c
+index 7b76ddfabd9476..13c4e8e0adcb5c 100644
+--- a/Modules/pyexpat.c
++++ b/Modules/pyexpat.c
+@@ -1,4 +1,5 @@
+ #include "Python.h"
++#include "pycore_ceval.h"           // _Py_EnterRecursiveCall()
+ #include <ctype.h>
+ 
+ #include "structmember.h"         // PyMemberDef
+@@ -517,6 +518,10 @@ static PyObject *
+ conv_content_model(XML_Content * const model,
+                    PyObject *(*conv_string)(const XML_Char *))
+ {
++    if (_Py_EnterRecursiveCall(" in conv_content_model")) {
++        return NULL;
++    }
++
+     PyObject *result = NULL;
+     PyObject *children = PyTuple_New(model->numchildren);
+     int i;
+@@ -528,7 +533,7 @@ conv_content_model(XML_Content * const model,
+                                                  conv_string);
+             if (child == NULL) {
+                 Py_XDECREF(children);
+-                return NULL;
++                goto done;
+             }
+             PyTuple_SET_ITEM(children, i, child);
+         }
+@@ -536,6 +541,8 @@ conv_content_model(XML_Content * const model,
+                                model->type, model->quant,
+                                conv_string,model->name, children);
+     }
++done:
++    _Py_LeaveRecursiveCall();
+     return result;
+ }
+ 
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-4519-1.patch 
python3.11-3.11.2/debian/patches/CVE-2026-4519-1.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-4519-1.patch      1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-4519-1.patch      2026-05-12 
12:03:12.000000000 +0700
@@ -0,0 +1,121 @@
+From ceac1efc66516ac387eef2c9a0ce671895b44f03 Mon Sep 17 00:00:00 2001
+From: tomcruiseqi <[email protected]>
+Date: Wed, 25 Mar 2026 02:23:28 +0800
+Subject: [PATCH] [3.11] gh-143930: Reject leading dashes in webbrowser URLs
+ (GH-143931) (GH-146364)
+
+(cherry picked from commit 82a24a4442312bdcfc4c799885e8b3e00990f02b)
+
+Co-authored-by: Seth Michael Larson <[email protected]>
+Origin: backport, 
https://github.com/python/cpython/commit/ceac1efc66516ac387eef2c9a0ce671895b44f03
+---
+ Lib/test/test_webbrowser.py                        |  5 +++++
+ Lib/webbrowser.py                                  | 14 ++++++++++++++
+ .../2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst |  1 +
+ 3 files changed, 20 insertions(+)
+ create mode 100644 
Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst
+
+diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py
+index 9d608d63a01ed3..0ac985f56c840e 100644
+--- a/Lib/test/test_webbrowser.py
++++ b/Lib/test/test_webbrowser.py
+@@ -59,6 +59,11 @@ def test_open(self):
+                    options=[],
+                    arguments=[URL])
+ 
++    def test_reject_dash_prefixes(self):
++        browser = self.browser_class(name=CMD_NAME)
++        with self.assertRaises(ValueError):
++            browser.open(f"--key=val {URL}")
++
+ 
+ class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase):
+ 
+diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py
+index 5d72524c087677..0fd0aeb3c1b6ef 100755
+--- a/Lib/webbrowser.py
++++ b/Lib/webbrowser.py
+@@ -155,6 +155,12 @@ def open_new(self, url):
+     def open_new_tab(self, url):
+         return self.open(url, 2)
+ 
++    @staticmethod
++    def _check_url(url):
++        """Ensures that the URL is safe to pass to subprocesses as a 
parameter"""
++        if url and url.lstrip().startswith("-"):
++            raise ValueError(f"Invalid URL: {url}")
++
+ 
+ class GenericBrowser(BaseBrowser):
+     """Class for all browsers started with a command
+@@ -172,6 +178,7 @@ def __init__(self, name):
+ 
+     def open(self, url, new=0, autoraise=True):
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         cmdline = [self.name] + [arg.replace("%s", url)
+                                  for arg in self.args]
+         try:
+@@ -192,6 +199,7 @@ def open(self, url, new=0, autoraise=True):
+         cmdline = [self.name] + [arg.replace("%s", url)
+                                  for arg in self.args]
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         try:
+             if sys.platform[:3] == 'win':
+                 p = subprocess.Popen(cmdline)
+@@ -257,6 +265,7 @@ def _invoke(self, args, remote, autoraise, url=None):
+ 
+     def open(self, url, new=0, autoraise=True):
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         if new == 0:
+             action = self.remote_action
+         elif new == 1:
+@@ -358,6 +367,7 @@ class Konqueror(BaseBrowser):
+ 
+     def open(self, url, new=0, autoraise=True):
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         # XXX Currently I know no way to prevent KFM from opening a new win.
+         if new == 2:
+             action = "newTab"
+@@ -442,6 +452,7 @@ def _remote(self, action):
+ 
+     def open(self, url, new=0, autoraise=True):
+         sys.audit("webbrowser.open", url)
++        self._check_url(url)
+         if new:
+             ok = self._remote("LOADNEW " + url)
+         else:
+@@ -605,6 +616,7 @@ def register_standard_browsers():
+     class WindowsDefault(BaseBrowser):
+         def open(self, url, new=0, autoraise=True):
+             sys.audit("webbrowser.open", url)
++            self._check_url(url)
+             try:
+                 os.startfile(url)
+             except OSError:
+@@ -637,6 +649,7 @@ def __init__(self, name):
+ 
+         def open(self, url, new=0, autoraise=True):
+             sys.audit("webbrowser.open", url)
++            self._check_url(url)
+             assert "'" not in url
+             # hack for local urls
+             if not ':' in url:
+@@ -688,6 +701,7 @@ def _name(self, val):
+             self.name = val
+ 
+         def open(self, url, new=0, autoraise=True):
++            self._check_url(url)
+             if self.name == 'default':
+                 script = 'open location "%s"' % url.replace('"', '%22') # 
opens in default browser
+             else:
+diff --git 
a/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst 
b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst
+new file mode 100644
+index 00000000000000..0f27eae99a0dfd
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst
+@@ -0,0 +1 @@
++Reject leading dashes in URLs passed to :func:`webbrowser.open`
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-4519-2.patch 
python3.11-3.11.2/debian/patches/CVE-2026-4519-2.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-4519-2.patch      1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-4519-2.patch      2026-05-12 
12:03:12.000000000 +0700
@@ -0,0 +1,154 @@
+From 96fc5048605863c7b6fd6289643feb0e97edd96c Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Sat, 4 Apr 2026 00:53:49 +0200
+Subject: [PATCH] [3.11] gh-143930: Tweak the exception message and increase
+ test coverage (GH-146476) (GH-148045) (GH-148051) (GH-148052)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+(cherry picked from commit cc023511238ad93ecc8796157c6f9139a2bb2932)
+(cherry picked from commit 89bfb8e5ed3c7caa241028f1a4eac5f6275a46a4)
+(cherry picked from commit 3681d47a440865aead912a054d4599087b4270dd)
+
+Co-authored-by: Łukasz Langa <[email protected]>
+Origin: upstream, 
https://github.com/python/cpython/commit/96fc5048605863c7b6fd6289643feb0e97edd96c
+---
+ Lib/test/test_webbrowser.py                   | 81 +++++++++++++++++--
+ Lib/webbrowser.py                             |  2 +-
+ ...-01-16-12-04-49.gh-issue-143930.zYC5x3.rst |  2 +-
+ 3 files changed, 77 insertions(+), 8 deletions(-)
+
+diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py
+index 0ac985f56c840e..522219bc8c55b3 100644
+--- a/Lib/test/test_webbrowser.py
++++ b/Lib/test/test_webbrowser.py
+@@ -1,6 +1,7 @@
++import io
++import os
+ import webbrowser
+ import unittest
+-import os
+ import sys
+ import subprocess
+ from unittest import mock
+@@ -49,6 +50,14 @@ def _test(self, meth, *, args=[URL], kw={}, options, 
arguments):
+             popen_args.pop(popen_args.index(option))
+         self.assertEqual(popen_args, arguments)
+ 
++    def test_reject_dash_prefixes(self):
++        browser = self.browser_class(name=CMD_NAME)
++        with self.assertRaisesRegex(
++            ValueError,
++            r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$"
++        ):
++            browser.open(f"--key=val {URL}")
++
+ 
+ class GenericBrowserCommandTest(CommandTestMixin, unittest.TestCase):
+ 
+@@ -59,11 +68,6 @@ def test_open(self):
+                    options=[],
+                    arguments=[URL])
+ 
+-    def test_reject_dash_prefixes(self):
+-        browser = self.browser_class(name=CMD_NAME)
+-        with self.assertRaises(ValueError):
+-            browser.open(f"--key=val {URL}")
+-
+ 
+ class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase):
+ 
+@@ -224,6 +228,71 @@ def test_open_new_tab(self):
+                    arguments=['openURL({},new-tab)'.format(URL)])
+ 
+ 
++class MockPopenPipe:
++    def __init__(self, cmd, mode):
++        self.cmd = cmd
++        self.mode = mode
++        self.pipe = io.StringIO()
++        self._closed = False
++
++    def write(self, buf):
++        self.pipe.write(buf)
++
++    def close(self):
++        self._closed = True
++        return None
++
++
[email protected](sys.platform == "darwin", "macOS specific test")
++class MacOSXOSAScriptTest(unittest.TestCase):
++    def setUp(self):
++        # Ensure that 'BROWSER' is not set to 'open' or something else.
++        # See: https://github.com/python/cpython/issues/131254.
++        env = self.enterContext(os_helper.EnvironmentVarGuard())
++        env.unset("BROWSER")
++
++        support.patch(self, os, "popen", self.mock_popen)
++        self.browser = webbrowser.MacOSXOSAScript("default")
++
++    def mock_popen(self, cmd, mode):
++        self.popen_pipe = MockPopenPipe(cmd, mode)
++        return self.popen_pipe
++
++    def test_default(self):
++        browser = webbrowser.get()
++        assert isinstance(browser, webbrowser.MacOSXOSAScript)
++        self.assertEqual(browser.name, "default")
++
++    def test_default_open(self):
++        url = "https://python.org";
++        self.browser.open(url)
++        self.assertTrue(self.popen_pipe._closed)
++        self.assertEqual(self.popen_pipe.cmd, "osascript")
++        script = self.popen_pipe.pipe.getvalue()
++        self.assertEqual(script.strip(), f'open location "{url}"')
++
++    def test_url_quote(self):
++        self.browser.open('https://python.org/"quote";')
++        script = self.popen_pipe.pipe.getvalue()
++        self.assertEqual(
++            script.strip(), 'open location "https://python.org/%22quote%22";'
++        )
++
++    def test_explicit_browser(self):
++        browser = webbrowser.MacOSXOSAScript("safari")
++        browser.open("https://python.org";)
++        script = self.popen_pipe.pipe.getvalue()
++        self.assertIn('tell application "safari"', script)
++        self.assertIn('open location "https://python.org";', script)
++
++    def test_reject_dash_prefixes(self):
++        with self.assertRaisesRegex(
++            ValueError,
++            r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$"
++        ):
++            self.browser.open(f"--key=val {URL}")
++
++
+ class BrowserRegistrationTest(unittest.TestCase):
+ 
+     def setUp(self):
+diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py
+index 0fd0aeb3c1b6ef..52ad205bd1b881 100755
+--- a/Lib/webbrowser.py
++++ b/Lib/webbrowser.py
+@@ -159,7 +159,7 @@ def open_new_tab(self, url):
+     def _check_url(url):
+         """Ensures that the URL is safe to pass to subprocesses as a 
parameter"""
+         if url and url.lstrip().startswith("-"):
+-            raise ValueError(f"Invalid URL: {url}")
++            raise ValueError(f"Invalid URL (leading dash disallowed): 
{url!r}")
+ 
+ 
+ class GenericBrowser(BaseBrowser):
+diff --git 
a/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst 
b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst
+index 0f27eae99a0dfd..c561023c3c2d7a 100644
+--- a/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst
++++ b/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst
+@@ -1 +1 @@
+-Reject leading dashes in URLs passed to :func:`webbrowser.open`
++Reject leading dashes in URLs passed to :func:`webbrowser.open`.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-4519-3.patch 
python3.11-3.11.2/debian/patches/CVE-2026-4519-3.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-4519-3.patch      1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-4519-3.patch      2026-05-12 
12:03:12.000000000 +0700
@@ -0,0 +1,66 @@
+From f4654824ae0850ac87227fb270f9057477946769 Mon Sep 17 00:00:00 2001
+From: Stan Ulbrych <[email protected]>
+Date: Mon, 13 Apr 2026 22:41:51 +0100
+Subject: [PATCH] [3.11] gh-148169: Fix webbrowser `%action` substitution
+ bypass of dash-prefix check (GH-148170) (#148520)
+
+(cherry picked from commit d22922c8a7958353689dc4763dd72da2dea03fff)
+
+Origin: upstream, 
https://github.com/python/cpython/commit/f4654824ae0850ac87227fb270f9057477946769
+---
+ Lib/test/test_webbrowser.py                               | 8 ++++++++
+ Lib/webbrowser.py                                         | 5 +++--
+ .../2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst        | 2 ++
+ 3 files changed, 13 insertions(+), 2 deletions(-)
+ create mode 100644 
Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst
+
+diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py
+index 74eca81c707ead..f10f54e4d4ec32 100644
+--- a/Lib/test/test_webbrowser.py
++++ b/Lib/test/test_webbrowser.py
+@@ -103,6 +103,14 @@ def test_open_new_tab(self):
+                    options=[],
+                    arguments=[URL])
+ 
++    def test_reject_action_dash_prefixes(self):
++        browser = self.browser_class(name=CMD_NAME)
++        with self.assertRaises(ValueError):
++            browser.open('%action--incognito')
++        # new=1: action is "--new-window", so "%action" itself expands to
++        # a dash-prefixed flag even with no dash in the original URL.
++        with self.assertRaises(ValueError):
++            browser.open('%action', new=1)
+ 
+ class MozillaCommandTest(CommandTestMixin, unittest.TestCase):
+ 
+diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py
+index ce6ec9a27d7d8b..9819a4ecf6d43e 100755
+--- a/Lib/webbrowser.py
++++ b/Lib/webbrowser.py
+@@ -265,7 +265,6 @@ def _invoke(self, args, remote, autoraise, url=None):
+ 
+     def open(self, url, new=0, autoraise=True):
+         sys.audit("webbrowser.open", url)
+-        self._check_url(url)
+         if new == 0:
+             action = self.remote_action
+         elif new == 1:
+@@ -279,7 +278,9 @@ def open(self, url, new=0, autoraise=True):
+             raise Error("Bad 'new' parameter to open(); " +
+                         "expected 0, 1, or 2, got %s" % new)
+ 
+-        args = [arg.replace("%s", url).replace("%action", action)
++        self._check_url(url.replace("%action", action))
++
++        args = [arg.replace("%action", action).replace("%s", url)
+                 for arg in self.remote_args]
+         args = [arg for arg in args if arg]
+         success = self._invoke(args, True, autoraise, url)
+diff --git 
a/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst 
b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst
+new file mode 100644
+index 00000000000000..45cdeebe1b6d64
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst
+@@ -0,0 +1,2 @@
++A bypass in :mod:`webbrowser` allowed URLs prefixed with ``%action`` to pass
++the dash-prefix safety check.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-6019.patch 
python3.11-3.11.2/debian/patches/CVE-2026-6019.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-6019.patch        1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-6019.patch        2026-05-12 
12:03:43.000000000 +0700
@@ -0,0 +1,133 @@
+From 3c59b8b53fc75c7f9578d16fb8201ceb43e8f76c Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Thu, 23 Apr 2026 15:05:17 +0200
+Subject: [PATCH] [3.13] gh-90309: Base64-encode cookie values embedded in JS
+ (GH-148888)
+
+(cherry picked from commit 76b3923d688c0efc580658476c5f525ec8735104)
+
+Co-authored-by: Seth Larson <[email protected]>
+Origin: upstream, 
https://github.com/python/cpython/commit/3c59b8b53fc75c7f9578d16fb8201ceb43e8f76c
+---
+ Lib/http/cookies.py                           |  8 +++--
+ Lib/test/test_http_cookies.py                 | 29 ++++++++++++-------
+ ...6-04-21-13-46-30.gh-issue-90309.srvj9q.rst |  3 ++
+ 3 files changed, 27 insertions(+), 13 deletions(-)
+ create mode 100644 
Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst
+
+diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
+index 63d119ad46c084..aebc2a163e4487 100644
+--- a/Lib/http/cookies.py
++++ b/Lib/http/cookies.py
+@@ -389,17 +389,21 @@ def __repr__(self):
+         return '<%s: %s>' % (self.__class__.__name__, self.OutputString())
+ 
+     def js_output(self, attrs=None):
++        import base64
+         # Print javascript
+         output_string = self.OutputString(attrs)
+         if _has_control_character(output_string):
+             raise CookieError("Control characters are not allowed in cookies")
++        # Base64-encode value to avoid template
++        # injection in cookie values.
++        output_encoded = 
base64.b64encode(output_string.encode('utf-8')).decode("ascii")
+         return """
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = \"%s\";
++        document.cookie = atob(\"%s\");
+         // end hiding -->
+         </script>
+-        """ % (output_string.replace('"', r'\"'))
++        """ % (output_encoded,)
+ 
+     def OutputString(self, attrs=None):
+         # Build up our result
+diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
+index 7edb026324ebdc..88914123d51303 100644
+--- a/Lib/test/test_http_cookies.py
++++ b/Lib/test/test_http_cookies.py
+@@ -1,5 +1,5 @@
+ # Simple test suite for http/cookies.py
+-
++import base64
+ import copy
+ import unittest
+ import doctest
+@@ -106,17 +106,19 @@ def test_load(self):
+ 
+         self.assertEqual(C.output(['path']),
+             'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
+-        self.assertEqual(C.js_output(), r"""
++        cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; 
Path=/acme; Version=1').decode('ascii')
++        self.assertEqual(C.js_output(), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1";
++        document.cookie = atob("{cookie_encoded}");
+         // end hiding -->
+         </script>
+         """)
+-        self.assertEqual(C.js_output(['path']), r"""
++        cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; 
Path=/acme').decode('ascii')
++        self.assertEqual(C.js_output(['path']), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme";
++        document.cookie = atob("{cookie_encoded}");
+         // end hiding -->
+         </script>
+         """)
+@@ -213,17 +215,19 @@ def test_quoted_meta(self):
+ 
+         self.assertEqual(C.output(['path']),
+                          'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
+-        self.assertEqual(C.js_output(), r"""
++        expected_encoded_cookie = 
base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme; 
Version=1').decode('ascii')
++        self.assertEqual(C.js_output(), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1";
++        document.cookie = atob("{expected_encoded_cookie}");
+         // end hiding -->
+         </script>
+         """)
+-        self.assertEqual(C.js_output(['path']), r"""
++        expected_encoded_cookie = 
base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme').decode('ascii')
++        self.assertEqual(C.js_output(['path']), fr"""
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "Customer=\"WILE_E_COYOTE\"; Path=/acme";
++        document.cookie = atob("{expected_encoded_cookie}");
+         // end hiding -->
+         </script>
+         """)
+@@ -314,13 +318,16 @@ def test_setter(self):
+             self.assertEqual(
+                 M.output(),
+                 "Set-Cookie: %s=%s; Path=/foo" % (i, "%s_coded_val" % i))
++            expected_encoded_cookie = base64.b64encode(
++                ("%s=%s; Path=/foo" % (i, "%s_coded_val" % i)).encode("ascii")
++            ).decode('ascii')
+             expected_js_output = """
+         <script type="text/javascript">
+         <!-- begin hiding
+-        document.cookie = "%s=%s; Path=/foo";
++        document.cookie = atob("%s");
+         // end hiding -->
+         </script>
+-        """ % (i, "%s_coded_val" % i)
++        """ % (expected_encoded_cookie,)
+             self.assertEqual(M.js_output(), expected_js_output)
+         for i in ["foo bar", "foo@bar"]:
+             # Try some illegal characters
+diff --git 
a/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst 
b/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst
+new file mode 100644
+index 00000000000000..d7d376737e4ad1
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst
+@@ -0,0 +1,3 @@
++Base64-encode values when embedding cookies to JavaScript using the
++:meth:`http.cookies.BaseCookie.js_output` method to avoid injection
++and escaping.
diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-6100.patch 
python3.11-3.11.2/debian/patches/CVE-2026-6100.patch
--- python3.11-3.11.2/debian/patches/CVE-2026-6100.patch        1970-01-01 
08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/CVE-2026-6100.patch        2026-05-12 
12:17:27.000000000 +0700
@@ -0,0 +1,53 @@
+From e20c6c9667c99ecaab96e1a2b3767082841ffc8b Mon Sep 17 00:00:00 2001
+From: Stan Ulbrych <[email protected]>
+Date: Mon, 13 Apr 2026 22:42:36 +0100
+Subject: [PATCH] [3.11] gh-148395: Fix a possible UAF in
+ `{LZMA,BZ2}Decompressor` (GH-148396) (#148504)
+
+Fix dangling input pointer after `MemoryError` in 
_lzma/_bz2/_ZlibDecompressor.decompress
+
+(cherry picked from commit 8fc66aef6d7b3ae58f43f5c66f9366cc8cbbfcd2)
+
+Origin: upstream, 
https://github.com/python/cpython/commit/e20c6c9667c99ecaab96e1a2b3767082841ffc8b
+---
+ .../Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst  | 5 +++++
+ Modules/_bz2module.c                                         | 1 +
+ Modules/_lzmamodule.c                                        | 1 +
+ 3 files changed, 7 insertions(+)
+ create mode 100644 
Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst
+
+diff --git 
a/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst 
b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst
+new file mode 100644
+index 00000000000000..349d1cf3cacdf4
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst
+@@ -0,0 +1,5 @@
++Fix a dangling input pointer in :class:`lzma.LZMADecompressor`,
++and :class:`bz2.BZ2Decompressor`
++when memory allocation fails with :exc:`MemoryError`, which could let a
++subsequent :meth:`!decompress` call read or write through a stale pointer to
++the already-released caller buffer.
+diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c
+index 798e9efc628f05..b08ac5e44e52f4 100644
+--- a/Modules/_bz2module.c
++++ b/Modules/_bz2module.c
+@@ -595,6 +595,7 @@ decompress(BZ2Decompressor *d, char *data, size_t len, 
Py_ssize_t max_length)
+     return result;
+ 
+ error:
++    bzs->next_in = NULL;
+     Py_XDECREF(result);
+     return NULL;
+ }
+diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c
+index 97453a28088131..51106a6a075b30 100644
+--- a/Modules/_lzmamodule.c
++++ b/Modules/_lzmamodule.c
+@@ -1103,6 +1103,7 @@ decompress(Decompressor *d, uint8_t *data, size_t len, 
Py_ssize_t max_length)
+     return result;
+ 
+ error:
++    lzs->next_in = NULL;
+     Py_XDECREF(result);
+     return NULL;
+ }
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     2026-04-07 17:42:43.000000000 
+0700
+++ python3.11-3.11.2/debian/patches/series     2026-05-12 12:17:27.000000000 
+0700
@@ -72,3 +72,13 @@
 CVE-2025-15282.patch
 CVE-2025-11468.patch
 CVE-2026-1299.patch
+CVE-2026-4224.patch
+CVE-2026-4519-1.patch
+CVE-2026-4519-2.patch
+CVE-2026-4519-3.patch
+CVE-2026-3644.patch
+CVE-2026-6019.patch
+CVE-2026-6100.patch
+CVE-2026-2297.patch
+CVE-2025-13462.patch
+skip-xml-tests-expat-CVE-2023-52425.patch
diff -Nru 
python3.11-3.11.2/debian/patches/skip-xml-tests-expat-CVE-2023-52425.patch 
python3.11-3.11.2/debian/patches/skip-xml-tests-expat-CVE-2023-52425.patch
--- python3.11-3.11.2/debian/patches/skip-xml-tests-expat-CVE-2023-52425.patch  
1970-01-01 08:00:00.000000000 +0800
+++ python3.11-3.11.2/debian/patches/skip-xml-tests-expat-CVE-2023-52425.patch  
2026-05-12 12:17:27.000000000 +0700
@@ -0,0 +1,48 @@
+From: Arnaud Rebillout <[email protected]>
+Date: Mon, 11 May 2026 22:27:54 +0700
+Origin: vendor
+Forwarded: not-needed
+Subject: Skip xml tests that fail with expat patched for CVE-2023-52425
+
+These tests fail due to the fact that we build against a version of libexpat
+that is < 2.6, and patched for CVE-2023-52425. Upstream checks expat version
+and adjust unit tests accordingly, but assumes vanilla expat, and can't support
+patched expat.
+
+Upstream recommendation is to disable those tests downstream, cf.
+* https://github.com/python/cpython/issues/125067#issuecomment-2460866998
+* https://github.com/python/cpython/issues/125067#issuecomment-2464312388
+
+However it's worth reading the whole discussion. It's not 100% clear if the
+failing tests are just noise. Maybe by skipping those tests we hide a real 
issue.
+
+For Debian, at this point I believe we'd better skip the tests and have the
+autopkgtests back in the green: it is the best way to detect regressions going
+forward. The autopkgtests are in the red in ci.debian.net for almost a year
+(since the upload of expat/2.5.0-1+deb12u2), this is not helpful, and we're
+unlikely to catch any regression.
+
+One last reference: an example of downstream patch from SUSE, who patched these
+failing tests away:
+https://build.opensuse.org/projects/openSUSE:Leap:15.6:Update/packages/python311/files/CVE-2023-52425-libexpat-2.6.0-backport.patch?expand=1
+---
+--- a/Lib/test/test_xml_etree.py
++++ b/Lib/test/test_xml_etree.py
+@@ -13,6 +13,7 @@ import itertools
+ import operator
+ import os
+ import pickle
++import pyexpat
+ import sys
+ import textwrap
+ import types
+@@ -1419,6 +1419,9 @@ class XMLPullParserTest(unittest.TestCas
+     def test_simple_xml(self):
+         for chunk_size in (None, 1, 5):
+             with self.subTest(chunk_size=chunk_size):
++                if chunk_size in [1, 5]:
++                    self.skipTest(
++                        f"Fail with patched version of Expat 
{pyexpat.version_info}")
+                 parser = ET.XMLPullParser()
+                 self.assert_event_tags(parser, [])
+                 self._feed(parser, "<!-- comment -->\n", chunk_size)

Reply via email to