import CS git python3.12-3.12.13-2.el8_10
This commit is contained in:
parent
339712fca7
commit
bb8e1876d4
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
||||
SOURCES/Python-3.12.12.tar.xz
|
||||
SOURCES/Python-3.12.13.tar.xz
|
||||
|
||||
@ -1 +1 @@
|
||||
dbe6dc34a132b1035c121583a9f37ba87458d0f5 SOURCES/Python-3.12.12.tar.xz
|
||||
ad3e9c333d91bee73f1d5f4a6fe6e88f2e74d911 SOURCES/Python-3.12.13.tar.xz
|
||||
|
||||
@ -1,140 +0,0 @@
|
||||
From 9c9dda6625a2a90d2a06c657eee021d6be19842d Mon Sep 17 00:00:00 2001
|
||||
From: "Miss Islington (bot)"
|
||||
<31488909+miss-islington@users.noreply.github.com>
|
||||
Date: Mon, 22 Dec 2025 14:48:49 +0100
|
||||
Subject: [PATCH] [3.12] gh-142145: Remove quadratic behavior in node ID cache
|
||||
clearing (GH-142146) (#142211)
|
||||
|
||||
* gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146)
|
||||
* gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794)
|
||||
(cherry picked from commit 1cc7551b3f9f71efbc88d96dce90f82de98b2454)
|
||||
(cherry picked from commit 08d8e18ad81cd45bc4a27d6da478b51ea49486e4)
|
||||
(cherry picked from commit 8d2d7bb2e754f8649a68ce4116271a4932f76907)
|
||||
|
||||
Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com>
|
||||
Co-authored-by: Seth Michael Larson <seth@python.org>
|
||||
Co-authored-by: Petr Viktorin <encukou@gmail.com>
|
||||
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: 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 <greg@krypto.org>
|
||||
---
|
||||
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 699265ccadc7f9..ab4823c8315e57 100644
|
||||
--- a/Lib/test/test_minidom.py
|
||||
+++ b/Lib/test/test_minidom.py
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import copy
|
||||
import pickle
|
||||
+import time
|
||||
import io
|
||||
from test import support
|
||||
import unittest
|
||||
|
||||
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
|
||||
|
||||
@@ -176,6 +177,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.
|
||||
@ -1,159 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: "Miss Islington (bot)"
|
||||
<31488909+miss-islington@users.noreply.github.com>
|
||||
Date: Mon, 22 Dec 2025 14:50:18 +0100
|
||||
Subject: 00472: CVE-2025-13836
|
||||
|
||||
[3.12] gh-119451: Fix a potential denial of service in http.client (GH-119454) (#142140)
|
||||
|
||||
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 <storchaka@gmail.com>
|
||||
---
|
||||
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 fb29923d94..70451d67d4 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
|
||||
@@ -639,10 +644,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 01f5a10190..e46dac0077 100644
|
||||
--- a/Lib/test/test_httplib.py
|
||||
+++ b/Lib/test/test_httplib.py
|
||||
@@ -1452,6 +1452,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 0000000000..6d6f25cd2f
|
||||
--- /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.
|
||||
@ -1,90 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Seth Michael Larson <seth@python.org>
|
||||
Date: Sat, 17 Jan 2026 11:46:21 -0600
|
||||
Subject: 00473: CVE-2026-0865
|
||||
|
||||
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
|
||||
---
|
||||
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 4c42234ccc..26c0af4b13 100644
|
||||
--- a/Lib/test/support/__init__.py
|
||||
+++ b/Lib/test/support/__init__.py
|
||||
@@ -2599,3 +2599,10 @@ def __iter__(self):
|
||||
if self.iter_raises:
|
||||
1/0
|
||||
return self
|
||||
+
|
||||
+
|
||||
+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 9316d0ecbc..28e3656632 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 fab851c5a4..fd98e85d75 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 0000000000..44bd0b2705
|
||||
--- /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.
|
||||
@ -1,110 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: "Miss Islington (bot)"
|
||||
<31488909+miss-islington@users.noreply.github.com>
|
||||
Date: Fri, 13 Feb 2026 17:04:54 +0100
|
||||
Subject: [PATCH] [3.12] gh-144125: email: verify headers are sound in
|
||||
BytesGenerator
|
||||
|
||||
gh-144125: email: verify headers are sound in BytesGenerator
|
||||
(cherry picked from commit 052e55e7d44718fe46cbba0ca995cb8fcc359413)
|
||||
|
||||
Co-authored-by: Seth Michael Larson <seth@python.org>
|
||||
Co-authored-by: Denis Ledoux <dle@odoo.com>
|
||||
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>
|
||||
Co-authored-by: Petr Viktorin <encukou@gmail.com>
|
||||
---
|
||||
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 47b9df8f4e6090..8cbc43ef5bc647 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]')
|
||||
|
||||
|
||||
class Generator:
|
||||
@@ -429,7 +430,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 c75a842c33578e..3ca79edf6a65d9 100644
|
||||
--- a/Lib/test/test_email/test_generator.py
|
||||
+++ b/Lib/test/test_email/test_generator.py
|
||||
@@ -313,7 +313,7 @@ def test_flatten_unicode_linesep(self):
|
||||
self.assertEqual(s.getvalue(), self.typ(expected))
|
||||
|
||||
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):
|
||||
@@ -334,6 +334,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
|
||||
@@ -296,7 +296,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',
|
||||
@@ -319,6 +319,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`).
|
||||
107
SOURCES/00479-cve-2026-1502.patch
Normal file
107
SOURCES/00479-cve-2026-1502.patch
Normal file
@ -0,0 +1,107 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Seth Larson <seth@python.org>
|
||||
Date: Fri, 10 Apr 2026 10:21:42 -0500
|
||||
Subject: 00479: CVE-2026-1502
|
||||
|
||||
Reject CR/LF in HTTP tunnel request headers
|
||||
|
||||
Co-authored-by: Illia Volochii <illia.volochii@gmail.com>
|
||||
---
|
||||
Lib/http/client.py | 11 ++++-
|
||||
Lib/test/test_httplib.py | 45 +++++++++++++++++++
|
||||
...-03-20-09-29-42.gh-issue-146211.PQVbs7.rst | 2 +
|
||||
3 files changed, 57 insertions(+), 1 deletion(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst
|
||||
|
||||
diff --git a/Lib/http/client.py b/Lib/http/client.py
|
||||
index 70451d67d4..7db4807b30 100644
|
||||
--- a/Lib/http/client.py
|
||||
+++ b/Lib/http/client.py
|
||||
@@ -972,13 +972,22 @@ def _wrap_ipv6(self, ip):
|
||||
return ip
|
||||
|
||||
def _tunnel(self):
|
||||
+ if _contains_disallowed_url_pchar_re.search(self._tunnel_host):
|
||||
+ raise ValueError('Tunnel host can\'t contain control characters %r'
|
||||
+ % (self._tunnel_host,))
|
||||
connect = b"CONNECT %s:%d %s\r\n" % (
|
||||
self._wrap_ipv6(self._tunnel_host.encode("idna")),
|
||||
self._tunnel_port,
|
||||
self._http_vsn_str.encode("ascii"))
|
||||
headers = [connect]
|
||||
for header, value in self._tunnel_headers.items():
|
||||
- headers.append(f"{header}: {value}\r\n".encode("latin-1"))
|
||||
+ header_bytes = header.encode("latin-1")
|
||||
+ value_bytes = value.encode("latin-1")
|
||||
+ if not _is_legal_header_name(header_bytes):
|
||||
+ raise ValueError('Invalid header name %r' % (header_bytes,))
|
||||
+ if _is_illegal_header_value(value_bytes):
|
||||
+ raise ValueError('Invalid header value %r' % (value_bytes,))
|
||||
+ headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes))
|
||||
headers.append(b"\r\n")
|
||||
# Making a single send() call instead of one per line encourages
|
||||
# the host OS to use a more optimal packet size instead of
|
||||
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
|
||||
index e46dac0077..e027d930d9 100644
|
||||
--- a/Lib/test/test_httplib.py
|
||||
+++ b/Lib/test/test_httplib.py
|
||||
@@ -369,6 +369,51 @@ def test_invalid_headers(self):
|
||||
with self.assertRaisesRegex(ValueError, 'Invalid header'):
|
||||
conn.putheader(name, value)
|
||||
|
||||
+ def test_invalid_tunnel_headers(self):
|
||||
+ cases = (
|
||||
+ ('Invalid\r\nName', 'ValidValue'),
|
||||
+ ('Invalid\rName', 'ValidValue'),
|
||||
+ ('Invalid\nName', 'ValidValue'),
|
||||
+ ('\r\nInvalidName', 'ValidValue'),
|
||||
+ ('\rInvalidName', 'ValidValue'),
|
||||
+ ('\nInvalidName', 'ValidValue'),
|
||||
+ (' InvalidName', 'ValidValue'),
|
||||
+ ('\tInvalidName', 'ValidValue'),
|
||||
+ ('Invalid:Name', 'ValidValue'),
|
||||
+ (':InvalidName', 'ValidValue'),
|
||||
+ ('ValidName', 'Invalid\r\nValue'),
|
||||
+ ('ValidName', 'Invalid\rValue'),
|
||||
+ ('ValidName', 'Invalid\nValue'),
|
||||
+ ('ValidName', 'InvalidValue\r\n'),
|
||||
+ ('ValidName', 'InvalidValue\r'),
|
||||
+ ('ValidName', 'InvalidValue\n'),
|
||||
+ )
|
||||
+ for name, value in cases:
|
||||
+ with self.subTest((name, value)):
|
||||
+ conn = client.HTTPConnection('example.com')
|
||||
+ conn.set_tunnel('tunnel', headers={
|
||||
+ name: value
|
||||
+ })
|
||||
+ conn.sock = FakeSocket('')
|
||||
+ with self.assertRaisesRegex(ValueError, 'Invalid header'):
|
||||
+ conn._tunnel() # Called in .connect()
|
||||
+
|
||||
+ def test_invalid_tunnel_host(self):
|
||||
+ cases = (
|
||||
+ 'invalid\r.host',
|
||||
+ '\ninvalid.host',
|
||||
+ 'invalid.host\r\n',
|
||||
+ 'invalid.host\x00',
|
||||
+ 'invalid host',
|
||||
+ )
|
||||
+ for tunnel_host in cases:
|
||||
+ with self.subTest(tunnel_host):
|
||||
+ conn = client.HTTPConnection('example.com')
|
||||
+ conn.set_tunnel(tunnel_host)
|
||||
+ conn.sock = FakeSocket('')
|
||||
+ with self.assertRaisesRegex(ValueError, 'Tunnel host can\'t contain control characters'):
|
||||
+ conn._tunnel() # Called in .connect()
|
||||
+
|
||||
def test_headers_debuglevel(self):
|
||||
body = (
|
||||
b'HTTP/1.1 200 OK\r\n'
|
||||
diff --git a/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst b/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst
|
||||
new file mode 100644
|
||||
index 0000000000..4993633b8e
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+Reject CR/LF characters in tunnel request headers for the
|
||||
+HTTPConnection.set_tunnel() method.
|
||||
64
SOURCES/00480-cve-2026-4786.patch
Normal file
64
SOURCES/00480-cve-2026-4786.patch
Normal file
@ -0,0 +1,64 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Stan Ulbrych <stan@python.org>
|
||||
Date: Mon, 13 Apr 2026 20:02:52 +0100
|
||||
Subject: 00480: CVE-2026-4786
|
||||
|
||||
Fix webbrowser `%action` substitution bypass of dash-prefix check
|
||||
---
|
||||
Lib/test/test_webbrowser.py | 9 +++++++++
|
||||
Lib/webbrowser.py | 5 +++--
|
||||
.../2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst | 2 ++
|
||||
3 files changed, 14 insertions(+), 2 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst
|
||||
|
||||
diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py
|
||||
index 60f094fd6a..e900c0212b 100644
|
||||
--- a/Lib/test/test_webbrowser.py
|
||||
+++ b/Lib/test/test_webbrowser.py
|
||||
@@ -99,6 +99,15 @@ def test_open_new_tab(self):
|
||||
options=[],
|
||||
arguments=[URL])
|
||||
|
||||
+ def test_reject_action_dash_prefixes(self):
|
||||
+ browser = self.browser_class(name=CMD_NAME)
|
||||
+ with self.assertRaises(ValueError):
|
||||
+ browser.open('%action--incognito')
|
||||
+ # new=1: action is "--new-window", so "%action" itself expands to
|
||||
+ # a dash-prefixed flag even with no dash in the original URL.
|
||||
+ with self.assertRaises(ValueError):
|
||||
+ browser.open('%action', new=1)
|
||||
+
|
||||
|
||||
class EdgeCommandTest(CommandTestMixin, unittest.TestCase):
|
||||
|
||||
diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py
|
||||
index 0bdb644d7d..79d410bcae 100755
|
||||
--- a/Lib/webbrowser.py
|
||||
+++ b/Lib/webbrowser.py
|
||||
@@ -268,7 +268,6 @@ def _invoke(self, args, remote, autoraise, url=None):
|
||||
|
||||
def open(self, url, new=0, autoraise=True):
|
||||
sys.audit("webbrowser.open", url)
|
||||
- self._check_url(url)
|
||||
if new == 0:
|
||||
action = self.remote_action
|
||||
elif new == 1:
|
||||
@@ -282,7 +281,9 @@ def open(self, url, new=0, autoraise=True):
|
||||
raise Error("Bad 'new' parameter to open(); " +
|
||||
"expected 0, 1, or 2, got %s" % new)
|
||||
|
||||
- args = [arg.replace("%s", url).replace("%action", action)
|
||||
+ self._check_url(url.replace("%action", action))
|
||||
+
|
||||
+ args = [arg.replace("%action", action).replace("%s", url)
|
||||
for arg in self.remote_args]
|
||||
args = [arg for arg in args if arg]
|
||||
success = self._invoke(args, True, autoraise, url)
|
||||
diff --git a/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst
|
||||
new file mode 100644
|
||||
index 0000000000..45cdeebe1b
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+A bypass in :mod:`webbrowser` allowed URLs prefixed with ``%action`` to pass
|
||||
+the dash-prefix safety check.
|
||||
61
SOURCES/00482-cve-2026-6100.patch
Normal file
61
SOURCES/00482-cve-2026-6100.patch
Normal file
@ -0,0 +1,61 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Stan Ulbrych <stan@python.org>
|
||||
Date: Mon, 13 Apr 2026 02:14:54 +0100
|
||||
Subject: 00482: CVE-2026-6100
|
||||
|
||||
Fix a possible UAF in {LZMA,BZ2,_Zlib}Decompressor
|
||||
---
|
||||
.../Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst | 5 +++++
|
||||
Modules/_bz2module.c | 1 +
|
||||
Modules/_lzmamodule.c | 1 +
|
||||
Modules/zlibmodule.c | 1 +
|
||||
4 files changed, 8 insertions(+)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst
|
||||
|
||||
diff --git a/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst
|
||||
new file mode 100644
|
||||
index 0000000000..9502189ab1
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst
|
||||
@@ -0,0 +1,5 @@
|
||||
+Fix a dangling input pointer in :class:`lzma.LZMADecompressor`,
|
||||
+:class:`bz2.BZ2Decompressor`, and internal :class:`!zlib._ZlibDecompressor`
|
||||
+when memory allocation fails with :exc:`MemoryError`, which could let a
|
||||
+subsequent :meth:`!decompress` call read or write through a stale pointer to
|
||||
+the already-released caller buffer.
|
||||
diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c
|
||||
index 97bd44b4ac..a732e89d55 100644
|
||||
--- a/Modules/_bz2module.c
|
||||
+++ b/Modules/_bz2module.c
|
||||
@@ -587,6 +587,7 @@ decompress(BZ2Decompressor *d, char *data, size_t len, Py_ssize_t max_length)
|
||||
return result;
|
||||
|
||||
error:
|
||||
+ bzs->next_in = NULL;
|
||||
Py_XDECREF(result);
|
||||
return NULL;
|
||||
}
|
||||
diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c
|
||||
index 7bbd6569aa..103a6ef86c 100644
|
||||
--- a/Modules/_lzmamodule.c
|
||||
+++ b/Modules/_lzmamodule.c
|
||||
@@ -1114,6 +1114,7 @@ decompress(Decompressor *d, uint8_t *data, size_t len, Py_ssize_t max_length)
|
||||
return result;
|
||||
|
||||
error:
|
||||
+ lzs->next_in = NULL;
|
||||
Py_XDECREF(result);
|
||||
return NULL;
|
||||
}
|
||||
diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c
|
||||
index f94c57e4c8..9759593b6a 100644
|
||||
--- a/Modules/zlibmodule.c
|
||||
+++ b/Modules/zlibmodule.c
|
||||
@@ -1645,6 +1645,7 @@ decompress(ZlibDecompressor *self, uint8_t *data,
|
||||
return result;
|
||||
|
||||
error:
|
||||
+ self->zst.next_in = NULL;
|
||||
Py_XDECREF(result);
|
||||
return NULL;
|
||||
}
|
||||
33
SOURCES/00483-cve-2026-2297.patch
Normal file
33
SOURCES/00483-cve-2026-2297.patch
Normal file
@ -0,0 +1,33 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Steve Dower <steve.dower@python.org>
|
||||
Date: Wed, 4 Mar 2026 19:55:52 +0000
|
||||
Subject: 00483: CVE-2026-2297
|
||||
|
||||
Logging Bypass in Legacy .pyc File Handling
|
||||
---
|
||||
Lib/importlib/_bootstrap_external.py | 2 +-
|
||||
.../Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst | 2 ++
|
||||
2 files changed, 3 insertions(+), 1 deletion(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst
|
||||
|
||||
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
|
||||
index 9b8a8dfc5a..6e4a087a10 100644
|
||||
--- a/Lib/importlib/_bootstrap_external.py
|
||||
+++ b/Lib/importlib/_bootstrap_external.py
|
||||
@@ -1186,7 +1186,7 @@ def get_filename(self, fullname):
|
||||
|
||||
def get_data(self, path):
|
||||
"""Return the data from path as raw bytes."""
|
||||
- if isinstance(self, (SourceLoader, ExtensionFileLoader)):
|
||||
+ if isinstance(self, (SourceLoader, SourcelessFileLoader, ExtensionFileLoader)):
|
||||
with _io.open_code(str(path)) as file:
|
||||
return file.read()
|
||||
else:
|
||||
diff --git a/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst b/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst
|
||||
new file mode 100644
|
||||
index 0000000000..dcdb44d4fa
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+Fixes :cve:`2026-2297` by ensuring that ``SourcelessFileLoader`` uses
|
||||
+:func:`io.open_code` when opening ``.pyc`` files.
|
||||
146
SOURCES/00484-cve-2026-3644.patch
Normal file
146
SOURCES/00484-cve-2026-3644.patch
Normal file
@ -0,0 +1,146 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
|
||||
Date: Mon, 16 Mar 2026 13:43:43 +0000
|
||||
Subject: 00484: CVE-2026-3644
|
||||
|
||||
Incomplete control character validation in http.cookies
|
||||
|
||||
Co-authored-by: Victor Stinner <victor.stinner@gmail.com>
|
||||
---
|
||||
Lib/http/cookies.py | 24 ++++++++++--
|
||||
Lib/test/test_http_cookies.py | 38 +++++++++++++++++++
|
||||
...-03-06-17-03-38.gh-issue-145599.kchwZV.rst | 4 ++
|
||||
3 files changed, 62 insertions(+), 4 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst
|
||||
|
||||
diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
|
||||
index d0a69cbe19..63d119ad46 100644
|
||||
--- a/Lib/http/cookies.py
|
||||
+++ b/Lib/http/cookies.py
|
||||
@@ -335,9 +335,16 @@ def update(self, values):
|
||||
key = key.lower()
|
||||
if key not in self._reserved:
|
||||
raise CookieError("Invalid attribute %r" % (key,))
|
||||
+ if _has_control_character(key, val):
|
||||
+ raise CookieError("Control characters are not allowed in "
|
||||
+ f"cookies {key!r} {val!r}")
|
||||
data[key] = val
|
||||
dict.update(self, data)
|
||||
|
||||
+ def __ior__(self, values):
|
||||
+ self.update(values)
|
||||
+ return self
|
||||
+
|
||||
def isReservedKey(self, K):
|
||||
return K.lower() in self._reserved
|
||||
|
||||
@@ -363,9 +370,15 @@ def __getstate__(self):
|
||||
}
|
||||
|
||||
def __setstate__(self, state):
|
||||
- self._key = state['key']
|
||||
- self._value = state['value']
|
||||
- self._coded_value = state['coded_value']
|
||||
+ key = state['key']
|
||||
+ value = state['value']
|
||||
+ coded_value = state['coded_value']
|
||||
+ if _has_control_character(key, value, coded_value):
|
||||
+ raise CookieError("Control characters are not allowed in cookies "
|
||||
+ f"{key!r} {value!r} {coded_value!r}")
|
||||
+ self._key = key
|
||||
+ self._value = value
|
||||
+ self._coded_value = coded_value
|
||||
|
||||
def output(self, attrs=None, header="Set-Cookie:"):
|
||||
return "%s %s" % (header, self.OutputString(attrs))
|
||||
@@ -377,13 +390,16 @@ def __repr__(self):
|
||||
|
||||
def js_output(self, attrs=None):
|
||||
# Print javascript
|
||||
+ output_string = self.OutputString(attrs)
|
||||
+ if _has_control_character(output_string):
|
||||
+ raise CookieError("Control characters are not allowed in cookies")
|
||||
return """
|
||||
<script type="text/javascript">
|
||||
<!-- begin hiding
|
||||
document.cookie = \"%s\";
|
||||
// end hiding -->
|
||||
</script>
|
||||
- """ % (self.OutputString(attrs).replace('"', r'\"'))
|
||||
+ """ % (output_string.replace('"', r'\"'))
|
||||
|
||||
def OutputString(self, attrs=None):
|
||||
# Build up our result
|
||||
diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
|
||||
index f196bcc48e..2478a6c630 100644
|
||||
--- a/Lib/test/test_http_cookies.py
|
||||
+++ b/Lib/test/test_http_cookies.py
|
||||
@@ -573,6 +573,14 @@ def test_control_characters(self):
|
||||
with self.assertRaises(cookies.CookieError):
|
||||
morsel["path"] = c0
|
||||
|
||||
+ # .__setstate__()
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ morsel.__setstate__({'key': c0, 'value': 'val', 'coded_value': 'coded'})
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ morsel.__setstate__({'key': 'key', 'value': c0, 'coded_value': 'coded'})
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ morsel.__setstate__({'key': 'key', 'value': 'val', 'coded_value': c0})
|
||||
+
|
||||
# .setdefault()
|
||||
with self.assertRaises(cookies.CookieError):
|
||||
morsel.setdefault("path", c0)
|
||||
@@ -587,6 +595,18 @@ def test_control_characters(self):
|
||||
with self.assertRaises(cookies.CookieError):
|
||||
morsel.set("path", "val", c0)
|
||||
|
||||
+ # .update()
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ morsel.update({"path": c0})
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ morsel.update({c0: "val"})
|
||||
+
|
||||
+ # .__ior__()
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ morsel |= {"path": c0}
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ morsel |= {c0: "val"}
|
||||
+
|
||||
def test_control_characters_output(self):
|
||||
# Tests that even if the internals of Morsel are modified
|
||||
# that a call to .output() has control character safeguards.
|
||||
@@ -607,6 +627,24 @@ def test_control_characters_output(self):
|
||||
with self.assertRaises(cookies.CookieError):
|
||||
cookie.output()
|
||||
|
||||
+ # Tests that .js_output() also has control character safeguards.
|
||||
+ for c0 in support.control_characters_c0():
|
||||
+ morsel = cookies.Morsel()
|
||||
+ morsel.set("key", "value", "coded-value")
|
||||
+ morsel._key = c0 # Override private variable.
|
||||
+ cookie = cookies.SimpleCookie()
|
||||
+ cookie["cookie"] = morsel
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ cookie.js_output()
|
||||
+
|
||||
+ morsel = cookies.Morsel()
|
||||
+ morsel.set("key", "value", "coded-value")
|
||||
+ morsel._coded_value = c0 # Override private variable.
|
||||
+ cookie = cookies.SimpleCookie()
|
||||
+ cookie["cookie"] = morsel
|
||||
+ with self.assertRaises(cookies.CookieError):
|
||||
+ cookie.js_output()
|
||||
+
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
tests.addTest(doctest.DocTestSuite(cookies))
|
||||
diff --git a/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst
|
||||
new file mode 100644
|
||||
index 0000000000..e53a932d12
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst
|
||||
@@ -0,0 +1,4 @@
|
||||
+Reject control characters in :class:`http.cookies.Morsel`
|
||||
+:meth:`~http.cookies.Morsel.update` and
|
||||
+:meth:`~http.cookies.BaseCookie.js_output`.
|
||||
+This addresses :cve:`2026-3644`.
|
||||
98
SOURCES/00485-cve-2026-4224.patch
Normal file
98
SOURCES/00485-cve-2026-4224.patch
Normal file
@ -0,0 +1,98 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
|
||||
Date: Sun, 15 Mar 2026 21:46:06 +0000
|
||||
Subject: 00485: CVE-2026-4224
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Stack overflow parsing XML with deeply nested DTD content models
|
||||
|
||||
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
|
||||
---
|
||||
Lib/test/test_pyexpat.py | 18 ++++++++++++++++++
|
||||
...6-03-14-17-31-39.gh-issue-145986.ifSSr8.rst | 4 ++++
|
||||
Modules/pyexpat.c | 9 ++++++++-
|
||||
3 files changed, 30 insertions(+), 1 deletion(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
|
||||
|
||||
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
|
||||
index 38f951573f..37d9086f40 100644
|
||||
--- a/Lib/test/test_pyexpat.py
|
||||
+++ b/Lib/test/test_pyexpat.py
|
||||
@@ -675,6 +675,24 @@ def test_change_size_2(self):
|
||||
parser.Parse(xml2, True)
|
||||
self.assertEqual(self.n, 4)
|
||||
|
||||
+class ElementDeclHandlerTest(unittest.TestCase):
|
||||
+ def test_deeply_nested_content_model(self):
|
||||
+ # This should raise a RecursionError and not crash.
|
||||
+ # See https://github.com/python/cpython/issues/145986.
|
||||
+ N = 500_000
|
||||
+ data = (
|
||||
+ b'<!DOCTYPE root [\n<!ELEMENT root '
|
||||
+ + b'(a, ' * N + b'a' + b')' * N
|
||||
+ + b'>\n]>\n<root/>\n'
|
||||
+ )
|
||||
+
|
||||
+ parser = expat.ParserCreate()
|
||||
+ parser.ElementDeclHandler = lambda _1, _2: None
|
||||
+ with support.infinite_recursion():
|
||||
+ with self.assertRaises(RecursionError):
|
||||
+ parser.Parse(data)
|
||||
+
|
||||
+
|
||||
class MalformedInputTest(unittest.TestCase):
|
||||
def test1(self):
|
||||
xml = b"\0\r\n"
|
||||
diff --git a/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
|
||||
new file mode 100644
|
||||
index 0000000000..79536d1fef
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
|
||||
@@ -0,0 +1,4 @@
|
||||
+:mod:`xml.parsers.expat`: Fixed a crash caused by unbounded C recursion when
|
||||
+converting deeply nested XML content models with
|
||||
+:meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`.
|
||||
+This addresses :cve:`2026-4224`.
|
||||
diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c
|
||||
index 79492ca5c4..8673540f35 100644
|
||||
--- a/Modules/pyexpat.c
|
||||
+++ b/Modules/pyexpat.c
|
||||
@@ -3,6 +3,7 @@
|
||||
#endif
|
||||
|
||||
#include "Python.h"
|
||||
+#include "pycore_ceval.h" // _Py_EnterRecursiveCall()
|
||||
#include "pycore_runtime.h" // _Py_ID()
|
||||
#include <ctype.h>
|
||||
|
||||
@@ -578,6 +579,10 @@ static PyObject *
|
||||
conv_content_model(XML_Content * const model,
|
||||
PyObject *(*conv_string)(const XML_Char *))
|
||||
{
|
||||
+ if (_Py_EnterRecursiveCall(" in conv_content_model")) {
|
||||
+ return NULL;
|
||||
+ }
|
||||
+
|
||||
PyObject *result = NULL;
|
||||
PyObject *children = PyTuple_New(model->numchildren);
|
||||
int i;
|
||||
@@ -589,7 +594,7 @@ conv_content_model(XML_Content * const model,
|
||||
conv_string);
|
||||
if (child == NULL) {
|
||||
Py_XDECREF(children);
|
||||
- return NULL;
|
||||
+ goto done;
|
||||
}
|
||||
PyTuple_SET_ITEM(children, i, child);
|
||||
}
|
||||
@@ -597,6 +602,8 @@ conv_content_model(XML_Content * const model,
|
||||
model->type, model->quant,
|
||||
conv_string,model->name, children);
|
||||
}
|
||||
+done:
|
||||
+ _Py_LeaveRecursiveCall();
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmjnnr1fFIAAAAAALgAo
|
||||
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
||||
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
||||
YwXF3Q//VrreGa+P8lvp9UMjoj/YquKPwLqjzzAWf5vzHipkebdiESsB1HfGu04k
|
||||
Jw+ctTnXHf/12u0W7ijv+56JtcJFqEzh8yGokWqOzc99rpCeCY9qtuwaVYtZrTNx
|
||||
wepRaDAHdhP4Z2kLPDiE6pCXu2NIR5wHqHjQ8JGmprhASc07uxEhNN/gucVR2Sbr
|
||||
cCfC9rHfHkdhoPpZRRbcraAaxPGL3VyBXf7HuYbHhf4GuF9EVDlFg5I0BzHCKJDd
|
||||
ebPXYHvsoDgrMMqPXiX/YkGNByf3Ze6KZTNSGICy8SDzIzZgpmtOe5rzvlOXJBZZ
|
||||
SVfX8SqP4Ufml+MfJrGEx30S9reYYvnyTSmttpbDznonROKPEZOuDt08+CG3yR+T
|
||||
o5RdIneWmGXRf1mBrFKH9Br5tfOd+YeldfxdoQgla2fFHFVRnab1lsZFOC/HZ5z2
|
||||
Q3rPfVMDYKO8yoIKqv0BUzlkn9wYphCWoPHq0Y+SGjcP+Zh5qRTMqZYIaGekhWmx
|
||||
86egHHVqedMI0Q9hvgIEirupVJ1q34FZn2+3sEka9hdOie9aNHXWTmgWCGDm46qj
|
||||
qC9tT/jkMzWIY2Y4RdVDMdSCb7HkBEl1eAANq511gJ+eSWAXbP1sVrQoiAQY+EkC
|
||||
Yu2ceZYsl9i6zm7i/QaU/mOGB7xMZhMQLZBnZTHSzAZo/pBN7y8=
|
||||
=RuLK
|
||||
-----END PGP SIGNATURE-----
|
||||
18
SOURCES/Python-3.12.13.tar.xz.asc
Normal file
18
SOURCES/Python-3.12.13.tar.xz.asc
Normal file
@ -0,0 +1,18 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmmm3hlfFIAAAAAALgAo
|
||||
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
||||
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
||||
YwW/NA/+MsD99kuy8nBgrK65H3nVj104QcYaau9qhQ62x5+d7P04sVs8bf0mo/AM
|
||||
zec7eCTHnwoLVborEiqyd5NPRUFNJRfsIn0g4z0PIWRBaXsPztMT4suQdNw+VOTd
|
||||
3+up8Um0cA2tnesiXZvT5mXY/2kjffcosP/D4OjdADEGYcUawgNeY4LQi9sTOzs6
|
||||
ubI+mrtSqlAzgKM5NeVj2LdXPeLhpFz4TPk5GTlp0N9p5I1/7ZjK6eLw9p1gPZNN
|
||||
xzN84JL191a9DybGJXIEJ6AcN5TxuBwXHV/KlnEK57AGj1WS5VOhKbHGoSwpRU5m
|
||||
BAtCfJj0zBVqME9W/rROR5mxhta/kG71594STPhXKppyQaH+f41lseNf32So0O+w
|
||||
PdCVebFJgNnCM8Sl+NMrfqiVKW17bAwk4POtbSVqALq3Y2mseDUr0ggU/ZNgeKHy
|
||||
bDxPBSWAAI3kE75yZlXrAJYYiEWgc5GMZJQgFqOJfw1nDmfZL626ckyDdsJotiJO
|
||||
6pZKmFqLlTPkXzw1t45z8/ZI8p/oWrMnTS/IGMcTobXcWHEBvbn9Ljt6nBLrZgO6
|
||||
67vTGss5KhjvfyKwNyQGkG+KIoUJLoewFJ6l5okZB5JIALYn1f/P3bl/kJPUDNp4
|
||||
eP2ao9kSYBBOwutUe1lH8tQYrNGA0C80hPykvbWDHcvF13J5k2E=
|
||||
=KNz3
|
||||
-----END PGP SIGNATURE-----
|
||||
@ -16,11 +16,11 @@ URL: https://www.python.org/
|
||||
|
||||
# WARNING When rebasing to a new Python version,
|
||||
# remember to update the python3-docs package as well
|
||||
%global general_version %{pybasever}.12
|
||||
%global general_version %{pybasever}.13
|
||||
#global prerel ...
|
||||
%global upstream_version %{general_version}%{?prerel}
|
||||
Version: %{general_version}%{?prerel:~%{prerel}}
|
||||
Release: 4%{?dist}
|
||||
Release: 2%{?dist}
|
||||
License: Python-2.0.1
|
||||
|
||||
|
||||
@ -392,36 +392,6 @@ Patch397: 00397-tarfile-filter.patch
|
||||
# CVE-2023-52425. Future versions of Expat may be more reactive.
|
||||
Patch422: 00422-fix-tests-for-xmlpullparser-with-expat-2-6-0.patch
|
||||
|
||||
# 00471 #
|
||||
# CVE-2025-12084
|
||||
#
|
||||
# gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146)
|
||||
# gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794)
|
||||
Patch471: 00471-CVE-2025-12084.patch
|
||||
|
||||
# 00472 # 2ba215eaba508b2cdd7c3acfdf3b9a6e32872274
|
||||
# CVE-2025-13836
|
||||
#
|
||||
# [3.12] gh-119451: Fix a potential denial of service in http.client (GH-119454) (#142140)
|
||||
#
|
||||
# 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.
|
||||
Patch472: 00472-cve-2025-13836.patch
|
||||
|
||||
# 00473 # dd705786aa0c1ccfde913858598e34e1f196be2e
|
||||
# CVE-2026-0865
|
||||
#
|
||||
# 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
|
||||
Patch473: 00473-cve-2026-0865.patch
|
||||
|
||||
# 00474 # 837ddca0372fa87ff9cee47142200caa21e77def
|
||||
# CVE-2025-15366
|
||||
#
|
||||
@ -438,18 +408,48 @@ Patch474: 00474-cve-2025-15366.patch
|
||||
# (cherry-picked from commit b234a2b67539f787e191d2ef19a7cbdce32874e7)
|
||||
Patch475: 00475-cve-2025-15367.patch
|
||||
|
||||
# 00476
|
||||
# CVE-2026-1299
|
||||
#
|
||||
# gh-144125: email: verify headers are sound in BytesGenerator
|
||||
Patch476: 00476-cve-2026-1299.patch
|
||||
|
||||
# 00478 # eb93352dc8e31f4d52546b84daad875e6ff7f29e
|
||||
# CVE-2026-4519
|
||||
#
|
||||
# Reject leading dashes in webbrowser URLs (GH-146360)
|
||||
Patch478: 00478-cve-2026-4519.patch
|
||||
|
||||
# 00479 # 97404b2cf62e545c2d41be7ccfed4e74da9ee665
|
||||
# CVE-2026-1502
|
||||
#
|
||||
# Reject CR/LF in HTTP tunnel request headers
|
||||
Patch479: 00479-cve-2026-1502.patch
|
||||
|
||||
# 00480 # 6f4eef3ba4d9818a53698e994550ee8db17a1e2e
|
||||
# CVE-2026-4786
|
||||
#
|
||||
# Fix webbrowser `%%action` substitution bypass of dash-prefix check
|
||||
Patch480: 00480-cve-2026-4786.patch
|
||||
|
||||
# 00482 # 69f14bc306fc62400d45565faa980b77858b9151
|
||||
# CVE-2026-6100
|
||||
#
|
||||
# Fix a possible UAF in {LZMA,BZ2,_Zlib}Decompressor
|
||||
Patch482: 00482-cve-2026-6100.patch
|
||||
|
||||
# 00483 # 577c595137ce6ff92158ddaf2d7b7ea86437825d
|
||||
# CVE-2026-2297
|
||||
#
|
||||
# Logging Bypass in Legacy .pyc File Handling
|
||||
Patch483: 00483-cve-2026-2297.patch
|
||||
|
||||
# 00484 # 8b5133c1ab17a060cd134bea2a4b6e1831c47fed
|
||||
# CVE-2026-3644
|
||||
#
|
||||
# Incomplete control character validation in http.cookies
|
||||
Patch484: 00484-cve-2026-3644.patch
|
||||
|
||||
# 00485 # 12a5b206676927bcee131ab4f2bd6783d2f5914a
|
||||
# CVE-2026-4224
|
||||
#
|
||||
# Stack overflow parsing XML with deeply nested DTD content models
|
||||
Patch485: 00485-cve-2026-4224.patch
|
||||
|
||||
# (New patches go here ^^^)
|
||||
#
|
||||
# When adding new patches to "python" and "python3" in Fedora, EL, etc.,
|
||||
@ -1940,6 +1940,15 @@ fi
|
||||
# ======================================================
|
||||
|
||||
%changelog
|
||||
* Thu Apr 16 2026 Charalampos Stratakis <cstratak@redhat.com> - 3.12.13-2
|
||||
- Security fixes for CVE-2026-1502, CVE-2026-4786, CVE-2026-6100, CVE-2026-2297, CVE-2026-3644, CVE-2026-4224
|
||||
Resolves: RHEL-168130, RHEL-167892
|
||||
|
||||
* Thu Apr 16 2026 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.12.13-1
|
||||
- Update to 3.12.13
|
||||
- Security fixes for CVE-2025-6075, CVE-2025-13837, CVE-2025-15282, CVE-2025-59375, CVE-2026-0672
|
||||
Related: RHEL-168130, RHEL-167892
|
||||
|
||||
* Fri Mar 27 2026 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.12.12-4
|
||||
- Security fix for CVE-2026-4519
|
||||
Resolves: RHEL-158029
|
||||
|
||||
Loading…
Reference in New Issue
Block a user