Version in base suite: 3.11.2-6+deb12u6 Base version: python3.11_3.11.2-6+deb12u6 Target version: python3.11_3.11.2-6+deb12u7 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python3.11/python3.11_3.11.2-6+deb12u6.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python3.11/python3.11_3.11.2-6+deb12u7.dsc changelog | 28 ++ patches/CVE-2025-11468.patch | 114 +++++++++ patches/CVE-2025-12084.patch | 170 +++++++++++++ patches/CVE-2025-13836.patch | 160 +++++++++++++ patches/CVE-2025-13837.patch | 160 +++++++++++++ patches/CVE-2025-15282.patch | 66 +++++ patches/CVE-2025-4516-1.patch | 156 ++++++++++++ patches/CVE-2025-4516-2.patch | 516 ++++++++++++++++++++++++++++++++++++++++++ patches/CVE-2025-6069.patch | 239 +++++++++++++++++++ patches/CVE-2025-6075.patch | 359 +++++++++++++++++++++++++++++ patches/CVE-2025-8194.patch | 218 +++++++++++++++++ patches/CVE-2025-8291.patch | 312 +++++++++++++++++++++++++ patches/CVE-2026-0672.patch | 190 +++++++++++++++ patches/CVE-2026-0865-1.patch | 98 +++++++ patches/CVE-2026-0865-2.patch | 149 ++++++++++++ patches/CVE-2026-1299.patch | 111 +++++++++ patches/series | 19 + salsa-ci.yml | 37 +++ 18 files changed, 3098 insertions(+), 4 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmply323y26/python3.11_3.11.2-6+deb12u6.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmply323y26/python3.11_3.11.2-6+deb12u7.dsc: no acceptable signature found diff -Nru python3.11-3.11.2/debian/changelog python3.11-3.11.2/debian/changelog --- python3.11-3.11.2/debian/changelog 2025-04-28 14:11:48.000000000 +0000 +++ python3.11-3.11.2/debian/changelog 2026-04-08 01:58:00.000000000 +0000 @@ -1,3 +1,31 @@ +python3.11 (3.11.2-6+deb12u7) bookworm; urgency=medium + + * Non-maintainer upload. + * Apply upstream patches for the following CVEs: + - CVE-2025-4516: issue in bytes.decode("unicode_escape", + error="ignore|replace") + - CVE-2025-6069: quadratic complexity in html.parser.HTMLParser + - CVE-2025-6075: performance degradation in os.path.expandvars() + - CVE-2025-8194: infinite loop and deadlock in tarfile + - CVE-2025-8291: incorrect ZIP64 End of Central Directory handling + - CVE-2025-11468: Folding email comments of unfoldable characters + didn't preserve parenthesis which could be abused. + - CVE-2025-12084: quadratic complexity in xml.dom.minidom appendChild etc + - CVE-2025-13836: OOM or other DoS due to incorrect Content-Length + handling in http.client + - CVE-2025-13837: OOM or other DoS due to incorrect data size handling + in plistlib + - CVE-2025-15282: User-controlled data URLs parsed by urllib allowed + injecting headers through newlines in the data URL mediatype. + - CVE-2026-0672: User-controlled cookie values and parameters could be + used to inject HTTP headers into messages. + - CVE-2026-0865: User-controlled header names and values containing + newlines could be used to inject HTTP headers. + - CVE-2026-1299: email module allowed header injection in the + BytesGenerator class. + + -- Arnaud Rebillout Wed, 08 Apr 2026 08:58:00 +0700 + python3.11 (3.11.2-6+deb12u6) bookworm; urgency=medium * Non-maintainer upload. diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-11468.patch python3.11-3.11.2/debian/patches/CVE-2025-11468.patch --- python3.11-3.11.2/debian/patches/CVE-2025-11468.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-11468.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,114 @@ +From e9970f077240c7c670e8a6fc6662f2b30d3b6ad0 Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Sun, 25 Jan 2026 18:10:38 +0100 +Subject: [PATCH] [3.11] gh-143935: Email preserve parens when folding comments + (GH-143936) (#144037) + +gh-143935: Email preserve parens when folding comments (GH-143936) + +Fix a bug in the folding of comments when flattening an email message +using a modern email policy. Comments consisting of a very long sequence of +non-foldable characters could trigger a forced line wrap that omitted the +required leading space on the continuation line, causing the remainder of +the comment to be interpreted as a new header field. This enabled header +injection with carefully crafted inputs. +(cherry picked from commit 17d1490aa97bd6b98a42b1a9b324ead84e7fd8a2) + +Co-authored-by: Seth Michael Larson +Co-authored-by: Denis Ledoux + +Origin: backport, https://github.com/python/cpython/commit/e9970f077240c7c670e8a6fc6662f2b30d3b6ad0 +--- + Lib/email/_header_value_parser.py | 15 +++++++++++- + .../test_email/test__header_value_parser.py | 23 +++++++++++++++++++ + ...-01-16-14-40-31.gh-issue-143935.U2YtKl.rst | 6 +++++ + 3 files changed, 43 insertions(+), 1 deletion(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst + +diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py +index 0183a1508b1..89950c825b6 100644 +--- a/Lib/email/_header_value_parser.py ++++ b/Lib/email/_header_value_parser.py +@@ -95,6 +95,12 @@ EXTENDED_ATTRIBUTE_ENDS = ATTRIBUTE_ENDS + NLSET = {'\n', '\r'} + SPECIALSNL = SPECIALS | NLSET + ++def make_parenthesis_pairs(value): ++ """Escape parenthesis and backslash for use within a comment.""" ++ return str(value).replace('\\', '\\\\') \ ++ .replace('(', '\\(').replace(')', '\\)') ++ ++ + def quote_string(value): + return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"' + +@@ -919,7 +925,7 @@ class WhiteSpaceTerminal(Terminal): + return ' ' + + def startswith_fws(self): +- return True ++ return self and self[0] in WSP + + + class ValueTerminal(Terminal): +@@ -2859,6 +2865,13 @@ def _refold_parse_tree(parse_tree, *, po + if not hasattr(part, 'encode'): + # It's not a terminal, try folding the subparts. + newparts = list(part) ++ if part.token_type == 'comment': ++ newparts = ( ++ [ValueTerminal('(', 'ptext')] + ++ [ValueTerminal(make_parenthesis_pairs(p), 'ptext') ++ if p.token_type == 'ptext' else p ++ for p in newparts] + ++ [ValueTerminal(')', 'ptext')]) + if not part.as_ew_allowed: + wrap_as_ew_blocked += 1 + newparts.append(end_ew_not_allowed) +diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py +index 6025b34ac4a..45ff73b5905 100644 +--- a/Lib/test/test_email/test__header_value_parser.py ++++ b/Lib/test/test_email/test__header_value_parser.py +@@ -2959,6 +2959,29 @@ class TestFolding(TestEmailBase): + ' , =?utf-8?q?H=C3=BCbsch?= Kaktus ' + '\n') + ++ def test_address_list_with_long_unwrapable_comment(self): ++ policy = self.policy.clone(max_line_length=40) ++ cases = [ ++ # (to, folded) ++ ('(loremipsumdolorsitametconsecteturadipi)', ++ '(loremipsumdolorsitametconsecteturadipi)\n'), ++ ('(loremipsumdolorsitametconsecteturadipi)', ++ '(loremipsumdolorsitametconsecteturadipi)\n'), ++ ('(loremipsum dolorsitametconsecteturadipi)', ++ '(loremipsum dolorsitametconsecteturadipi)\n'), ++ ('(loremipsum dolorsitametconsecteturadipi)', ++ '(loremipsum\n dolorsitametconsecteturadipi)\n'), ++ ('(Escaped \\( \\) chars \\\\ in comments stay escaped)', ++ '(Escaped \\( \\) chars \\\\ in comments stay\n escaped)\n'), ++ ('((loremipsum)(loremipsum)(loremipsum)(loremipsum))', ++ '((loremipsum)(loremipsum)(loremipsum)(loremipsum))\n'), ++ ('((loremipsum)(loremipsum)(loremipsum) (loremipsum))', ++ '((loremipsum)(loremipsum)(loremipsum)\n (loremipsum))\n'), ++ ] ++ for (to, folded) in cases: ++ with self.subTest(to=to): ++ self._test(parser.get_address_list(to)[0], folded, policy=policy) ++ + # XXX Need tests with comments on various sides of a unicode token, + # and with unicode tokens in the comments. Spaces inside the quotes + # currently don't do the right thing. +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst b/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst +new file mode 100644 +index 00000000000..c3d86493688 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst +@@ -0,0 +1,6 @@ ++Fixed a bug in the folding of comments when flattening an email message ++using a modern email policy. Comments consisting of a very long sequence of ++non-foldable characters could trigger a forced line wrap that omitted the ++required leading space on the continuation line, causing the remainder of ++the comment to be interpreted as a new header field. This enabled header ++injection with carefully crafted inputs. diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-12084.patch python3.11-3.11.2/debian/patches/CVE-2025-12084.patch --- python3.11-3.11.2/debian/patches/CVE-2025-12084.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-12084.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,170 @@ +From a46c10ec9d4050ab67b8a932e0859a2ea60c3cb8 Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Sun, 25 Jan 2026 18:10:53 +0100 +Subject: [PATCH] [3.11] gh-142145: Remove quadratic behavior in node ID cache + clearing (GH-142146) (#142212) + +* gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146) + +* Remove quadratic behavior in node ID cache clearing + +Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com> + +* Add news fragment + +--------- +(cherry picked from commit 08d8e18ad81cd45bc4a27d6da478b51ea49486e4) + +Co-authored-by: Seth Michael Larson +Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com> + +* [3.14] gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794) (#142818) + +gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794) +(cherry picked from commit 1cc7551b3f9f71efbc88d96dce90f82de98b2454) + +Co-authored-by: Petr Viktorin +Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> + +* gh-142145: relax the no-longer-quadratic test timing (GH-143030) + +* gh-142145: relax the no-longer-quadratic test timing + +* require cpu resource +(cherry picked from commit 8d2d7bb2e754f8649a68ce4116271a4932f76907) + +Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> + +* merge NEWS entries into one + +--------- + +Co-authored-by: Seth Michael Larson +Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com> +Co-authored-by: Petr Viktorin +Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> +Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> +Co-authored-by: Gregory P. Smith + +Origin: upstream, https://github.com/python/cpython/commit/a46c10ec9d4050ab67b8a932e0859a2ea60c3cb8 +--- + Lib/test/test_minidom.py | 33 ++++++++++++++++++- + Lib/xml/dom/minidom.py | 11 ++----- + ...-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 6 ++++ + 3 files changed, 41 insertions(+), 9 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst + +diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py +index ef38c362103fc6..c68bd990f723c9 100644 +--- a/Lib/test/test_minidom.py ++++ b/Lib/test/test_minidom.py +@@ -2,6 +2,7 @@ + + import copy + import pickle ++import time + import io + from test import support + import unittest +@@ -9,7 +10,7 @@ + import pyexpat + import xml.dom.minidom + +-from xml.dom.minidom import parse, Attr, Node, Document, parseString ++from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString + from xml.dom.minidom import getDOMImplementation + from xml.parsers.expat import ExpatError + +@@ -177,6 +178,36 @@ def testAppendChild(self): + self.confirm(dom.documentElement.childNodes[-1].data == "Hello") + dom.unlink() + ++ @support.requires_resource('cpu') ++ def testAppendChildNoQuadraticComplexity(self): ++ impl = getDOMImplementation() ++ ++ newdoc = impl.createDocument(None, "some_tag", None) ++ top_element = newdoc.documentElement ++ children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)] ++ element = top_element ++ ++ start = time.monotonic() ++ for child in children: ++ element.appendChild(child) ++ element = child ++ end = time.monotonic() ++ ++ # This example used to take at least 30 seconds. ++ # Conservative assertion due to the wide variety of systems and ++ # build configs timing based tests wind up run under. ++ # A --with-address-sanitizer --with-pydebug build on a rpi5 still ++ # completes this loop in <0.5 seconds. ++ self.assertLess(end - start, 4) ++ ++ def testSetAttributeNodeWithoutOwnerDocument(self): ++ # regression test for gh-142754 ++ elem = Element("test") ++ attr = Attr("id") ++ attr.value = "test-id" ++ elem.setAttributeNode(attr) ++ self.assertEqual(elem.getAttribute("id"), "test-id") ++ + def testAppendChildFragment(self): + dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes() + dom.documentElement.appendChild(frag) +diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py +index ef8a159833bbc0..cada981f39f3ee 100644 +--- a/Lib/xml/dom/minidom.py ++++ b/Lib/xml/dom/minidom.py +@@ -292,13 +292,6 @@ def _append_child(self, node): + childNodes.append(node) + node.parentNode = self + +-def _in_document(node): +- # return True iff node is part of a document tree +- while node is not None: +- if node.nodeType == Node.DOCUMENT_NODE: +- return True +- node = node.parentNode +- return False + + def _write_data(writer, data): + "Writes datachars to writer." +@@ -355,6 +348,7 @@ class Attr(Node): + def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None, + prefix=None): + self.ownerElement = None ++ self.ownerDocument = None + self._name = qName + self.namespaceURI = namespaceURI + self._prefix = prefix +@@ -680,6 +674,7 @@ class Element(Node): + + def __init__(self, tagName, namespaceURI=EMPTY_NAMESPACE, prefix=None, + localName=None): ++ self.ownerDocument = None + self.parentNode = None + self.tagName = self.nodeName = tagName + self.prefix = prefix +@@ -1539,7 +1534,7 @@ def _clear_id_cache(node): + if node.nodeType == Node.DOCUMENT_NODE: + node._id_cache.clear() + node._id_search_stack = None +- elif _in_document(node): ++ elif node.ownerDocument: + node.ownerDocument._id_cache.clear() + node.ownerDocument._id_search_stack= None + +diff --git a/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst +new file mode 100644 +index 00000000000000..05c7df35d14bef +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst +@@ -0,0 +1,6 @@ ++Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. In order ++to do this without breaking existing users, we also add the *ownerDocument* ++attribute to :mod:`xml.dom.minidom` elements and attributes created by directly ++instantiating the ``Element`` or ``Attr`` class. Note that this way of creating ++nodes is not supported; creator functions like ++:py:meth:`xml.dom.Document.documentElement` should be used instead. diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-13836.patch python3.11-3.11.2/debian/patches/CVE-2025-13836.patch --- python3.11-3.11.2/debian/patches/CVE-2025-13836.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-13836.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,160 @@ +From afc40bdd3dd71f343fd9016f6d8eebbacbd6587c Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Sun, 25 Jan 2026 18:11:02 +0100 +Subject: [PATCH] [3.11] gh-119451: Fix a potential denial of service in + http.client (GH-119454) (#142141) + +gh-119451: Fix a potential denial of service in http.client (GH-119454) + +Reading the whole body of the HTTP response could cause OOM if +the Content-Length value is too large even if the server does not send +a large amount of data. Now the HTTP client reads large data by chunks, +therefore the amount of consumed memory is proportional to the amount +of sent data. +(cherry picked from commit 5a4c4a033a4a54481be6870aa1896fad732555b5) + +Co-authored-by: Serhiy Storchaka + +Origin: upstream, https://github.com/python/cpython/commit/afc40bdd3dd71f343fd9016f6d8eebbacbd6587c +--- + Lib/http/client.py | 28 ++++++-- + Lib/test/test_httplib.py | 66 +++++++++++++++++++ + ...-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5 ++ + 3 files changed, 95 insertions(+), 4 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst + +diff --git a/Lib/http/client.py b/Lib/http/client.py +index 91ee1b470cfd47..c977612732afbc 100644 +--- a/Lib/http/client.py ++++ b/Lib/http/client.py +@@ -111,6 +111,11 @@ + _MAXLINE = 65536 + _MAXHEADERS = 100 + ++# Data larger than this will be read in chunks, to prevent extreme ++# overallocation. ++_MIN_READ_BUF_SIZE = 1 << 20 ++ ++ + # Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2) + # + # VCHAR = %x21-7E +@@ -627,10 +632,25 @@ def _safe_read(self, amt): + reading. If the bytes are truly not available (due to EOF), then the + IncompleteRead exception can be used to detect the problem. + """ +- data = self.fp.read(amt) +- if len(data) < amt: +- raise IncompleteRead(data, amt-len(data)) +- return data ++ cursize = min(amt, _MIN_READ_BUF_SIZE) ++ data = self.fp.read(cursize) ++ if len(data) >= amt: ++ return data ++ if len(data) < cursize: ++ raise IncompleteRead(data, amt - len(data)) ++ ++ data = io.BytesIO(data) ++ data.seek(0, 2) ++ while True: ++ # This is a geometric increase in read size (never more than ++ # doubling out the current length of data per loop iteration). ++ delta = min(cursize, amt - cursize) ++ data.write(self.fp.read(delta)) ++ if data.tell() >= amt: ++ return data.getvalue() ++ cursize += delta ++ if data.tell() < cursize: ++ raise IncompleteRead(data.getvalue(), amt - data.tell()) + + def _safe_readinto(self, b): + """Same as _safe_read, but for reading into a buffer.""" +diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py +index 8b9d49ec094813..55363413b3b140 100644 +--- a/Lib/test/test_httplib.py ++++ b/Lib/test/test_httplib.py +@@ -1372,6 +1372,72 @@ def run_server(): + thread.join() + self.assertEqual(result, b"proxied data\n") + ++ def test_large_content_length(self): ++ serv = socket.create_server((HOST, 0)) ++ self.addCleanup(serv.close) ++ ++ def run_server(): ++ [conn, address] = serv.accept() ++ with conn: ++ while conn.recv(1024): ++ conn.sendall( ++ b"HTTP/1.1 200 Ok\r\n" ++ b"Content-Length: %d\r\n" ++ b"\r\n" % size) ++ conn.sendall(b'A' * (size//3)) ++ conn.sendall(b'B' * (size - size//3)) ++ ++ thread = threading.Thread(target=run_server) ++ thread.start() ++ self.addCleanup(thread.join, 1.0) ++ ++ conn = client.HTTPConnection(*serv.getsockname()) ++ try: ++ for w in range(15, 27): ++ size = 1 << w ++ conn.request("GET", "/") ++ with conn.getresponse() as response: ++ self.assertEqual(len(response.read()), size) ++ finally: ++ conn.close() ++ thread.join(1.0) ++ ++ def test_large_content_length_truncated(self): ++ serv = socket.create_server((HOST, 0)) ++ self.addCleanup(serv.close) ++ ++ def run_server(): ++ while True: ++ [conn, address] = serv.accept() ++ with conn: ++ conn.recv(1024) ++ if not size: ++ break ++ conn.sendall( ++ b"HTTP/1.1 200 Ok\r\n" ++ b"Content-Length: %d\r\n" ++ b"\r\n" ++ b"Text" % size) ++ ++ thread = threading.Thread(target=run_server) ++ thread.start() ++ self.addCleanup(thread.join, 1.0) ++ ++ conn = client.HTTPConnection(*serv.getsockname()) ++ try: ++ for w in range(18, 65): ++ size = 1 << w ++ conn.request("GET", "/") ++ with conn.getresponse() as response: ++ self.assertRaises(client.IncompleteRead, response.read) ++ conn.close() ++ finally: ++ conn.close() ++ size = 0 ++ conn.request("GET", "/") ++ conn.close() ++ thread.join(1.0) ++ + def test_putrequest_override_domain_validation(self): + """ + It should be possible to override the default validation +diff --git a/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst b/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst +new file mode 100644 +index 00000000000000..6d6f25cd2f8bf7 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst +@@ -0,0 +1,5 @@ ++Fix a potential memory denial of service in the :mod:`http.client` module. ++When connecting to a malicious server, it could cause ++an arbitrary amount of memory to be allocated. ++This could have led to symptoms including a :exc:`MemoryError`, swapping, out ++of memory (OOM) killed processes or containers, or even system crashes. diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-13837.patch python3.11-3.11.2/debian/patches/CVE-2025-13837.patch --- python3.11-3.11.2/debian/patches/CVE-2025-13837.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-13837.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,160 @@ +From cefee7d118a26ef6cd43db59bb9d98ca9a331111 Mon Sep 17 00:00:00 2001 +From: Serhiy Storchaka +Date: Tue, 3 Mar 2026 00:55:04 +0200 +Subject: [PATCH] [3.11] gh-119342: Fix a potential denial of service in + plistlib (GH-119343) (#142150) + +Reading a specially prepared small Plist file could cause OOM because file's +read(n) preallocates a bytes object for reading the specified amount of +data. Now plistlib reads large data by chunks, therefore the upper limit of +consumed memory is proportional to the size of the input file. +(cherry picked from commit 694922cf40aa3a28f898b5f5ee08b71b4922df70) + +Origin: upstream, https://github.com/python/cpython/commit/cefee7d118a26ef6cd43db59bb9d98ca9a331111 +--- + Lib/plistlib.py | 31 ++++++++++------ + Lib/test/test_plistlib.py | 37 +++++++++++++++++-- + ...-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst | 5 +++ + 3 files changed, 59 insertions(+), 14 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst + +diff --git a/Lib/plistlib.py b/Lib/plistlib.py +index 53e718f063b3ec..63fefbd5f6d499 100644 +--- a/Lib/plistlib.py ++++ b/Lib/plistlib.py +@@ -64,6 +64,9 @@ + PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__) + globals().update(PlistFormat.__members__) + ++# Data larger than this will be read in chunks, to prevent extreme ++# overallocation. ++_MIN_READ_BUF_SIZE = 1 << 20 + + class UID: + def __init__(self, data): +@@ -490,12 +493,24 @@ def _get_size(self, tokenL): + + return tokenL + ++ def _read(self, size): ++ cursize = min(size, _MIN_READ_BUF_SIZE) ++ data = self._fp.read(cursize) ++ while True: ++ if len(data) != cursize: ++ raise InvalidFileException ++ if cursize == size: ++ return data ++ delta = min(cursize, size - cursize) ++ data += self._fp.read(delta) ++ cursize += delta ++ + def _read_ints(self, n, size): +- data = self._fp.read(size * n) ++ data = self._read(size * n) + if size in _BINARY_FORMAT: + return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data) + else: +- if not size or len(data) != size * n: ++ if not size: + raise InvalidFileException() + return tuple(int.from_bytes(data[i: i + size], 'big') + for i in range(0, size * n, size)) +@@ -552,22 +567,16 @@ def _read_object(self, ref): + + elif tokenH == 0x40: # data + s = self._get_size(tokenL) +- result = self._fp.read(s) +- if len(result) != s: +- raise InvalidFileException() ++ result = self._read(s) + + elif tokenH == 0x50: # ascii string + s = self._get_size(tokenL) +- data = self._fp.read(s) +- if len(data) != s: +- raise InvalidFileException() ++ data = self._read(s) + result = data.decode('ascii') + + elif tokenH == 0x60: # unicode string + s = self._get_size(tokenL) * 2 +- data = self._fp.read(s) +- if len(data) != s: +- raise InvalidFileException() ++ data = self._read(s) + result = data.decode('utf-16be') + + elif tokenH == 0x80: # UID +diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py +index 95b7a649774dca..2bc64afdbe932f 100644 +--- a/Lib/test/test_plistlib.py ++++ b/Lib/test/test_plistlib.py +@@ -841,8 +841,7 @@ def test_xml_plist_with_entity_decl(self): + + class TestBinaryPlistlib(unittest.TestCase): + +- @staticmethod +- def decode(*objects, offset_size=1, ref_size=1): ++ def build(self, *objects, offset_size=1, ref_size=1): + data = [b'bplist00'] + offset = 8 + offsets = [] +@@ -854,7 +853,11 @@ def decode(*objects, offset_size=1, ref_size=1): + len(objects), 0, offset) + data.extend(offsets) + data.append(tail) +- return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY) ++ return b''.join(data) ++ ++ def decode(self, *objects, offset_size=1, ref_size=1): ++ data = self.build(*objects, offset_size=offset_size, ref_size=ref_size) ++ return plistlib.loads(data, fmt=plistlib.FMT_BINARY) + + def test_nonstandard_refs_size(self): + # Issue #21538: Refs and offsets are 24-bit integers +@@ -962,6 +965,34 @@ def test_invalid_binary(self): + with self.assertRaises(plistlib.InvalidFileException): + plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY) + ++ def test_truncated_large_data(self): ++ self.addCleanup(os_helper.unlink, os_helper.TESTFN) ++ def check(data): ++ with open(os_helper.TESTFN, 'wb') as f: ++ f.write(data) ++ # buffered file ++ with open(os_helper.TESTFN, 'rb') as f: ++ with self.assertRaises(plistlib.InvalidFileException): ++ plistlib.load(f, fmt=plistlib.FMT_BINARY) ++ # unbuffered file ++ with open(os_helper.TESTFN, 'rb', buffering=0) as f: ++ with self.assertRaises(plistlib.InvalidFileException): ++ plistlib.load(f, fmt=plistlib.FMT_BINARY) ++ for w in range(20, 64): ++ s = 1 << w ++ # data ++ check(self.build(b'\x4f\x13' + s.to_bytes(8, 'big'))) ++ # ascii string ++ check(self.build(b'\x5f\x13' + s.to_bytes(8, 'big'))) ++ # unicode string ++ check(self.build(b'\x6f\x13' + s.to_bytes(8, 'big'))) ++ # array ++ check(self.build(b'\xaf\x13' + s.to_bytes(8, 'big'))) ++ # dict ++ check(self.build(b'\xdf\x13' + s.to_bytes(8, 'big'))) ++ # number of objects ++ check(b'bplist00' + struct.pack('>6xBBQQQ', 1, 1, s, 0, 8)) ++ + + class TestKeyedArchive(unittest.TestCase): + def test_keyed_archive_data(self): +diff --git a/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst b/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst +new file mode 100644 +index 00000000000000..04fd8faca4cf7e +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst +@@ -0,0 +1,5 @@ ++Fix a potential memory denial of service in the :mod:`plistlib` module. ++When reading a Plist file received from untrusted source, it could cause ++an arbitrary amount of memory to be allocated. ++This could have led to symptoms including a :exc:`MemoryError`, swapping, out ++of memory (OOM) killed processes or containers, or even system crashes. diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-15282.patch python3.11-3.11.2/debian/patches/CVE-2025-15282.patch --- python3.11-3.11.2/debian/patches/CVE-2025-15282.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-15282.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,66 @@ +From 3f396ca9d7bbe2a50ea6b8c9b27c0082884d9f80 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Sun, 25 Jan 2026 11:05:19 -0600 +Subject: [PATCH] [3.11] gh-143925: Reject control characters in data: URL + mediatypes (#144114) + +(cherry picked from commit f25509e78e8be6ea73c811ac2b8c928c28841b9f) +(cherry picked from commit 2c9c746077d8119b5bcf5142316992e464594946) + +Origin: upstream, https://github.com/python/cpython/commit/3f396ca9d7bbe2a50ea6b8c9b27c0082884d9f80 +--- + Lib/test/test_urllib.py | 8 ++++++++ + Lib/urllib/request.py | 5 +++++ + .../2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst | 1 + + 3 files changed, 14 insertions(+) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst + +diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py +index f067560ca6caa1..497372a38e392a 100644 +--- a/Lib/test/test_urllib.py ++++ b/Lib/test/test_urllib.py +@@ -12,6 +12,7 @@ + from test.support import os_helper + from test.support import socket_helper + from test.support import warnings_helper ++from test.support import control_characters_c0 + import os + try: + import ssl +@@ -683,6 +684,13 @@ def test_invalid_base64_data(self): + # missing padding character + self.assertRaises(ValueError,urllib.request.urlopen,'data:;base64,Cg=') + ++ def test_invalid_mediatype(self): ++ for c0 in control_characters_c0(): ++ self.assertRaises(ValueError,urllib.request.urlopen, ++ f'data:text/html;{c0},data') ++ for c0 in control_characters_c0(): ++ self.assertRaises(ValueError,urllib.request.urlopen, ++ f'data:text/html{c0};base64,ZGF0YQ==') + + class urlretrieve_FileTests(unittest.TestCase): + """Test urllib.urlretrieve() on local files""" +diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py +index d98ba5dd1983b9..3abb7ae1b049b7 100644 +--- a/Lib/urllib/request.py ++++ b/Lib/urllib/request.py +@@ -1654,6 +1654,11 @@ def data_open(self, req): + scheme, data = url.split(":",1) + mediatype, data = data.split(",",1) + ++ # Disallow control characters within mediatype. ++ if re.search(r"[\x00-\x1F\x7F]", mediatype): ++ raise ValueError( ++ "Control characters not allowed in data: mediatype") ++ + # even base64 encoded data URLs might be quoted so unquote in any case: + data = unquote_to_bytes(data) + if mediatype.endswith(";base64"): +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst +new file mode 100644 +index 00000000000000..46109dfbef3ee7 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst +@@ -0,0 +1 @@ ++Reject control characters in ``data:`` URL media types. diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-4516-1.patch python3.11-3.11.2/debian/patches/CVE-2025-4516-1.patch --- python3.11-3.11.2/debian/patches/CVE-2025-4516-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-4516-1.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,156 @@ +From b8b3e6afc0a48c3cbb7c36d2f73e332edcd6058c Mon Sep 17 00:00:00 2001 +From: Serhiy Storchaka +Date: Tue, 25 Jul 2023 14:15:14 +0300 +Subject: [PATCH] [3.11] gh-99612: Fix PyUnicode_DecodeUTF8Stateful() for + ASCII-only data (GH-99613) (GH-107224) + +Previously *consumed was not set in this case. +(cherry picked from commit f08e52ccb027f6f703302b8c1a82db9fd3934270) + +Origin: upstream, https://github.com/python/cpython/commit/b8b3e6afc0a48c3cbb7c36d2f73e332edcd6058c +--- + Lib/test/test_capi/test_codecs.py | 54 +++++++++++++++++++ + ...2-11-20-09-52-50.gh-issue-99612.eBHksg.rst | 2 + + Modules/_testcapimodule.c | 37 ++++++++++++- + Objects/unicodeobject.c | 3 ++ + 4 files changed, 95 insertions(+), 1 deletion(-) + create mode 100644 Lib/test/test_capi/test_codecs.py + create mode 100644 Misc/NEWS.d/next/C API/2022-11-20-09-52-50.gh-issue-99612.eBHksg.rst + +diff --git a/Lib/test/test_capi/test_codecs.py b/Lib/test/test_capi/test_codecs.py +new file mode 100644 +index 00000000000000..e46726192aa05b +--- /dev/null ++++ b/Lib/test/test_capi/test_codecs.py +@@ -0,0 +1,54 @@ ++import unittest ++from test.support import import_helper ++ ++_testcapi = import_helper.import_module('_testcapi') ++ ++ ++class CAPITest(unittest.TestCase): ++ ++ def test_decodeutf8(self): ++ """Test PyUnicode_DecodeUTF8()""" ++ decodeutf8 = _testcapi.unicode_decodeutf8 ++ ++ for s in ['abc', '\xa1\xa2', '\u4f60\u597d', 'a\U0001f600']: ++ b = s.encode('utf-8') ++ self.assertEqual(decodeutf8(b), s) ++ self.assertEqual(decodeutf8(b, 'strict'), s) ++ ++ self.assertRaises(UnicodeDecodeError, decodeutf8, b'\x80') ++ self.assertRaises(UnicodeDecodeError, decodeutf8, b'\xc0') ++ self.assertRaises(UnicodeDecodeError, decodeutf8, b'\xff') ++ self.assertRaises(UnicodeDecodeError, decodeutf8, b'a\xf0\x9f') ++ self.assertEqual(decodeutf8(b'a\xf0\x9f', 'replace'), 'a\ufffd') ++ self.assertEqual(decodeutf8(b'a\xf0\x9fb', 'replace'), 'a\ufffdb') ++ ++ self.assertRaises(LookupError, decodeutf8, b'a\x80', 'foo') ++ # TODO: Test PyUnicode_DecodeUTF8() with NULL as data and ++ # negative size. ++ ++ def test_decodeutf8stateful(self): ++ """Test PyUnicode_DecodeUTF8Stateful()""" ++ decodeutf8stateful = _testcapi.unicode_decodeutf8stateful ++ ++ for s in ['abc', '\xa1\xa2', '\u4f60\u597d', 'a\U0001f600']: ++ b = s.encode('utf-8') ++ self.assertEqual(decodeutf8stateful(b), (s, len(b))) ++ self.assertEqual(decodeutf8stateful(b, 'strict'), (s, len(b))) ++ ++ self.assertRaises(UnicodeDecodeError, decodeutf8stateful, b'\x80') ++ self.assertRaises(UnicodeDecodeError, decodeutf8stateful, b'\xc0') ++ self.assertRaises(UnicodeDecodeError, decodeutf8stateful, b'\xff') ++ self.assertEqual(decodeutf8stateful(b'a\xf0\x9f'), ('a', 1)) ++ self.assertEqual(decodeutf8stateful(b'a\xf0\x9f', 'replace'), ('a', 1)) ++ self.assertRaises(UnicodeDecodeError, decodeutf8stateful, b'a\xf0\x9fb') ++ self.assertEqual(decodeutf8stateful(b'a\xf0\x9fb', 'replace'), ('a\ufffdb', 4)) ++ ++ self.assertRaises(LookupError, decodeutf8stateful, b'a\x80', 'foo') ++ # TODO: Test PyUnicode_DecodeUTF8Stateful() with NULL as data and ++ # negative size. ++ # TODO: Test PyUnicode_DecodeUTF8Stateful() with NULL as the address of ++ # "consumed". ++ ++ ++if __name__ == "__main__": ++ unittest.main() +diff --git a/Misc/NEWS.d/next/C API/2022-11-20-09-52-50.gh-issue-99612.eBHksg.rst b/Misc/NEWS.d/next/C API/2022-11-20-09-52-50.gh-issue-99612.eBHksg.rst +new file mode 100644 +index 00000000000000..40e3c8db5403c7 +--- /dev/null ++++ b/Misc/NEWS.d/next/C API/2022-11-20-09-52-50.gh-issue-99612.eBHksg.rst +@@ -0,0 +1,2 @@ ++Fix :c:func:`PyUnicode_DecodeUTF8Stateful` for ASCII-only data: ++``*consumed`` was not set. +diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c +index b40368eb4b3409..b29a919a8325b6 100644 +--- a/Modules/_testcapimodule.c ++++ b/Modules/_testcapimodule.c +@@ -2307,6 +2307,40 @@ unicode_asutf8andsize(PyObject *self, PyObject *args) + return Py_BuildValue("(Nn)", result, utf8_len); + } + ++/* Test PyUnicode_DecodeUTF8() */ ++static PyObject * ++unicode_decodeutf8(PyObject *self, PyObject *args) ++{ ++ const char *data; ++ Py_ssize_t size; ++ const char *errors = NULL; ++ ++ if (!PyArg_ParseTuple(args, "y#|z", &data, &size, &errors)) ++ return NULL; ++ ++ return PyUnicode_DecodeUTF8(data, size, errors); ++} ++ ++/* Test PyUnicode_DecodeUTF8Stateful() */ ++static PyObject * ++unicode_decodeutf8stateful(PyObject *self, PyObject *args) ++{ ++ const char *data; ++ Py_ssize_t size; ++ const char *errors = NULL; ++ Py_ssize_t consumed = 123456789; ++ PyObject *result; ++ ++ if (!PyArg_ParseTuple(args, "y#|z", &data, &size, &errors)) ++ return NULL; ++ ++ result = PyUnicode_DecodeUTF8Stateful(data, size, errors, &consumed); ++ if (!result) { ++ return NULL; ++ } ++ return Py_BuildValue("(Nn)", result, consumed); ++} ++ + static PyObject * + unicode_findchar(PyObject *self, PyObject *args) + { +@@ -6524,7 +6558,8 @@ static PyMethodDef TestMethods[] = { + {"unicode_asucs4", unicode_asucs4, METH_VARARGS}, + {"unicode_asutf8", unicode_asutf8, METH_VARARGS}, + {"unicode_asutf8andsize", unicode_asutf8andsize, METH_VARARGS}, +- {"unicode_findchar", unicode_findchar, METH_VARARGS}, ++ {"unicode_decodeutf8", unicode_decodeutf8, METH_VARARGS}, ++ {"unicode_decodeutf8stateful",unicode_decodeutf8stateful, METH_VARARGS}, {"unicode_findchar", unicode_findchar, METH_VARARGS}, + {"unicode_copycharacters", unicode_copycharacters, METH_VARARGS}, + #if USE_UNICODE_WCHAR_CACHE + {"unicode_legacy_string", unicode_legacy_string, METH_VARARGS}, +diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c +index 50deefe0adc2cc..ffa8b982bdc29f 100644 +--- a/Objects/unicodeobject.c ++++ b/Objects/unicodeobject.c +@@ -5120,6 +5120,9 @@ unicode_decode_utf8(const char *s, Py_ssize_t size, + } + s += ascii_decode(s, end, PyUnicode_1BYTE_DATA(u)); + if (s == end) { ++ if (consumed) { ++ *consumed = size; ++ } + return u; + } + diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-4516-2.patch python3.11-3.11.2/debian/patches/CVE-2025-4516-2.patch --- python3.11-3.11.2/debian/patches/CVE-2025-4516-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-4516-2.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,516 @@ +From 73b3040f592436385007918887b7e2132aa8431f Mon Sep 17 00:00:00 2001 +From: Serhiy Storchaka +Date: Mon, 2 Jun 2025 18:52:52 +0300 +Subject: [PATCH] [3.11] gh-133767: Fix use-after-free in the unicode-escape + decoder with an error handler (GH-129648) (GH-133944) (GH-134341) + +If the error handler is used, a new bytes object is created to set as +the object attribute of UnicodeDecodeError, and that bytes object then +replaces the original data. A pointer to the decoded data will became invalid +after destroying that temporary bytes object. So we need other way to return +the first invalid escape from _PyUnicode_DecodeUnicodeEscapeInternal(). + +_PyBytes_DecodeEscape() does not have such issue, because it does not +use the error handlers registry, but it should be changed for compatibility +with _PyUnicode_DecodeUnicodeEscapeInternal(). +(cherry picked from commit 9f69a58623bd01349a18ba0c7a9cb1dad6a51e8e) +(cherry picked from commit 6279eb8c076d89d3739a6edb393e43c7929b429d) +(cherry picked from commit a75953b347716fff694aa59a7c7c2489fa50d1f5) + +Co-authored-by: Serhiy Storchaka + +Origin: upstream, https://github.com/python/cpython/commit/73b3040f592436385007918887b7e2132aa8431f +--- + Include/cpython/bytesobject.h | 4 ++ + Include/cpython/unicodeobject.h | 13 ++++ + Lib/test/test_codeccallbacks.py | 38 ++++++++++- + Lib/test/test_codecs.py | 52 ++++++++++++--- + ...-05-09-20-22-54.gh-issue-133767.kN2i3Q.rst | 2 + + Objects/bytesobject.c | 56 +++++++++++------ + Objects/unicodeobject.c | 63 +++++++++++++------ + Parser/string_parser.c | 26 +++++--- + 8 files changed, 197 insertions(+), 57 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-05-09-20-22-54.gh-issue-133767.kN2i3Q.rst + +diff --git a/Include/cpython/bytesobject.h b/Include/cpython/bytesobject.h +index 53343661f0ec43..0899bf62615ef6 100644 +--- a/Include/cpython/bytesobject.h ++++ b/Include/cpython/bytesobject.h +@@ -25,6 +25,10 @@ PyAPI_FUNC(PyObject*) _PyBytes_FromHex( + int use_bytearray); + + /* Helper for PyBytes_DecodeEscape that detects invalid escape chars. */ ++PyAPI_FUNC(PyObject*) _PyBytes_DecodeEscape2(const char *, Py_ssize_t, ++ const char *, ++ int *, const char **); ++// Export for binary compatibility. + PyAPI_FUNC(PyObject *) _PyBytes_DecodeEscape(const char *, Py_ssize_t, + const char *, const char **); + +diff --git a/Include/cpython/unicodeobject.h b/Include/cpython/unicodeobject.h +index 84307d18854725..41debcbc06af69 100644 +--- a/Include/cpython/unicodeobject.h ++++ b/Include/cpython/unicodeobject.h +@@ -914,6 +914,19 @@ PyAPI_FUNC(PyObject*) _PyUnicode_DecodeUnicodeEscapeStateful( + ); + /* Helper for PyUnicode_DecodeUnicodeEscape that detects invalid escape + chars. */ ++PyAPI_FUNC(PyObject*) _PyUnicode_DecodeUnicodeEscapeInternal2( ++ const char *string, /* Unicode-Escape encoded string */ ++ Py_ssize_t length, /* size of string */ ++ const char *errors, /* error handling */ ++ Py_ssize_t *consumed, /* bytes consumed */ ++ int *first_invalid_escape_char, /* on return, if not -1, contain the first ++ invalid escaped char (<= 0xff) or invalid ++ octal escape (> 0xff) in string. */ ++ const char **first_invalid_escape_ptr); /* on return, if not NULL, may ++ point to the first invalid escaped ++ char in string. ++ May be NULL if errors is not NULL. */ ++// Export for binary compatibility. + PyAPI_FUNC(PyObject*) _PyUnicode_DecodeUnicodeEscapeInternal( + const char *string, /* Unicode-Escape encoded string */ + Py_ssize_t length, /* size of string */ +diff --git a/Lib/test/test_codeccallbacks.py b/Lib/test/test_codeccallbacks.py +index 4991330489d139..eed13e838ebd43 100644 +--- a/Lib/test/test_codeccallbacks.py ++++ b/Lib/test/test_codeccallbacks.py +@@ -1124,7 +1124,7 @@ def test_bug828737(self): + text = 'abcghi'*n + text.translate(charmap) + +- def test_mutatingdecodehandler(self): ++ def test_mutating_decode_handler(self): + baddata = [ + ("ascii", b"\xff"), + ("utf-7", b"++"), +@@ -1159,6 +1159,42 @@ def mutating(exc): + for (encoding, data) in baddata: + self.assertEqual(data.decode(encoding, "test.mutating"), "\u4242") + ++ def test_mutating_decode_handler_unicode_escape(self): ++ decode = codecs.unicode_escape_decode ++ def mutating(exc): ++ if isinstance(exc, UnicodeDecodeError): ++ r = data.get(exc.object[:exc.end]) ++ if r is not None: ++ exc.object = r[0] + exc.object[exc.end:] ++ return ('\u0404', r[1]) ++ raise AssertionError("don't know how to handle %r" % exc) ++ ++ codecs.register_error('test.mutating2', mutating) ++ data = { ++ br'\x0': (b'\\', 0), ++ br'\x3': (b'xxx\\', 3), ++ br'\x5': (b'x\\', 1), ++ } ++ def check(input, expected, msg): ++ with self.assertWarns(DeprecationWarning) as cm: ++ self.assertEqual(decode(input, 'test.mutating2'), (expected, len(input))) ++ self.assertIn(msg, str(cm.warning)) ++ ++ check(br'\x0n\z', '\u0404\n\\z', r"invalid escape sequence '\z'") ++ check(br'\x0n\501', '\u0404\n\u0141', r"invalid octal escape sequence '\501'") ++ check(br'\x0z', '\u0404\\z', r"invalid escape sequence '\z'") ++ ++ check(br'\x3n\zr', '\u0404\n\\zr', r"invalid escape sequence '\z'") ++ check(br'\x3zr', '\u0404\\zr', r"invalid escape sequence '\z'") ++ check(br'\x3z5', '\u0404\\z5', r"invalid escape sequence '\z'") ++ check(memoryview(br'\x3z5x')[:-1], '\u0404\\z5', r"invalid escape sequence '\z'") ++ check(memoryview(br'\x3z5xy')[:-2], '\u0404\\z5', r"invalid escape sequence '\z'") ++ ++ check(br'\x5n\z', '\u0404\n\\z', r"invalid escape sequence '\z'") ++ check(br'\x5n\501', '\u0404\n\u0141', r"invalid octal escape sequence '\501'") ++ check(br'\x5z', '\u0404\\z', r"invalid escape sequence '\z'") ++ check(memoryview(br'\x5zy')[:-1], '\u0404\\z', r"invalid escape sequence '\z'") ++ + # issue32583 + def test_crashing_decode_handler(self): + # better generating one more character to fill the extra space slot +diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py +index a7440eea67c101..78d67a568a365c 100644 +--- a/Lib/test/test_codecs.py ++++ b/Lib/test/test_codecs.py +@@ -1196,23 +1196,39 @@ def test_escape(self): + check(br"[\1010]", b"[A0]") + check(br"[\x41]", b"[A]") + check(br"[\x410]", b"[A0]") ++ ++ def test_warnings(self): ++ decode = codecs.escape_decode ++ check = coding_checker(self, decode) + for i in range(97, 123): + b = bytes([i]) + if b not in b'abfnrtvx': +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\%c'" % i): + check(b"\\" + b, b"\\" + b) +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\%c'" % (i-32)): + check(b"\\" + b.upper(), b"\\" + b.upper()) +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\8'"): + check(br"\8", b"\\8") + with self.assertWarns(DeprecationWarning): + check(br"\9", b"\\9") +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\\xfa'") as cm: + check(b"\\\xfa", b"\\\xfa") + for i in range(0o400, 0o1000): +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid octal escape sequence '\\%o'" % i): + check(rb'\%o' % i, bytes([i & 0o377])) + ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\z'"): ++ self.assertEqual(decode(br'\x\z', 'ignore'), (b'\\z', 4)) ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid octal escape sequence '\\501'"): ++ self.assertEqual(decode(br'\x\501', 'ignore'), (b'A', 6)) ++ + def test_errors(self): + decode = codecs.escape_decode + self.assertRaises(ValueError, decode, br"\x") +@@ -2430,24 +2446,40 @@ def test_escape_decode(self): + check(br"[\x410]", "[A0]") + check(br"\u20ac", "\u20ac") + check(br"\U0001d120", "\U0001d120") ++ ++ def test_decode_warnings(self): ++ decode = codecs.unicode_escape_decode ++ check = coding_checker(self, decode) + for i in range(97, 123): + b = bytes([i]) + if b not in b'abfnrtuvx': +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\%c'" % i): + check(b"\\" + b, "\\" + chr(i)) + if b.upper() not in b'UN': +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\%c'" % (i-32)): + check(b"\\" + b.upper(), "\\" + chr(i-32)) +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\8'"): + check(br"\8", "\\8") + with self.assertWarns(DeprecationWarning): + check(br"\9", "\\9") +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\\xfa'") as cm: + check(b"\\\xfa", "\\\xfa") + for i in range(0o400, 0o1000): +- with self.assertWarns(DeprecationWarning): ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid octal escape sequence '\\%o'" % i): + check(rb'\%o' % i, chr(i)) + ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid escape sequence '\\z'"): ++ self.assertEqual(decode(br'\x\z', 'ignore'), ('\\z', 4)) ++ with self.assertWarnsRegex(DeprecationWarning, ++ r"invalid octal escape sequence '\\501'"): ++ self.assertEqual(decode(br'\x\501', 'ignore'), ('\u0141', 6)) ++ + def test_decode_errors(self): + decode = codecs.unicode_escape_decode + for c, d in (b'x', 2), (b'u', 4), (b'U', 4): +diff --git a/Misc/NEWS.d/next/Security/2025-05-09-20-22-54.gh-issue-133767.kN2i3Q.rst b/Misc/NEWS.d/next/Security/2025-05-09-20-22-54.gh-issue-133767.kN2i3Q.rst +new file mode 100644 +index 00000000000000..39d2f1e1a892cf +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2025-05-09-20-22-54.gh-issue-133767.kN2i3Q.rst +@@ -0,0 +1,2 @@ ++Fix use-after-free in the "unicode-escape" decoder with a non-"strict" error ++handler. +diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c +index 279579f63418c0..bc530899ce76db 100644 +--- a/Objects/bytesobject.c ++++ b/Objects/bytesobject.c +@@ -1061,10 +1061,11 @@ _PyBytes_FormatEx(const char *format, Py_ssize_t format_len, + } + + /* Unescape a backslash-escaped string. */ +-PyObject *_PyBytes_DecodeEscape(const char *s, ++PyObject *_PyBytes_DecodeEscape2(const char *s, + Py_ssize_t len, + const char *errors, +- const char **first_invalid_escape) ++ int *first_invalid_escape_char, ++ const char **first_invalid_escape_ptr) + { + int c; + char *p; +@@ -1078,7 +1079,8 @@ PyObject *_PyBytes_DecodeEscape(const char *s, + return NULL; + writer.overallocate = 1; + +- *first_invalid_escape = NULL; ++ *first_invalid_escape_char = -1; ++ *first_invalid_escape_ptr = NULL; + + end = s + len; + while (s < end) { +@@ -1116,9 +1118,10 @@ PyObject *_PyBytes_DecodeEscape(const char *s, + c = (c<<3) + *s++ - '0'; + } + if (c > 0377) { +- if (*first_invalid_escape == NULL) { +- *first_invalid_escape = s-3; /* Back up 3 chars, since we've +- already incremented s. */ ++ if (*first_invalid_escape_char == -1) { ++ *first_invalid_escape_char = c; ++ /* Back up 3 chars, since we've already incremented s. */ ++ *first_invalid_escape_ptr = s - 3; + } + } + *p++ = c; +@@ -1159,9 +1162,10 @@ PyObject *_PyBytes_DecodeEscape(const char *s, + break; + + default: +- if (*first_invalid_escape == NULL) { +- *first_invalid_escape = s-1; /* Back up one char, since we've +- already incremented s. */ ++ if (*first_invalid_escape_char == -1) { ++ *first_invalid_escape_char = (unsigned char)s[-1]; ++ /* Back up one char, since we've already incremented s. */ ++ *first_invalid_escape_ptr = s - 1; + } + *p++ = '\\'; + s--; +@@ -1175,23 +1179,39 @@ PyObject *_PyBytes_DecodeEscape(const char *s, + return NULL; + } + ++// Export for binary compatibility. ++PyObject *_PyBytes_DecodeEscape(const char *s, ++ Py_ssize_t len, ++ const char *errors, ++ const char **first_invalid_escape) ++{ ++ int first_invalid_escape_char; ++ return _PyBytes_DecodeEscape2( ++ s, len, errors, ++ &first_invalid_escape_char, ++ first_invalid_escape); ++} ++ + PyObject *PyBytes_DecodeEscape(const char *s, + Py_ssize_t len, + const char *errors, + Py_ssize_t Py_UNUSED(unicode), + const char *Py_UNUSED(recode_encoding)) + { +- const char* first_invalid_escape; +- PyObject *result = _PyBytes_DecodeEscape(s, len, errors, +- &first_invalid_escape); ++ int first_invalid_escape_char; ++ const char *first_invalid_escape_ptr; ++ PyObject *result = _PyBytes_DecodeEscape2(s, len, errors, ++ &first_invalid_escape_char, ++ &first_invalid_escape_ptr); + if (result == NULL) + return NULL; +- if (first_invalid_escape != NULL) { +- unsigned char c = *first_invalid_escape; +- if ('4' <= c && c <= '7') { ++ if (first_invalid_escape_char != -1) { ++ if (first_invalid_escape_char > 0xff) { ++ char buf[12] = ""; ++ snprintf(buf, sizeof buf, "%o", first_invalid_escape_char); + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, +- "invalid octal escape sequence '\\%.3s'", +- first_invalid_escape) < 0) ++ "invalid octal escape sequence '\\%s'", ++ buf) < 0) + { + Py_DECREF(result); + return NULL; +@@ -1200,7 +1220,7 @@ PyObject *PyBytes_DecodeEscape(const char *s, + else { + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, + "invalid escape sequence '\\%c'", +- c) < 0) ++ first_invalid_escape_char) < 0) + { + Py_DECREF(result); + return NULL; +diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c +index 47c4e2a103a4a8..ffbd9ca2938893 100644 +--- a/Objects/unicodeobject.c ++++ b/Objects/unicodeobject.c +@@ -6301,20 +6301,23 @@ PyUnicode_AsUTF16String(PyObject *unicode) + static _PyUnicode_Name_CAPI *ucnhash_capi = NULL; + + PyObject * +-_PyUnicode_DecodeUnicodeEscapeInternal(const char *s, ++_PyUnicode_DecodeUnicodeEscapeInternal2(const char *s, + Py_ssize_t size, + const char *errors, + Py_ssize_t *consumed, +- const char **first_invalid_escape) ++ int *first_invalid_escape_char, ++ const char **first_invalid_escape_ptr) + { + const char *starts = s; ++ const char *initial_starts = starts; + _PyUnicodeWriter writer; + const char *end; + PyObject *errorHandler = NULL; + PyObject *exc = NULL; + + // so we can remember if we've seen an invalid escape char or not +- *first_invalid_escape = NULL; ++ *first_invalid_escape_char = -1; ++ *first_invalid_escape_ptr = NULL; + + if (size == 0) { + if (consumed) { +@@ -6402,9 +6405,12 @@ _PyUnicode_DecodeUnicodeEscapeInternal(const char *s, + } + } + if (ch > 0377) { +- if (*first_invalid_escape == NULL) { +- *first_invalid_escape = s-3; /* Back up 3 chars, since we've +- already incremented s. */ ++ if (*first_invalid_escape_char == -1) { ++ *first_invalid_escape_char = ch; ++ if (starts == initial_starts) { ++ /* Back up 3 chars, since we've already incremented s. */ ++ *first_invalid_escape_ptr = s - 3; ++ } + } + } + WRITE_CHAR(ch); +@@ -6503,9 +6509,12 @@ _PyUnicode_DecodeUnicodeEscapeInternal(const char *s, + goto error; + + default: +- if (*first_invalid_escape == NULL) { +- *first_invalid_escape = s-1; /* Back up one char, since we've +- already incremented s. */ ++ if (*first_invalid_escape_char == -1) { ++ *first_invalid_escape_char = c; ++ if (starts == initial_starts) { ++ /* Back up one char, since we've already incremented s. */ ++ *first_invalid_escape_ptr = s - 1; ++ } + } + WRITE_ASCII_CHAR('\\'); + WRITE_CHAR(c); +@@ -6544,24 +6553,42 @@ _PyUnicode_DecodeUnicodeEscapeInternal(const char *s, + return NULL; + } + ++// Export for binary compatibility. ++PyObject * ++_PyUnicode_DecodeUnicodeEscapeInternal(const char *s, ++ Py_ssize_t size, ++ const char *errors, ++ Py_ssize_t *consumed, ++ const char **first_invalid_escape) ++{ ++ int first_invalid_escape_char; ++ return _PyUnicode_DecodeUnicodeEscapeInternal2( ++ s, size, errors, consumed, ++ &first_invalid_escape_char, ++ first_invalid_escape); ++} ++ + PyObject * + _PyUnicode_DecodeUnicodeEscapeStateful(const char *s, + Py_ssize_t size, + const char *errors, + Py_ssize_t *consumed) + { +- const char *first_invalid_escape; +- PyObject *result = _PyUnicode_DecodeUnicodeEscapeInternal(s, size, errors, ++ int first_invalid_escape_char; ++ const char *first_invalid_escape_ptr; ++ PyObject *result = _PyUnicode_DecodeUnicodeEscapeInternal2(s, size, errors, + consumed, +- &first_invalid_escape); ++ &first_invalid_escape_char, ++ &first_invalid_escape_ptr); + if (result == NULL) + return NULL; +- if (first_invalid_escape != NULL) { +- unsigned char c = *first_invalid_escape; +- if ('4' <= c && c <= '7') { ++ if (first_invalid_escape_char != -1) { ++ if (first_invalid_escape_char > 0xff) { ++ char buf[12] = ""; ++ snprintf(buf, sizeof buf, "%o", first_invalid_escape_char); + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, +- "invalid octal escape sequence '\\%.3s'", +- first_invalid_escape) < 0) ++ "invalid octal escape sequence '\\%s'", ++ buf) < 0) + { + Py_DECREF(result); + return NULL; +@@ -6570,7 +6597,7 @@ _PyUnicode_DecodeUnicodeEscapeStateful(const char *s, + else { + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, + "invalid escape sequence '\\%c'", +- c) < 0) ++ first_invalid_escape_char) < 0) + { + Py_DECREF(result); + return NULL; +diff --git a/Parser/string_parser.c b/Parser/string_parser.c +index 7079b82d04f8ec..9c237bbbad2d04 100644 +--- a/Parser/string_parser.c ++++ b/Parser/string_parser.c +@@ -125,12 +125,15 @@ decode_unicode_with_escapes(Parser *parser, const char *s, size_t len, Token *t) + len = p - buf; + s = buf; + +- const char *first_invalid_escape; +- v = _PyUnicode_DecodeUnicodeEscapeInternal(s, len, NULL, NULL, &first_invalid_escape); +- +- if (v != NULL && first_invalid_escape != NULL) { +- if (warn_invalid_escape_sequence(parser, first_invalid_escape, t) < 0) { +- /* We have not decref u before because first_invalid_escape points ++ int first_invalid_escape_char; ++ const char *first_invalid_escape_ptr; ++ v = _PyUnicode_DecodeUnicodeEscapeInternal2(s, (Py_ssize_t)len, NULL, NULL, ++ &first_invalid_escape_char, ++ &first_invalid_escape_ptr); ++ ++ if (v != NULL && first_invalid_escape_ptr != NULL) { ++ if (warn_invalid_escape_sequence(parser, first_invalid_escape_ptr, t) < 0) { ++ /* We have not decref u before because first_invalid_escape_ptr points + inside u. */ + Py_XDECREF(u); + Py_DECREF(v); +@@ -144,14 +147,17 @@ decode_unicode_with_escapes(Parser *parser, const char *s, size_t len, Token *t) + static PyObject * + decode_bytes_with_escapes(Parser *p, const char *s, Py_ssize_t len, Token *t) + { +- const char *first_invalid_escape; +- PyObject *result = _PyBytes_DecodeEscape(s, len, NULL, &first_invalid_escape); ++ int first_invalid_escape_char; ++ const char *first_invalid_escape_ptr; ++ PyObject *result = _PyBytes_DecodeEscape2(s, len, NULL, ++ &first_invalid_escape_char, ++ &first_invalid_escape_ptr); + if (result == NULL) { + return NULL; + } + +- if (first_invalid_escape != NULL) { +- if (warn_invalid_escape_sequence(p, first_invalid_escape, t) < 0) { ++ if (first_invalid_escape_ptr != NULL) { ++ if (warn_invalid_escape_sequence(p, first_invalid_escape_ptr, t) < 0) { + Py_DECREF(result); + return NULL; + } diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-6069.patch python3.11-3.11.2/debian/patches/CVE-2025-6069.patch --- python3.11-3.11.2/debian/patches/CVE-2025-6069.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-6069.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,239 @@ +From f3c6f882cddc8dc30320d2e73edf019e201394fc Mon Sep 17 00:00:00 2001 +From: Serhiy Storchaka +Date: Fri, 4 Jul 2025 00:05:46 +0300 +Subject: [PATCH] [3.11] gh-135462: Fix quadratic complexity in processing + special input in HTMLParser (GH-135464) (GH-135484) + +End-of-file errors are now handled according to the HTML5 specs -- +comments and declarations are automatically closed, tags are ignored. +(cherry picked from commit 6eb6c5dbfb528bd07d77b60fd71fd05d81d45c41) + +Origin: upstream, https://github.com/python/cpython/commit/f3c6f882cddc8dc30320d2e73edf019e201394fc +--- + Lib/html/parser.py | 41 +++++--- + Lib/test/test_htmlparser.py | 95 ++++++++++++++++--- + ...-06-13-15-55-22.gh-issue-135462.KBeJpc.rst | 4 + + 3 files changed, 117 insertions(+), 23 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-06-13-15-55-22.gh-issue-135462.KBeJpc.rst + +diff --git a/Lib/html/parser.py b/Lib/html/parser.py +index bef0f4fe4bf776..9c38008bbfd06b 100644 +--- a/Lib/html/parser.py ++++ b/Lib/html/parser.py +@@ -25,6 +25,7 @@ + charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]') + + starttagopen = re.compile('<[a-zA-Z]') ++endtagopen = re.compile('') + commentclose = re.compile(r'--\s*>') + # Note: +@@ -176,7 +177,7 @@ def goahead(self, end): + k = self.parse_pi(i) + elif startswith("', i + 1) +- if k < 0: +- k = rawdata.find('<', i + 1) +- if k < 0: +- k = i + 1 +- else: +- k += 1 +- if self.convert_charrefs and not self.cdata_elem: +- self.handle_data(unescape(rawdata[i:k])) ++ if starttagopen.match(rawdata, i): # < + letter ++ pass ++ elif startswith("'), +- ('comment', '/img'), +- ('endtag', 'html<')]) ++ ('data', '\n')]) + + def test_starttag_junk_chars(self): ++ self._run_check("<", [('data', '<')]) ++ self._run_check("<>", [('data', '<>')]) ++ self._run_check("< >", [('data', '< >')]) ++ self._run_check("< ", [('data', '< ')]) + self._run_check("", []) ++ self._run_check("<$>", [('data', '<$>')]) + self._run_check("", [('comment', '$')]) + self._run_check("", [('endtag', 'a')]) ++ self._run_check("", [('starttag', 'a", [('endtag', 'a'", [('data', "'", []) ++ self._run_check("", [('starttag', 'a$b', [])]) + self._run_check("", [('startendtag', 'a$b', [])]) + self._run_check("", [('starttag', 'a$b', [])]) + self._run_check("", [('startendtag', 'a$b', [])]) ++ self._run_check("", [('endtag', 'a$b')]) + + def test_slashes_in_starttag(self): + self._run_check('', [('startendtag', 'a', [('foo', 'var')])]) +@@ -537,13 +545,56 @@ def test_EOF_in_charref(self): + for html, expected in data: + self._run_check(html, expected) + +- def test_broken_comments(self): +- html = ('' ++ def test_eof_in_comments(self): ++ data = [ ++ ('', [('comment', '-!>')]), ++ ('' + '' + '' + '') + expected = [ ++ ('comment', 'ELEMENT br EMPTY'), + ('comment', ' not really a comment '), + ('comment', ' not a comment either --'), + ('comment', ' -- close enough --'), +@@ -598,6 +649,26 @@ def test_convert_charrefs_dropped_text(self): + ('endtag', 'a'), ('data', ' bar & baz')] + ) + ++ @support.requires_resource('cpu') ++ def test_eof_no_quadratic_complexity(self): ++ # Each of these examples used to take about an hour. ++ # Now they take a fraction of a second. ++ def check(source): ++ parser = html.parser.HTMLParser() ++ parser.feed(source) ++ parser.close() ++ n = 120_000 ++ check(" +Date: Fri, 31 Oct 2025 18:15:08 +0100 +Subject: [PATCH] [3.11] gh-136065: Fix quadratic complexity in + os.path.expandvars() (GH-134952) (GH-140848) + +(cherry picked from commit f029e8db626ddc6e3a3beea4eff511a71aaceb5c) + +Co-authored-by: Serhiy Storchaka + +Origin: backport, https://github.com/python/cpython/commit/5dceb93486176e6b4a6d9754491005113eb23427 +--- + Lib/ntpath.py | 126 ++++++------------ + Lib/posixpath.py | 43 +++--- + Lib/test/test_genericpath.py | 14 ++ + Lib/test/test_ntpath.py | 22 ++- + ...-05-30-22-33-27.gh-issue-136065.bu337o.rst | 1 + + 5 files changed, 94 insertions(+), 112 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst + +diff --git a/Lib/ntpath.py b/Lib/ntpath.py +index ebc55eb891082e..8a71f29a32f287 100644 +--- a/Lib/ntpath.py ++++ b/Lib/ntpath.py +@@ -377,17 +377,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'}' +@@ -396,94 +402,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. +diff --git a/Lib/posixpath.py b/Lib/posixpath.py +index ce71a477b21928..8f300aea745170 100644 +--- a/Lib/posixpath.py ++++ b/Lib/posixpath.py +@@ -287,42 +287,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: +@@ -330,13 +329,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. +diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py +index 4f311c2d498e9f..ce501a94516544 100644 +--- a/Lib/test/test_genericpath.py ++++ b/Lib/test/test_genericpath.py +@@ -7,6 +7,7 @@ + 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 @@ def check(value, expected): + 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(): +diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py +index 7d0c0a095bc50a..a55d8022ee3666 100644 +--- a/Lib/test/test_ntpath.py ++++ b/Lib/test/test_ntpath.py +@@ -3,8 +3,8 @@ + import sys + import unittest + import warnings +-from test.support import 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 +@@ -54,7 +54,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 +@@ -70,7 +70,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))) + + +@@ -604,6 +604,19 @@ def check(value, expected): + 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') + +@@ -874,6 +887,7 @@ def test_nt_helpers(self): + self.assertIsInstance(b_final_path, bytes) + self.assertGreater(len(b_final_path), 0) + ++ + class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase): + pathmodule = ntpath + attributes = ['relpath'] +diff --git a/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst +new file mode 100644 +index 00000000000000..1d152bb5318380 +--- /dev/null ++++ b/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 -Nru python3.11-3.11.2/debian/patches/CVE-2025-8194.patch python3.11-3.11.2/debian/patches/CVE-2025-8194.patch --- python3.11-3.11.2/debian/patches/CVE-2025-8194.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-8194.patch 2026-03-26 05:43:16.000000000 +0000 @@ -0,0 +1,218 @@ +From b4ec17488eedec36d3c05fec127df71c0071f6cb Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Tue, 19 Aug 2025 20:00:46 +0200 +Subject: [PATCH] [3.11] gh-130577: tarfile now validates archives to ensure + member offsets are non-negative (GH-137027) (#137172) + +gh-130577: tarfile now validates archives to ensure member offsets are non-negative (GH-137027) +(cherry picked from commit 7040aa54f14676938970e10c5f74ea93cd56aa38) + +Co-authored-by: Alexander Urieles +Co-authored-by: Gregory P. Smith + +Origin: backport, https://github.com/python/cpython/commit/b4ec17488eedec36d3c05fec127df71c0071f6cb +--- + Lib/tarfile.py | 3 + + Lib/test/test_tarfile.py | 156 ++++++++++++++++++ + ...-07-23-00-35-29.gh-issue-130577.c7EITy.rst | 3 + + 3 files changed, 162 insertions(+) + create mode 100644 Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst + +diff --git a/Lib/tarfile.py b/Lib/tarfile.py +index 2423e14bc540d8..c04c576ea22d2d 100755 +--- a/Lib/tarfile.py ++++ b/Lib/tarfile.py +@@ -1427,6 +1427,9 @@ def _block(self, count): + """Round up a byte count by BLOCKSIZE and return it, + e.g. _block(834) => 1024. + """ ++ # Only non-negative offsets are allowed ++ if count < 0: ++ raise InvalidHeaderError("invalid offset") + blocks, remainder = divmod(count, BLOCKSIZE) + if remainder: + blocks += 1 +diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py +index 7377acdf398622..366aac781df1e7 100644 +--- a/Lib/test/test_tarfile.py ++++ b/Lib/test/test_tarfile.py +@@ -43,6 +43,7 @@ def sha256sum(data): + xzname = os.path.join(TEMPDIR, "testtar.tar.xz") + tmpname = os.path.join(TEMPDIR, "tmp.tar") + dotlessname = os.path.join(TEMPDIR, "testtar") ++SPACE = b" " + + sha256_regtype = ( + "e09e4bc8b3c9d9177e77256353b36c159f5f040531bbd4b024a8f9b9196c71ce" +@@ -2941,6 +2942,161 @@ def extractall(self, ar): + tarfl.extract, filename_1, TEMPDIR, False, True) + + ++class OffsetValidationTests(unittest.TestCase): ++ tarname = tmpname ++ invalid_posix_header = ( ++ # name: 100 bytes ++ tarfile.NUL * tarfile.LENGTH_NAME ++ # mode, space, null terminator: 8 bytes ++ + b"000755" + SPACE + tarfile.NUL ++ # uid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # gid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # size, space: 12 bytes ++ + b"\xff" * 11 + SPACE ++ # mtime, space: 12 bytes ++ + tarfile.NUL * 11 + SPACE ++ # chksum: 8 bytes ++ + b"0011407" + tarfile.NUL ++ # type: 1 byte ++ + tarfile.REGTYPE ++ # linkname: 100 bytes ++ + tarfile.NUL * tarfile.LENGTH_LINK ++ # magic: 6 bytes, version: 2 bytes ++ + tarfile.POSIX_MAGIC ++ # uname: 32 bytes ++ + tarfile.NUL * 32 ++ # gname: 32 bytes ++ + tarfile.NUL * 32 ++ # devmajor, space, null terminator: 8 bytes ++ + tarfile.NUL * 6 + SPACE + tarfile.NUL ++ # devminor, space, null terminator: 8 bytes ++ + tarfile.NUL * 6 + SPACE + tarfile.NUL ++ # prefix: 155 bytes ++ + tarfile.NUL * tarfile.LENGTH_PREFIX ++ # padding: 12 bytes ++ + tarfile.NUL * 12 ++ ) ++ invalid_gnu_header = ( ++ # name: 100 bytes ++ tarfile.NUL * tarfile.LENGTH_NAME ++ # mode, null terminator: 8 bytes ++ + b"0000755" + tarfile.NUL ++ # uid, null terminator: 8 bytes ++ + b"0000001" + tarfile.NUL ++ # gid, space, null terminator: 8 bytes ++ + b"0000001" + tarfile.NUL ++ # size, space: 12 bytes ++ + b"\xff" * 11 + SPACE ++ # mtime, space: 12 bytes ++ + tarfile.NUL * 11 + SPACE ++ # chksum: 8 bytes ++ + b"0011327" + tarfile.NUL ++ # type: 1 byte ++ + tarfile.REGTYPE ++ # linkname: 100 bytes ++ + tarfile.NUL * tarfile.LENGTH_LINK ++ # magic: 8 bytes ++ + tarfile.GNU_MAGIC ++ # uname: 32 bytes ++ + tarfile.NUL * 32 ++ # gname: 32 bytes ++ + tarfile.NUL * 32 ++ # devmajor, null terminator: 8 bytes ++ + tarfile.NUL * 8 ++ # devminor, null terminator: 8 bytes ++ + tarfile.NUL * 8 ++ # padding: 167 bytes ++ + tarfile.NUL * 167 ++ ) ++ invalid_v7_header = ( ++ # name: 100 bytes ++ tarfile.NUL * tarfile.LENGTH_NAME ++ # mode, space, null terminator: 8 bytes ++ + b"000755" + SPACE + tarfile.NUL ++ # uid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # gid, space, null terminator: 8 bytes ++ + b"000001" + SPACE + tarfile.NUL ++ # size, space: 12 bytes ++ + b"\xff" * 11 + SPACE ++ # mtime, space: 12 bytes ++ + tarfile.NUL * 11 + SPACE ++ # chksum: 8 bytes ++ + b"0010070" + tarfile.NUL ++ # type: 1 byte ++ + tarfile.REGTYPE ++ # linkname: 100 bytes ++ + tarfile.NUL * tarfile.LENGTH_LINK ++ # padding: 255 bytes ++ + tarfile.NUL * 255 ++ ) ++ valid_gnu_header = tarfile.TarInfo("filename").tobuf(tarfile.GNU_FORMAT) ++ data_block = b"\xff" * tarfile.BLOCKSIZE ++ ++ def _write_buffer(self, buffer): ++ with open(self.tarname, "wb") as f: ++ f.write(buffer) ++ ++ def _get_members(self, ignore_zeros=None): ++ with open(self.tarname, "rb") as f: ++ with tarfile.open( ++ mode="r", fileobj=f, ignore_zeros=ignore_zeros ++ ) as tar: ++ return tar.getmembers() ++ ++ def _assert_raises_read_error_exception(self): ++ with self.assertRaisesRegex( ++ tarfile.ReadError, "file could not be opened successfully" ++ ): ++ self._get_members() ++ ++ def test_invalid_offset_header_validations(self): ++ for tar_format, invalid_header in ( ++ ("posix", self.invalid_posix_header), ++ ("gnu", self.invalid_gnu_header), ++ ("v7", self.invalid_v7_header), ++ ): ++ with self.subTest(format=tar_format): ++ self._write_buffer(invalid_header) ++ self._assert_raises_read_error_exception() ++ ++ def test_early_stop_at_invalid_offset_header(self): ++ buffer = self.valid_gnu_header + self.invalid_gnu_header + self.valid_gnu_header ++ self._write_buffer(buffer) ++ members = self._get_members() ++ self.assertEqual(len(members), 1) ++ self.assertEqual(members[0].name, "filename") ++ self.assertEqual(members[0].offset, 0) ++ ++ def test_ignore_invalid_archive(self): ++ # 3 invalid headers with their respective data ++ buffer = (self.invalid_gnu_header + self.data_block) * 3 ++ self._write_buffer(buffer) ++ members = self._get_members(ignore_zeros=True) ++ self.assertEqual(len(members), 0) ++ ++ def test_ignore_invalid_offset_headers(self): ++ for first_block, second_block, expected_offset in ( ++ ( ++ (self.valid_gnu_header), ++ (self.invalid_gnu_header + self.data_block), ++ 0, ++ ), ++ ( ++ (self.invalid_gnu_header + self.data_block), ++ (self.valid_gnu_header), ++ 1024, ++ ), ++ ): ++ self._write_buffer(first_block + second_block) ++ members = self._get_members(ignore_zeros=True) ++ self.assertEqual(len(members), 1) ++ self.assertEqual(members[0].name, "filename") ++ self.assertEqual(members[0].offset, expected_offset) ++ ++ + def setUpModule(): + os_helper.unlink(TEMPDIR) + os.makedirs(TEMPDIR) +diff --git a/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst b/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst +new file mode 100644 +index 00000000000000..342cabbc865dc4 +--- /dev/null ++++ b/Misc/NEWS.d/next/Library/2025-07-23-00-35-29.gh-issue-130577.c7EITy.rst +@@ -0,0 +1,3 @@ ++:mod:`tarfile` now validates archives to ensure member offsets are ++non-negative. (Contributed by Alexander Enrique Urieles Nieto in ++:gh:`130577`.) diff -Nru python3.11-3.11.2/debian/patches/CVE-2025-8291.patch python3.11-3.11.2/debian/patches/CVE-2025-8291.patch --- python3.11-3.11.2/debian/patches/CVE-2025-8291.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2025-8291.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,312 @@ +From 1d29afb0d6218aa8fb5e1e4a6133a4778d89bb46 Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Wed, 8 Oct 2025 13:46:45 +0200 +Subject: [PATCH] [3.11] gh-139700: Check consistency of the zip64 end of + central directory record (GH-139702) (GH-139708) (GH-139713) + +(cherry picked from commit 333d4a6f4967d3ace91492a39ededbcf3faa76a6) + +Support records with "zip64 extensible data" if there are no bytes +prepended to the ZIP file. +(cherry picked from commit 162997bb70e067668c039700141770687bc8f267) + +Co-authored-by: Serhiy Storchaka + +Origin: upstream, https://github.com/python/cpython/commit/1d29afb0d6218aa8fb5e1e4a6133a4778d89bb46 +--- + Lib/test/test_zipfile.py | 82 ++++++++++++++++++- + Lib/zipfile.py | 51 +++++++----- + ...-10-07-19-31-34.gh-issue-139700.vNHU1O.rst | 3 + + 3 files changed, 113 insertions(+), 23 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst + +diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py +index 52831a7bd7ce00..a0bca62ab88ac3 100644 +--- a/Lib/test/test_zipfile.py ++++ b/Lib/test/test_zipfile.py +@@ -866,6 +866,8 @@ def make_zip64_file( + 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. + +@@ -919,6 +921,12 @@ def make_zip64_file( + + central_dir_size = struct.pack(' 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 +@@ -278,6 +294,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 + + +@@ -311,7 +328,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" +@@ -335,8 +352,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 +@@ -1375,9 +1391,6 @@ def _RealGetContents(self): + + # "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 +@@ -1977,7 +1990,7 @@ def _write_end_record(self): + " 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/Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst b/Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst +new file mode 100644 +index 00000000000000..a8e7a1f1878c6b +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst +@@ -0,0 +1,3 @@ ++Check consistency of the zip64 end of central directory record. Support ++records with "zip64 extensible data" if there are no bytes prepended to the ++ZIP file. diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-0672.patch python3.11-3.11.2/debian/patches/CVE-2026-0672.patch --- python3.11-3.11.2/debian/patches/CVE-2026-0672.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2026-0672.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,190 @@ +From b1869ff648bbee0717221d09e6deff46617f3e85 Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Sun, 25 Jan 2026 18:10:18 +0100 +Subject: [PATCH] [3.11] gh-143919: Reject control characters in http cookies + (#144092) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +gh-143919: Reject control characters in http cookies +(cherry picked from commit 95746b3a13a985787ef53b977129041971ed7f70) + +Co-authored-by: Seth Michael Larson +Co-authored-by: Bartosz Sławecki +Co-authored-by: sobolevn + +Origin: upstream, https://github.com/python/cpython/commit/b1869ff648bbee0717221d09e6deff46617f3e85 +--- + Doc/library/http.cookies.rst | 4 +- + Lib/http/cookies.py | 25 +++++++-- + Lib/test/test_http_cookies.py | 52 +++++++++++++++++-- + ...-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1 + + 4 files changed, 73 insertions(+), 9 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst + +diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst +index e91972fe621a48..e2abb31149ff10 100644 +--- a/Doc/library/http.cookies.rst ++++ b/Doc/library/http.cookies.rst +@@ -270,9 +270,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module. + Set-Cookie: chips=ahoy + Set-Cookie: vienna=finger + >>> C = cookies.SimpleCookie() +- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') ++ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";') + >>> print(C) +- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" ++ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;" + >>> C = cookies.SimpleCookie() + >>> C["oreo"] = "doublestuff" + >>> C["oreo"]["path"] = "/" +diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py +index 2c1f021d0abede..5cfa7a8072c7f7 100644 +--- a/Lib/http/cookies.py ++++ b/Lib/http/cookies.py +@@ -87,9 +87,9 @@ + such trickeries do not confuse it. + + >>> C = cookies.SimpleCookie() +- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') ++ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";') + >>> print(C) +- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" ++ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;" + + Each element of the Cookie also supports all of the RFC 2109 + Cookie attributes. Here's an example which sets the Path +@@ -170,6 +170,15 @@ class CookieError(Exception): + }) + + _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch ++_control_character_re = re.compile(r'[\x00-\x1F\x7F]') ++ ++ ++def _has_control_character(*val): ++ """Detects control characters within a value. ++ Supports any type, as header values can be any type. ++ """ ++ return any(_control_character_re.search(str(v)) for v in val) ++ + + def _quote(str): + r"""Quote a string for use in a cookie header. +@@ -292,12 +301,16 @@ def __setitem__(self, K, V): + K = K.lower() + if not K in self._reserved: + raise CookieError("Invalid attribute %r" % (K,)) ++ if _has_control_character(K, V): ++ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}") + dict.__setitem__(self, K, V) + + def setdefault(self, key, val=None): + 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 cookies %r %r" % (key, val,)) + return dict.setdefault(self, key, val) + + def __eq__(self, morsel): +@@ -333,6 +346,9 @@ def set(self, key, val, coded_val): + raise CookieError('Attempt to set a reserved key %r' % (key,)) + if not _is_legal_key(key): + raise CookieError('Illegal key %r' % (key,)) ++ if _has_control_character(key, val, coded_val): ++ raise CookieError( ++ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,)) + + # It's a good key, so save it. + self._key = key +@@ -484,7 +500,10 @@ def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"): + result = [] + items = sorted(self.items()) + for key, value in items: +- result.append(value.output(attrs, header)) ++ value_output = value.output(attrs, header) ++ if _has_control_character(value_output): ++ raise CookieError("Control characters are not allowed in cookies") ++ result.append(value_output) + return sep.join(result) + + __str__ = output +diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py +index 8879902a6e2f41..2438c57ef40458 100644 +--- a/Lib/test/test_http_cookies.py ++++ b/Lib/test/test_http_cookies.py +@@ -17,10 +17,10 @@ def test_basic(self): + 'repr': "", + 'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'}, + +- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"', +- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'}, +- 'repr': '''''', +- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'}, ++ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"', ++ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'}, ++ 'repr': '''''', ++ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'}, + + # Check illegal cookies that have an '=' char in an unquoted value + {'data': 'keebler=E=mc2', +@@ -517,6 +517,50 @@ def test_repr(self): + r'Set-Cookie: key=coded_val; ' + r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+') + ++ def test_control_characters(self): ++ for c0 in support.control_characters_c0(): ++ morsel = cookies.Morsel() ++ ++ # .__setitem__() ++ with self.assertRaises(cookies.CookieError): ++ morsel[c0] = "val" ++ with self.assertRaises(cookies.CookieError): ++ morsel["path"] = c0 ++ ++ # .setdefault() ++ with self.assertRaises(cookies.CookieError): ++ morsel.setdefault("path", c0) ++ with self.assertRaises(cookies.CookieError): ++ morsel.setdefault(c0, "val") ++ ++ # .set() ++ with self.assertRaises(cookies.CookieError): ++ morsel.set(c0, "val", "coded-value") ++ with self.assertRaises(cookies.CookieError): ++ morsel.set("path", c0, "coded-value") ++ with self.assertRaises(cookies.CookieError): ++ morsel.set("path", "val", c0) ++ ++ 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. ++ 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.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.output() ++ + + def load_tests(loader, tests, pattern): + tests.addTest(doctest.DocTestSuite(cookies)) +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst +new file mode 100644 +index 00000000000000..788c3e4ac2ebf7 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst +@@ -0,0 +1 @@ ++Reject control characters in :class:`http.cookies.Morsel` fields and values. diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-0865-1.patch python3.11-3.11.2/debian/patches/CVE-2026-0865-1.patch --- python3.11-3.11.2/debian/patches/CVE-2026-0865-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2026-0865-1.patch 2026-03-26 05:45:22.000000000 +0000 @@ -0,0 +1,98 @@ +From e4846a93ac07a8ae9aa18203af0dd13d6e7a6995 Mon Sep 17 00:00:00 2001 +From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> +Date: Tue, 20 Jan 2026 14:51:58 -0800 +Subject: [PATCH] [3.11] gh-143916: Reject control characters in + wsgiref.headers.Headers + +gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917) + +* Add 'test.support' fixture for C0 control characters +* gh-143916: Reject control characters in wsgiref.headers.Headers + +(cherry picked from commit f7fceed79ca1bceae8dbe5ba5bc8928564da7211) +(cherry picked from commit 22e4d55285cee52bc4dbe061324e5f30bd4dee58) + +Co-authored-by: Seth Michael Larson + +Origin: backport, https://github.com/python/cpython/commit/e4846a93ac07a8ae9aa18203af0dd13d6e7a6995 +--- + Lib/test/support/__init__.py | 7 +++++++ + Lib/test/test_wsgiref.py | 12 +++++++++++- + Lib/wsgiref/headers.py | 3 +++ + .../2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2 ++ + 4 files changed, 23 insertions(+), 1 deletion(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst + +diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py +index 79e3d1c642289d..9270f3f8d622b5 100644 +--- a/Lib/test/support/__init__.py ++++ b/Lib/test/support/__init__.py +@@ -2234,3 +2234,10 @@ def copy_python_src_ignore(path, names): + yield + finally: + sys.set_int_max_str_digits(current) ++ ++ ++def control_characters_c0() -> list[str]: ++ """Returns a list of C0 control characters as strings. ++ C0 control characters defined as the byte range 0x00-0x1F, and 0x7F. ++ """ ++ return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"] +diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py +index 9316d0ecbcf1ae..28e3656632828a 100644 +--- a/Lib/test/test_wsgiref.py ++++ b/Lib/test/test_wsgiref.py +@@ -1,6 +1,6 @@ + from unittest import mock + from test import support +-from test.support import socket_helper ++from test.support import socket_helper, control_characters_c0 + from test.test_httpservers import NoLogRequestHandler + from unittest import TestCase + from wsgiref.util import setup_testing_defaults +@@ -503,6 +503,16 @@ def testExtras(self): + '\r\n' + ) + ++ def testRaisesControlCharacters(self): ++ headers = Headers() ++ for c0 in control_characters_c0(): ++ self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") ++ self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") ++ self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") ++ self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") ++ self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") ++ ++ + class ErrorHandler(BaseCGIHandler): + """Simple handler subclass for testing BaseHandler""" + +diff --git a/Lib/wsgiref/headers.py b/Lib/wsgiref/headers.py +index fab851c5a44430..fd98e85d75492b 100644 +--- a/Lib/wsgiref/headers.py ++++ b/Lib/wsgiref/headers.py +@@ -9,6 +9,7 @@ + # existence of which force quoting of the parameter value. + import re + tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') ++_control_chars_re = re.compile(r'[\x00-\x1F\x7F]') + + def _formatparam(param, value=None, quote=1): + """Convenience function to format and return a key=value pair. +@@ -41,6 +42,8 @@ def __init__(self, headers=None): + def _convert_string_type(self, value): + """Convert/check value type.""" + if type(value) is str: ++ if _control_chars_re.search(value): ++ raise ValueError("Control characters not allowed in headers") + return value + raise AssertionError("Header names/values must be" + " of type str (got {0})".format(repr(value))) +diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst +new file mode 100644 +index 00000000000000..44bd0b27059f94 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst +@@ -0,0 +1,2 @@ ++Reject C0 control characters within wsgiref.headers.Headers fields, values, ++and parameters. diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-0865-2.patch python3.11-3.11.2/debian/patches/CVE-2026-0865-2.patch --- python3.11-3.11.2/debian/patches/CVE-2026-0865-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2026-0865-2.patch 2026-03-26 05:43:16.000000000 +0000 @@ -0,0 +1,149 @@ +From 286e3ac39984fe85a17f4ab39c64d382137aae5f Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Mon, 2 Mar 2026 23:59:25 +0100 +Subject: [PATCH] [3.11] gh-143916: Allow HTAB in wsgiref header values + (#145139) + +gh-143916: Allow HTAB in wsgiref header values +(cherry picked from commit 66da7bf6fe7b81e3ecc9c0a25bd47d4616c8d1a6) + +Co-authored-by: Seth Michael Larson +Co-authored-by: Victor Stinner + +Origin: upstream, https://github.com/python/cpython/commit/286e3ac39984fe85a17f4ab39c64d382137aae5f +--- + Lib/test/test_wsgiref.py | 20 +++++++++++++------- + Lib/wsgiref/headers.py | 35 ++++++++++++++++++++--------------- + 2 files changed, 33 insertions(+), 22 deletions(-) + +diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py +index 28e3656632828a..bb575bdaef62a2 100644 +--- a/Lib/test/test_wsgiref.py ++++ b/Lib/test/test_wsgiref.py +@@ -504,14 +504,20 @@ def testExtras(self): + ) + + def testRaisesControlCharacters(self): +- headers = Headers() + for c0 in control_characters_c0(): +- self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") +- self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") +- self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") +- self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") +- self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") +- ++ with self.subTest(c0): ++ headers = Headers() ++ self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") ++ self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") ++ # HTAB (\x09) is allowed in values, not names. ++ if c0 == "\t": ++ headers["key"] = f"val{c0}" ++ headers.add_header("key", f"val{c0}") ++ headers.setdefault(f"key", f"val{c0}") ++ else: ++ self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") ++ self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") ++ self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") + + class ErrorHandler(BaseCGIHandler): + """Simple handler subclass for testing BaseHandler""" +diff --git a/Lib/wsgiref/headers.py b/Lib/wsgiref/headers.py +index fd98e85d75492b..17559b0a37bd20 100644 +--- a/Lib/wsgiref/headers.py ++++ b/Lib/wsgiref/headers.py +@@ -9,7 +9,11 @@ + # existence of which force quoting of the parameter value. + import re + tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') +-_control_chars_re = re.compile(r'[\x00-\x1F\x7F]') ++# Disallowed characters for headers and values. ++# HTAB (\x09) is allowed in header values, but ++# not in header names. (RFC 9110 Section 5.5) ++_name_disallowed_re = re.compile(r'[\x00-\x1F\x7F]') ++_value_disallowed_re = re.compile(r'[\x00-\x08\x0A-\x1F\x7F]') + + def _formatparam(param, value=None, quote=1): + """Convenience function to format and return a key=value pair. +@@ -36,13 +40,14 @@ def __init__(self, headers=None): + self._headers = headers + if __debug__: + for k, v in headers: +- self._convert_string_type(k) +- self._convert_string_type(v) ++ self._convert_string_type(k, name=True) ++ self._convert_string_type(v, name=False) + +- def _convert_string_type(self, value): ++ def _convert_string_type(self, value, *, name): + """Convert/check value type.""" + if type(value) is str: +- if _control_chars_re.search(value): ++ regex = (_name_disallowed_re if name else _value_disallowed_re) ++ if regex.search(value): + raise ValueError("Control characters not allowed in headers") + return value + raise AssertionError("Header names/values must be" +@@ -56,14 +61,14 @@ def __setitem__(self, name, val): + """Set the value of a header.""" + del self[name] + self._headers.append( +- (self._convert_string_type(name), self._convert_string_type(val))) ++ (self._convert_string_type(name, name=True), self._convert_string_type(val, name=False))) + + def __delitem__(self,name): + """Delete all occurrences of a header, if present. + + Does *not* raise an exception if the header is missing. + """ +- name = self._convert_string_type(name.lower()) ++ name = self._convert_string_type(name.lower(), name=True) + self._headers[:] = [kv for kv in self._headers if kv[0].lower() != name] + + def __getitem__(self,name): +@@ -90,13 +95,13 @@ def get_all(self, name): + fields deleted and re-inserted are always appended to the header list. + If no fields exist with the given name, returns an empty list. + """ +- name = self._convert_string_type(name.lower()) ++ name = self._convert_string_type(name.lower(), name=True) + return [kv[1] for kv in self._headers if kv[0].lower()==name] + + + def get(self,name,default=None): + """Get the first header value for 'name', or return 'default'""" +- name = self._convert_string_type(name.lower()) ++ name = self._convert_string_type(name.lower(), name=True) + for k,v in self._headers: + if k.lower()==name: + return v +@@ -151,8 +156,8 @@ def setdefault(self,name,value): + and value 'value'.""" + result = self.get(name) + if result is None: +- self._headers.append((self._convert_string_type(name), +- self._convert_string_type(value))) ++ self._headers.append((self._convert_string_type(name, name=True), ++ self._convert_string_type(value, name=False))) + return value + else: + return result +@@ -175,13 +180,13 @@ def add_header(self, _name, _value, **_params): + """ + parts = [] + if _value is not None: +- _value = self._convert_string_type(_value) ++ _value = self._convert_string_type(_value, name=False) + parts.append(_value) + for k, v in _params.items(): +- k = self._convert_string_type(k) ++ k = self._convert_string_type(k, name=True) + if v is None: + parts.append(k.replace('_', '-')) + else: +- v = self._convert_string_type(v) ++ v = self._convert_string_type(v, name=False) + parts.append(_formatparam(k.replace('_', '-'), v)) +- self._headers.append((self._convert_string_type(_name), "; ".join(parts))) ++ self._headers.append((self._convert_string_type(_name, name=True), "; ".join(parts))) diff -Nru python3.11-3.11.2/debian/patches/CVE-2026-1299.patch python3.11-3.11.2/debian/patches/CVE-2026-1299.patch --- python3.11-3.11.2/debian/patches/CVE-2026-1299.patch 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/patches/CVE-2026-1299.patch 2026-03-26 03:37:28.000000000 +0000 @@ -0,0 +1,111 @@ +From 842ce19a0c0b58d61591e8f6a708c38db1fb94e4 Mon Sep 17 00:00:00 2001 +From: "Miss Islington (bot)" + <31488909+miss-islington@users.noreply.github.com> +Date: Sun, 25 Jan 2026 18:09:56 +0100 +Subject: [PATCH] [3.11] gh-144125: email: verify headers are sound in + BytesGenerator (#144189) + +gh-144125: email: verify headers are sound in BytesGenerator +(cherry picked from commit 052e55e7d44718fe46cbba0ca995cb8fcc359413) + +Co-authored-by: Seth Michael Larson +Co-authored-by: Denis Ledoux +Co-authored-by: Denis Ledoux <5822488+beledouxdenis@users.noreply.github.com> +Co-authored-by: Petr Viktorin <302922+encukou@users.noreply.github.com> +Co-authored-by: Bas Bloemsaat <1586868+basbloemsaat@users.noreply.github.com> + +Origin: upstream, https://github.com/python/cpython/commit/842ce19a0c0b58d61591e8f6a708c38db1fb94e4 +--- + Lib/email/generator.py | 12 +++++++++++- + Lib/test/test_email/test_generator.py | 4 +++- + Lib/test/test_email/test_policy.py | 6 +++++- + .../2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 ++++ + 4 files changed, 23 insertions(+), 3 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst + +diff --git a/Lib/email/generator.py b/Lib/email/generator.py +index 563ca170726943..61dc942b83f8fb 100644 +--- a/Lib/email/generator.py ++++ b/Lib/email/generator.py +@@ -22,6 +22,7 @@ + NLCRE = re.compile(r'\r\n|\r|\n') + fcre = re.compile(r'^From ', re.MULTILINE) + NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') ++NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') + + + +@@ -430,7 +431,16 @@ def _write_headers(self, msg): + # This is almost the same as the string version, except for handling + # strings with 8bit bytes. + for h, v in msg.raw_items(): +- self._fp.write(self.policy.fold_binary(h, v)) ++ folded = self.policy.fold_binary(h, v) ++ if self.policy.verify_generated_headers: ++ linesep = self.policy.linesep.encode() ++ if not folded.endswith(linesep): ++ raise HeaderWriteError( ++ f'folded header does not end with {linesep!r}: {folded!r}') ++ if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)): ++ raise HeaderWriteError( ++ f'folded header contains newline: {folded!r}') ++ self._fp.write(folded) + # A blank line always separates headers from body + self.write(self._NL) + +diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py +index d29400f0ed1dbb..a641f871dd7d67 100644 +--- a/Lib/test/test_email/test_generator.py ++++ b/Lib/test/test_email/test_generator.py +@@ -264,7 +264,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase): + typ = str + + def test_verify_generated_headers(self): +- """gh-121650: by default the generator prevents header injection""" ++ # gh-121650: by default the generator prevents header injection + class LiteralHeader(str): + name = 'Header' + def fold(self, **kwargs): +@@ -285,6 +285,8 @@ def fold(self, **kwargs): + + with self.assertRaises(email.errors.HeaderWriteError): + message.as_string() ++ with self.assertRaises(email.errors.HeaderWriteError): ++ message.as_bytes() + + + class TestBytesGenerator(TestGeneratorBase, TestEmailBase): +diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py +index baa35fd68e49c5..71ec0febb0fd86 100644 +--- a/Lib/test/test_email/test_policy.py ++++ b/Lib/test/test_email/test_policy.py +@@ -279,7 +279,7 @@ def test_short_maxlen_error(self): + policy.fold("Subject", subject) + + def test_verify_generated_headers(self): +- """Turning protection off allows header injection""" ++ # Turning protection off allows header injection + policy = email.policy.default.clone(verify_generated_headers=False) + for text in ( + 'Header: Value\r\nBad: Injection\r\n', +@@ -302,6 +302,10 @@ def fold(self, **kwargs): + message.as_string(), + f"{text}\nBody", + ) ++ self.assertEqual( ++ message.as_bytes(), ++ f"{text}\nBody".encode(), ++ ) + + # XXX: Need subclassing tests. + # For adding subclassed objects, make sure the usual rules apply (subclass +diff --git a/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst +new file mode 100644 +index 00000000000000..e6333e724972c5 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst +@@ -0,0 +1,4 @@ ++:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers ++that are unsafely folded or delimited; see ++:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas ++Bloemsaat and Petr Viktorin in :gh:`121650`). 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 2025-04-28 14:11:48.000000000 +0000 +++ python3.11-3.11.2/debian/patches/series 2026-04-07 10:42:43.000000000 +0000 @@ -1,4 +1,3 @@ -# git-updates.diff deb-setup.diff deb-locations.diff distutils-install-layout.diff @@ -15,8 +14,6 @@ disable-sem-check.diff lib-argparse.diff ctypes-arm.diff -# link-timemodule.diff -#lto-link-flags.diff multiarch.diff lib2to3-no-pickled-grammar.diff ext-no-libpython-link.diff @@ -30,7 +27,6 @@ pydoc-use-pager.diff local-doc-references.diff doc-build-texinfo.diff -#build-math-object.diff argparse-no-shutil.diff sysconfigdata-name.diff hurd_kfreebsd_thread_native_id.diff @@ -61,3 +57,18 @@ 0001-3.11-gh-105704-Disallow-square-brackets-and-in-domai.patch 0002-3.11-gh-100884-email-_header_value_parser-don-t-enco.patch 0003-3.11-gh-118643-Fix-AttributeError-in-the-email-modul.patch +CVE-2025-4516-1.patch +CVE-2025-4516-2.patch +CVE-2025-6069.patch +CVE-2025-6075.patch +CVE-2025-8194.patch +CVE-2025-8291.patch +CVE-2025-12084.patch +CVE-2025-13836.patch +CVE-2025-13837.patch +CVE-2026-0865-1.patch +CVE-2026-0865-2.patch +CVE-2026-0672.patch +CVE-2025-15282.patch +CVE-2025-11468.patch +CVE-2026-1299.patch diff -Nru python3.11-3.11.2/debian/salsa-ci.yml python3.11-3.11.2/debian/salsa-ci.yml --- python3.11-3.11.2/debian/salsa-ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ python3.11-3.11.2/debian/salsa-ci.yml 2026-03-16 06:54:10.000000000 +0000 @@ -0,0 +1,37 @@ +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml + +variables: + RELEASE: 'bookworm' + +build armhf: + allow_failure: true + +blhc: + allow_failure: true + +lintian: + allow_failure: true + +.package-source: &package-source + before_script: + - echo 'deb-src http://deb.debian.org/debian bookworm main' > /etc/apt/sources.list.d/src.list + - apt-get -y update + timeout: 3h + +build: + <<: *package-source + +build i386: + <<: *package-source + +build source: + <<: *package-source + +test-build-all: + <<: *package-source + allow_failure: true + +test-build-any: + <<: *package-source + allow_failure: true