diff --git a/backport-CVE-2025-12084.patch b/backport-CVE-2025-12084.patch new file mode 100644 index 0000000000000000000000000000000000000000..f85d325e1dbc32d6ab77a8cf4091eccbff16e28c --- /dev/null +++ b/backport-CVE-2025-12084.patch @@ -0,0 +1,94 @@ +From b95c10349956d95e258553def0fcc52ea3ef8f82 Mon Sep 17 00:00:00 2001 +From: Seth Michael Larson +Date: Wed, 3 Dec 2025 01:16:37 -0600 +Subject: [PATCH] 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> +--- + Lib/test/test_minidom.py | 18 ++++++++++++++++++ + Lib/xml/dom/minidom.py | 9 +-------- + ...5-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 1 + + 3 files changed, 20 insertions(+), 8 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..82b32bcad693ad 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 +@@ -177,6 +178,23 @@ def testAppendChild(self): + self.confirm(dom.documentElement.childNodes[-1].data == "Hello") + dom.unlink() + ++ 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.time() ++ for child in children: ++ element.appendChild(child) ++ element = child ++ end = time.time() ++ ++ # This example used to take at least 30 seconds. ++ self.assertLess(end - start, 1) ++ + 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..83f717eeb5d043 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." +@@ -1539,7 +1532,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..440bc7794c69ef +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst +@@ -0,0 +1 @@ ++Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. diff --git a/backport-CVE-2025-13836.patch b/backport-CVE-2025-13836.patch new file mode 100644 index 0000000000000000000000000000000000000000..16eb57dba0b0dd15041e29da0c8431a4ed9f3226 --- /dev/null +++ b/backport-CVE-2025-13836.patch @@ -0,0 +1,152 @@ +From 5a4c4a033a4a54481be6870aa1896fad732555b5 Mon Sep 17 00:00:00 2001 +From: Serhiy Storchaka +Date: Mon, 1 Dec 2025 17:26:07 +0200 +Subject: [PATCH] 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. +--- + 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 4b9a61cfc1159f..73c3256734a64f 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 +@@ -642,10 +647,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 47e3914d1dd62e..44044d0385c72e 100644 +--- a/Lib/test/test_httplib.py ++++ b/Lib/test/test_httplib.py +@@ -1511,6 +1511,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 --git a/backport-CVE-2025-13837.patch b/backport-CVE-2025-13837.patch new file mode 100644 index 0000000000000000000000000000000000000000..41fa435f8d95f1315fc6738ca1f3ab852c5dc4af --- /dev/null +++ b/backport-CVE-2025-13837.patch @@ -0,0 +1,160 @@ +From aa9edbb11a2bf7805fd5046cdd5c2d3864aa39f2 Mon Sep 17 00:00:00 2001 +From: Serhiy Storchaka +Date: Mon, 1 Dec 2025 17:28:15 +0200 +Subject: [PATCH] [3.11] gh-119342: Fix a potential denial of service in + plistlib (GH-119343) + +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) + +Co-authored-by: Serhiy Storchaka +--- + 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 +@@ -73,6 +73,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): +@@ -499,12 +502,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)) +@@ -561,22 +576,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 +@@ -963,6 +966,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 --git a/python3.spec b/python3.spec index fbb249db98c9f3ae285f7260627a4a23479e8d71..f02587336634a5c153339809c087ac7b98a932f6 100644 --- a/python3.spec +++ b/python3.spec @@ -6,7 +6,7 @@ Summary: Interpreter of the Python3 programming language URL: https://www.python.org/ Version: 3.11.13 -Release: 5 +Release: 6 License: Python-2.0 %global branchversion 3.11 @@ -99,6 +99,10 @@ Patch6000: backport-CVE-2025-8194.patch Patch6001: backport-CVE-2025-6069.patch Patch6002: backport-CVE-2025-8291.patch Patch6003: backport-CVE-2025-6075.patch +Patch6004: backport-CVE-2025-13836.patch +Patch6005: backport-CVE-2025-13837.patch +Patch6006: backport-CVE-2025-12084.patch + Patch9000: add-the-sm3-method-for-obtaining-the-salt-value.patch Patch9001: 0001-add-loongarch64-support-for-python.patch @@ -858,6 +862,12 @@ export BEP_GTDLIST="$BEP_GTDLIST_TMP" %{_mandir}/*/* %changelog +* Thu Dec 04 2025 lipengyu - 3.11.13-6 +- Type:CVE +- CVE:CVE-2025-13836 CVE-2025-13837 CVE-2025-12084 +- SUG:NA +- DESC:fix CVE-2025-13836 CVE-2025-13837 CVE-2025-12084 + * Wed Nov 05 2025 lipengyu - 3.11.13-5 - Type:CVE - CVE:CVE-2025-6075