Update to 3.11.15:

- Security
    - gh-144125: BytesGenerator will now refuse to serialize
      (write) headers that are unsafely folded or delimited; see
      verify_generated_headers. (Contributed by Bas Bloemsaat and
      Petr Viktorin in gh-121650).
    - gh-143935: 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 (bsc#1257029 CVE-2025-11468).
    - gh-143925: Reject control characters in data: URL media
      types (bsc#1257046, CVE-2025-15282).
    - gh-143919: Reject control characters in http.cookies.Morsel
      fields and values (bsc#1257031, CVE-2026-0672).
    - gh-143916: Reject C0 control characters within
      wsgiref.headers.Headers fields, values, and parameters
      (bsc#1257042, CVE-2026-0865).
    - gh-142145: 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
      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
      xml.dom.Document.documentElement() should be used instead
      (bsc#1254997, CVE-2025-12084).
    - gh-137836: Add support of the “plaintext” element, RAWTEXT
      elements “xmp”, “iframe”, “noembed” and “noframes”, and
      optionally RAWTEXT element “noscript” in
      html.parser.HTMLParser.
    - gh-136063: email.message: ensure linear complexity for
      legacy HTTP parameters parsing. Patch by Bénédikt Tran.
    - gh-136065: Fix quadratic complexity in
      os.path.expandvars() (bsc#1252974, CVE-2025-6075).
    - gh-119451: Fix a potential memory denial of service in the
      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 MemoryError, swapping, out of memory (OOM) killed
      processes or containers, or even system crashes
      (CVE-2025-13836, bsc#1254400).
    - gh-119452: Fix a potential memory denial of service in the
      http.server module. When a malicious user is connected to
      the CGI server on Windows, it could cause an arbitrary
      amount of memory to be allocated. This could have led to
      symptoms including a MemoryError, swapping, out of memory
      (OOM) killed processes or containers, or even system
      crashes.
    - gh-119342: Fix a potential memory denial of service in the
      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 MemoryError, swapping, out of memory (OOM)
      killed processes or containers, or even system crashes
      (bsc#1254401, CVE-2025-13837).
  - Library
    - gh-144833: Fixed a use-after-free in ssl when SSL_new()
      returns NULL in newPySSLSocket(). The error was reported
      via a dangling pointer after the object had already been
      freed.
    - gh-144363: Update bundled libexpat to 2.7.4
    - gh-90949: Add SetAllocTrackerActivationThreshold() and
      SetAllocTrackerMaximumAmplification() to xmlparser objects
      to prevent use of disproportional amounts of dynamic memory
      from within an Expat parser. Patch by Bénédikt Tran.
  - Core and Builtins
    - gh-120384: Fix an array out of bounds crash in
      list_ass_subscript, which could be invoked via some
      specificly tailored input: including concurrent
      modification of a list object, where one thread assigns
      a slice and another clears it.
    - gh-120298: Fix use-after free in list_richcompare_impl
      which can be invoked via some specificly tailored evil
      input.
Remove upstreamed patches:
  - CVE-2025-11468-email-hdr-fold-comment.patch
  - CVE-2025-12084-minidom-quad-search.patch
  - CVE-2025-13836-http-resp-cont-len.patch
  - CVE-2025-13837-plistlib-mailicious-length.patch
  - CVE-2025-6075-expandvars-perf-degrad.patch
  - CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
  - CVE-2026-0865-wsgiref-ctrl-chars.patch
  - CVE-2025-15282-urllib-ctrl-chars.patch
This commit is contained in:
2026-03-06 20:06:11 +01:00
parent 26a31005ec
commit 861a23080f
21 changed files with 171 additions and 1399 deletions

View File

@@ -1,15 +1,15 @@
---
Lib/test/support/__init__.py | 16 ++++++++++++++--
Lib/test/support/__init__.py | 3 ++-
Lib/test/test_minidom.py | 23 +++++++++--------------
Lib/test/test_pyexpat.py | 12 +++++-------
Lib/test/test_sax.py | 18 +++++++++---------
Lib/test/test_xml_etree.py | 12 ------------
5 files changed, 37 insertions(+), 44 deletions(-)
5 files changed, 25 insertions(+), 43 deletions(-)
Index: Python-3.11.14/Lib/test/support/__init__.py
Index: Python-3.11.15/Lib/test/support/__init__.py
===================================================================
--- Python-3.11.14.orig/Lib/test/support/__init__.py 2025-11-15 19:15:08.449938538 +0100
+++ Python-3.11.14/Lib/test/support/__init__.py 2025-11-15 19:15:12.859120260 +0100
--- Python-3.11.15.orig/Lib/test/support/__init__.py 2026-03-06 16:07:01.043952977 +0100
+++ Python-3.11.15/Lib/test/support/__init__.py 2026-03-06 18:23:12.041822692 +0100
@@ -8,6 +8,7 @@
import functools
import os
@@ -27,38 +27,19 @@ Index: Python-3.11.14/Lib/test/support/__init__.py
]
@@ -2279,6 +2280,17 @@
}
return ignored
-#Windows doesn't have os.uname() but it doesn't support s390x.
+
+# Windows doesn't have os.uname() but it doesn't support s390x.
skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x',
'skipped on s390x')
+
+
+@functools.lru_cache
+def _is_expat_2_6_0():
+ return hasattr(pyexpat.ParserCreate(), 'SetReparseDeferralEnabled')
+is_expat_2_6_0 = _is_expat_2_6_0()
+
+fails_with_expat_2_6_0 = (unittest.expectedFailure
+ if is_expat_2_6_0
+ else lambda test: test)
Index: Python-3.11.14/Lib/test/test_minidom.py
Index: Python-3.11.15/Lib/test/test_minidom.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_minidom.py 2025-11-15 19:14:53.915952608 +0100
+++ Python-3.11.14/Lib/test/test_minidom.py 2025-11-15 19:15:12.859877278 +0100
@@ -6,7 +6,6 @@
--- Python-3.11.15.orig/Lib/test/test_minidom.py 2026-03-06 16:06:33.553303637 +0100
+++ Python-3.11.15/Lib/test/test_minidom.py 2026-03-06 16:07:07.712628182 +0100
@@ -7,7 +7,6 @@
from test import support
import unittest
-import pyexpat
import xml.dom.minidom
from xml.dom.minidom import parse, Attr, Node, Document, parseString
@@ -1163,13 +1162,11 @@
from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString
@@ -1194,13 +1193,11 @@
# Verify that character decoding errors raise exceptions instead
# of crashing
@@ -77,7 +58,7 @@ Index: Python-3.11.14/Lib/test/test_minidom.py
b'<fran\xe7ais>Comment \xe7a va ? Tr\xe8s bien ?</fran\xe7ais>')
doc.unlink()
@@ -1631,12 +1628,10 @@
@@ -1662,12 +1659,10 @@
self.confirm(doc2.namespaceURI == xml.dom.EMPTY_NAMESPACE)
def testExceptionOnSpacesInXMLNSValue(self):
@@ -94,11 +75,11 @@ Index: Python-3.11.14/Lib/test/test_minidom.py
parseString('<element xmlns:abc="http:abc.com/de f g/hi/j k"><abc:foo /></element>')
def testDocRemoveChild(self):
Index: Python-3.11.14/Lib/test/test_pyexpat.py
Index: Python-3.11.15/Lib/test/test_pyexpat.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_pyexpat.py 2025-11-15 19:14:53.915952608 +0100
+++ Python-3.11.14/Lib/test/test_pyexpat.py 2025-11-15 19:15:12.860334045 +0100
@@ -14,8 +14,7 @@
--- Python-3.11.15.orig/Lib/test/test_pyexpat.py 2026-03-06 16:06:33.771857223 +0100
+++ Python-3.11.15/Lib/test/test_pyexpat.py 2026-03-06 16:07:07.713428031 +0100
@@ -18,8 +18,7 @@
from xml.parsers import expat
from xml.parsers.expat import errors
@@ -108,7 +89,7 @@ Index: Python-3.11.14/Lib/test/test_pyexpat.py
class SetAttributeTest(unittest.TestCase):
def setUp(self):
@@ -806,9 +805,8 @@
@@ -810,9 +809,8 @@
self.assertIs(parser.GetReparseDeferralEnabled(), enabled)
def test_reparse_deferral_enabled(self):
@@ -120,7 +101,7 @@ Index: Python-3.11.14/Lib/test/test_pyexpat.py
started = []
@@ -837,9 +835,9 @@
@@ -841,9 +839,9 @@
parser = expat.ParserCreate()
parser.StartElementHandler = start_element
@@ -132,10 +113,10 @@ Index: Python-3.11.14/Lib/test/test_pyexpat.py
for chunk in (b'<doc', b'/>'):
parser.Parse(chunk, False)
Index: Python-3.11.14/Lib/test/test_sax.py
Index: Python-3.11.15/Lib/test/test_sax.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_sax.py 2025-11-15 19:14:53.915952608 +0100
+++ Python-3.11.14/Lib/test/test_sax.py 2025-11-15 19:15:12.860746114 +0100
--- Python-3.11.15.orig/Lib/test/test_sax.py 2026-03-06 16:06:33.820462744 +0100
+++ Python-3.11.15/Lib/test/test_sax.py 2026-03-06 16:07:07.713855947 +0100
@@ -19,13 +19,11 @@
from io import BytesIO, StringIO
import codecs
@@ -187,10 +168,10 @@ Index: Python-3.11.14/Lib/test/test_sax.py
self.assertFalse(parser._parser.GetReparseDeferralEnabled())
Index: Python-3.11.14/Lib/test/test_xml_etree.py
Index: Python-3.11.15/Lib/test/test_xml_etree.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_xml_etree.py 2025-11-15 19:14:53.915952608 +0100
+++ Python-3.11.14/Lib/test/test_xml_etree.py 2025-11-15 19:15:12.861491049 +0100
--- Python-3.11.15.orig/Lib/test/test_xml_etree.py 2026-03-06 16:06:34.220861378 +0100
+++ Python-3.11.15/Lib/test/test_xml_etree.py 2026-03-06 16:07:07.714503769 +0100
@@ -13,7 +13,6 @@
import operator
import os

View File

@@ -4,11 +4,11 @@
Lib/test/test_xml_etree.py | 2 ++
3 files changed, 6 insertions(+)
Index: Python-3.11.14/Lib/test/test_pyexpat.py
Index: Python-3.11.15/Lib/test/test_pyexpat.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_pyexpat.py 2025-11-15 19:15:12.860334045 +0100
+++ Python-3.11.14/Lib/test/test_pyexpat.py 2025-11-15 19:15:15.541090355 +0100
@@ -804,6 +804,7 @@
--- Python-3.11.15.orig/Lib/test/test_pyexpat.py 2026-03-06 16:07:07.713428031 +0100
+++ Python-3.11.15/Lib/test/test_pyexpat.py 2026-03-06 18:23:34.307976750 +0100
@@ -808,6 +808,7 @@
parser.SetReparseDeferralEnabled(True)
self.assertIs(parser.GetReparseDeferralEnabled(), enabled)
@@ -16,7 +16,7 @@ Index: Python-3.11.14/Lib/test/test_pyexpat.py
def test_reparse_deferral_enabled(self):
if not is_expat_2_6_0:
self.skipTest("Linked libexpat doesn't support reparse deferral")
@@ -827,6 +828,7 @@
@@ -831,6 +832,7 @@
self.assertEqual(started, ['doc'])
@@ -24,10 +24,10 @@ Index: Python-3.11.14/Lib/test/test_pyexpat.py
def test_reparse_deferral_disabled(self):
started = []
Index: Python-3.11.14/Lib/test/test_sax.py
Index: Python-3.11.15/Lib/test/test_sax.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_sax.py 2025-11-15 19:15:12.860746114 +0100
+++ Python-3.11.14/Lib/test/test_sax.py 2025-11-15 19:15:15.541608234 +0100
--- Python-3.11.15.orig/Lib/test/test_sax.py 2026-03-06 16:07:07.713855947 +0100
+++ Python-3.11.15/Lib/test/test_sax.py 2026-03-06 18:23:34.309155833 +0100
@@ -1213,6 +1213,7 @@
self.assertEqual(result.getvalue(), start + b"<doc>text</doc>")
@@ -44,10 +44,10 @@ Index: Python-3.11.14/Lib/test/test_sax.py
def test_flush_reparse_deferral_disabled(self):
if not is_expat_2_6_0:
self.skipTest("Linked libexpat doesn't support reparse deferral")
Index: Python-3.11.14/Lib/test/test_xml_etree.py
Index: Python-3.11.15/Lib/test/test_xml_etree.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_xml_etree.py 2025-11-15 19:15:12.861491049 +0100
+++ Python-3.11.14/Lib/test/test_xml_etree.py 2025-11-15 19:15:15.542327817 +0100
--- Python-3.11.15.orig/Lib/test/test_xml_etree.py 2026-03-06 16:07:07.714503769 +0100
+++ Python-3.11.15/Lib/test/test_xml_etree.py 2026-03-06 18:23:34.309977052 +0100
@@ -1620,6 +1620,7 @@
with self.assertRaises(ValueError):
ET.XMLPullParser(events=('start', 'end', 'bogus'))

View File

@@ -1,109 +0,0 @@
From dfaeb01d7859417f4e4aab8c3e6c88028c878056 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Mon, 19 Jan 2026 06:38:22 -0600
Subject: [PATCH] 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 <seth@python.org>
Co-authored-by: Denis Ledoux <dle@odoo.com>
---
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 0183a1508b1219..89950c825b6fff 100644
--- a/Lib/email/_header_value_parser.py
+++ b/Lib/email/_header_value_parser.py
@@ -101,6 +101,12 @@ def make_quoted_pairs(value):
return str(value).replace('\\', '\\\\').replace('"', '\\"')
+def make_parenthesis_pairs(value):
+ """Escape parenthesis and backslash for use within a comment."""
+ return str(value).replace('\\', '\\\\') \
+ .replace('(', '\\(').replace(')', '\\)')
+
+
def quote_string(value):
escaped = make_quoted_pairs(value)
return f'"{escaped}"'
@@ -927,7 +933,7 @@ def value(self):
return ' '
def startswith_fws(self):
- return True
+ return self and self[0] in WSP
class ValueTerminal(Terminal):
@@ -2883,6 +2889,13 @@ def _refold_parse_tree(parse_tree, *, policy):
[ValueTerminal(make_quoted_pairs(p), 'ptext')
for p in newparts] +
[ValueTerminal('"', 'ptext')])
+ 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 6025b34ac4a0f8..45ff73b5905fde 100644
--- a/Lib/test/test_email/test__header_value_parser.py
+++ b/Lib/test/test_email/test__header_value_parser.py
@@ -3050,6 +3050,29 @@ def test_address_list_with_specials_in_long_quoted_string(self):
with self.subTest(to=to):
self._test(parser.get_address_list(to)[0], folded, policy=policy)
+ def test_address_list_with_long_unwrapable_comment(self):
+ policy = self.policy.clone(max_line_length=40)
+ cases = [
+ # (to, folded)
+ ('(loremipsumdolorsitametconsecteturadipi)<spy@example.org>',
+ '(loremipsumdolorsitametconsecteturadipi)<spy@example.org>\n'),
+ ('<spy@example.org>(loremipsumdolorsitametconsecteturadipi)',
+ '<spy@example.org>(loremipsumdolorsitametconsecteturadipi)\n'),
+ ('(loremipsum dolorsitametconsecteturadipi)<spy@example.org>',
+ '(loremipsum dolorsitametconsecteturadipi)<spy@example.org>\n'),
+ ('<spy@example.org>(loremipsum dolorsitametconsecteturadipi)',
+ '<spy@example.org>(loremipsum\n dolorsitametconsecteturadipi)\n'),
+ ('(Escaped \\( \\) chars \\\\ in comments stay escaped)<spy@example.org>',
+ '(Escaped \\( \\) chars \\\\ in comments stay\n escaped)<spy@example.org>\n'),
+ ('((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>',
+ '((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>\n'),
+ ('((loremipsum)(loremipsum)(loremipsum) (loremipsum))<spy@example.org>',
+ '((loremipsum)(loremipsum)(loremipsum)\n (loremipsum))<spy@example.org>\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 00000000000000..c3d864936884ac
--- /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.

View File

@@ -1,93 +0,0 @@
From b95c10349956d95e258553def0fcc52ea3ef8f82 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
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 <seth@python.org>
Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com>
---
Lib/test/test_minidom.py | 18 ++++++++++
Lib/xml/dom/minidom.py | 9 -----
Misc/NEWS.d/next/Security/2025-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
Index: Python-3.11.14/Lib/test/test_minidom.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_minidom.py 2025-12-19 22:55:59.547417036 +0100
+++ Python-3.11.14/Lib/test/test_minidom.py 2025-12-19 22:56:07.607956864 +0100
@@ -2,6 +2,7 @@
import copy
import pickle
+import time
import io
from test import support
import unittest
@@ -176,6 +177,23 @@
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)
Index: Python-3.11.14/Lib/xml/dom/minidom.py
===================================================================
--- Python-3.11.14.orig/Lib/xml/dom/minidom.py 2025-10-09 18:16:55.000000000 +0200
+++ Python-3.11.14/Lib/xml/dom/minidom.py 2025-12-19 22:56:07.608359083 +0100
@@ -292,13 +292,6 @@
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 @@
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
Index: Python-3.11.14/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.11.14/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst 2025-12-19 22:56:07.608664851 +0100
@@ -0,0 +1 @@
+Remove quadratic behavior in ``xml.minidom`` node ID cache clearing.

View File

@@ -12,10 +12,10 @@ argument of b32decode().
Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst | 5 +
4 files changed, 91 insertions(+), 14 deletions(-)
Index: Python-3.11.14/Doc/library/base64.rst
Index: Python-3.11.15/Doc/library/base64.rst
===================================================================
--- Python-3.11.14.orig/Doc/library/base64.rst 2025-10-09 18:16:55.000000000 +0200
+++ Python-3.11.14/Doc/library/base64.rst 2026-02-11 23:44:54.612595397 +0100
--- Python-3.11.15.orig/Doc/library/base64.rst 2026-03-03 01:52:57.000000000 +0100
+++ Python-3.11.15/Doc/library/base64.rst 2026-03-06 19:52:36.492967768 +0100
@@ -74,15 +74,20 @@
A :exc:`binascii.Error` exception is raised
if *s* is incorrectly padded.
@@ -52,10 +52,10 @@ Index: Python-3.11.14/Doc/library/base64.rst
.. function:: b32encode(s)
Index: Python-3.11.14/Lib/base64.py
Index: Python-3.11.15/Lib/base64.py
===================================================================
--- Python-3.11.14.orig/Lib/base64.py 2026-02-11 23:44:42.099270109 +0100
+++ Python-3.11.14/Lib/base64.py 2026-02-11 23:44:54.613055284 +0100
--- Python-3.11.15.orig/Lib/base64.py 2026-03-06 16:06:30.195774827 +0100
+++ Python-3.11.15/Lib/base64.py 2026-03-06 19:52:36.493488040 +0100
@@ -71,20 +71,39 @@
The result is returned as a bytes object. A binascii.Error is raised if
s is incorrectly padded.
@@ -121,10 +121,10 @@ Index: Python-3.11.14/Lib/base64.py
Index: Python-3.11.14/Lib/test/test_base64.py
Index: Python-3.11.15/Lib/test/test_base64.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_base64.py 2026-02-11 23:44:44.270637438 +0100
+++ Python-3.11.14/Lib/test/test_base64.py 2026-02-11 23:44:54.613405604 +0100
--- Python-3.11.15.orig/Lib/test/test_base64.py 2026-03-06 16:06:32.552854037 +0100
+++ Python-3.11.15/Lib/test/test_base64.py 2026-03-06 19:52:36.494050069 +0100
@@ -228,6 +228,25 @@
b'\xd3V\xbeo\xf7\x1d')
self.check_decode_type_errors(base64.urlsafe_b64decode)
@@ -181,10 +181,10 @@ Index: Python-3.11.14/Lib/test/test_base64.py
def test_b32encode(self):
eq = self.assertEqual
Index: Python-3.11.14/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst
Index: Python-3.11.15/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.11.14/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst 2026-02-11 23:44:54.613764682 +0100
+++ Python-3.11.15/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst 2026-03-06 19:52:36.494404708 +0100
@@ -0,0 +1,5 @@
+Accepting ``+`` and ``/`` characters with an alternative alphabet in
+:func:`base64.b64decode` and :func:`base64.urlsafe_b64decode` is now

View File

@@ -1,155 +0,0 @@
From 4f2bc24b750a82d3b439f174e7717fc09820bfeb Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
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.
(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 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
@@ -635,10 +640,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
@@ -1390,6 +1390,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.

View File

@@ -1,160 +0,0 @@
From aa9edbb11a2bf7805fd5046cdd5c2d3864aa39f2 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
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 <storchaka@gmail.com>
---
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.

View File

@@ -1,65 +0,0 @@
From 66c966a2d07cfcf555117309ef6aa088001bc487 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Tue, 20 Jan 2026 14:45:58 -0600
Subject: [PATCH] [3.11] gh-143925: Reject control characters in data: URL
mediatypes (cherry picked from commit
f25509e78e8be6ea73c811ac2b8c928c28841b9f) (cherry picked from commit
2c9c746077d8119b5bcf5142316992e464594946)
Co-authored-by: Seth Michael Larson <seth@python.org>
---
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.

View File

@@ -8,10 +8,10 @@ Subject: [PATCH 1/2] Add 'test.support' fixture for C0 control characters
Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst | 1 +
2 files changed, 4 insertions(+), 1 deletion(-)
Index: Python-3.12.12/Lib/imaplib.py
Index: Python-3.11.15/Lib/imaplib.py
===================================================================
--- Python-3.12.12.orig/Lib/imaplib.py 2026-02-10 22:15:03.417592955 +0100
+++ Python-3.12.12/Lib/imaplib.py 2026-02-10 22:18:02.094605035 +0100
--- Python-3.11.15.orig/Lib/imaplib.py 2026-03-06 16:06:31.545110864 +0100
+++ Python-3.11.15/Lib/imaplib.py 2026-03-06 19:51:51.838695961 +0100
@@ -132,7 +132,7 @@
# We compile these in _mode_xxx.
_Literal = br'.*{(?P<size>\d+)}$'
@@ -30,9 +30,9 @@ Index: Python-3.12.12/Lib/imaplib.py
data = data + b' ' + arg
literal = self.literal
Index: Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst
Index: Python-3.11.15/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst 2026-02-10 22:18:02.095167966 +0100
+++ Python-3.11.15/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst 2026-03-06 19:51:51.839096617 +0100
@@ -0,0 +1 @@
+Reject control characters in IMAP commands.

View File

@@ -9,10 +9,10 @@ Subject: [PATCH 1/2] Add 'test.support' fixture for C0 control characters
Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst | 1 +
3 files changed, 11 insertions(+)
Index: Python-3.11.14/Lib/poplib.py
Index: Python-3.11.15/Lib/poplib.py
===================================================================
--- Python-3.11.14.orig/Lib/poplib.py 2025-10-09 18:16:55.000000000 +0200
+++ Python-3.11.14/Lib/poplib.py 2026-02-11 23:38:35.281675745 +0100
--- Python-3.11.15.orig/Lib/poplib.py 2026-03-06 16:06:32.025693538 +0100
+++ Python-3.11.15/Lib/poplib.py 2026-03-06 19:52:31.051258464 +0100
@@ -122,6 +122,8 @@
def _putcmd(self, line):
if self._debugging: print('*cmd*', repr(line))
@@ -22,10 +22,10 @@ Index: Python-3.11.14/Lib/poplib.py
self._putline(line)
Index: Python-3.11.14/Lib/test/test_poplib.py
Index: Python-3.11.15/Lib/test/test_poplib.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_poplib.py 2025-10-09 18:16:55.000000000 +0200
+++ Python-3.11.14/Lib/test/test_poplib.py 2026-02-11 23:39:24.009682813 +0100
--- Python-3.11.15.orig/Lib/test/test_poplib.py 2026-03-06 16:06:33.708669394 +0100
+++ Python-3.11.15/Lib/test/test_poplib.py 2026-03-06 19:52:31.052258474 +0100
@@ -16,6 +16,7 @@
from test.support import socket_helper
from test.support import threading_helper
@@ -48,9 +48,9 @@ Index: Python-3.11.14/Lib/test/test_poplib.py
@requires_ssl
def test_stls_capa(self):
capa = self.client.capa()
Index: Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst
Index: Python-3.11.15/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst 2026-02-11 23:38:35.282276228 +0100
+++ Python-3.11.15/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst 2026-03-06 19:52:31.053950556 +0100
@@ -0,0 +1 @@
+Reject control characters in POP3 commands.

View File

@@ -1,359 +0,0 @@
From e717839989908ecea9c1c8f3bd17ec9fb1ac8963 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
Date: Fri, 31 Oct 2025 15:49:51 +0200
Subject: [PATCH] [3.11] gh-136065: Fix quadratic complexity in
os.path.expandvars() (GH-134952) (cherry picked from commit
f029e8db626ddc6e3a3beea4eff511a71aaceb5c)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
---
Lib/ntpath.py | 126 +++-------
Lib/posixpath.py | 43 +--
Lib/test/test_genericpath.py | 14 +
Lib/test/test_ntpath.py | 22 +
Misc/NEWS.d/next/Security/2025-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
Index: Python-3.11.14/Lib/ntpath.py
===================================================================
--- Python-3.11.14.orig/Lib/ntpath.py 2025-11-15 19:14:27.424009612 +0100
+++ Python-3.11.14/Lib/ntpath.py 2025-11-15 19:14:41.389069009 +0100
@@ -378,17 +378,23 @@
# 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'}'
@@ -397,94 +403,44 @@
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.
Index: Python-3.11.14/Lib/posixpath.py
===================================================================
--- Python-3.11.14.orig/Lib/posixpath.py 2025-11-15 19:14:27.465471369 +0100
+++ Python-3.11.14/Lib/posixpath.py 2025-11-15 19:14:41.389334199 +0100
@@ -287,42 +287,41 @@
# 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 @@
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.
Index: Python-3.11.14/Lib/test/test_genericpath.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_genericpath.py 2025-11-15 19:14:28.470950071 +0100
+++ Python-3.11.14/Lib/test/test_genericpath.py 2025-11-15 19:14:41.389533528 +0100
@@ -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 @@
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():
Index: Python-3.11.14/Lib/test/test_ntpath.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_ntpath.py 2025-11-15 19:14:29.042372971 +0100
+++ Python-3.11.14/Lib/test/test_ntpath.py 2025-11-15 19:14:41.389799697 +0100
@@ -6,8 +6,8 @@
import unittest
import warnings
from ntpath import ALLOW_MISSING
-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
@@ -57,7 +57,7 @@
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
@@ -73,7 +73,7 @@
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)))
@@ -820,6 +820,19 @@
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')
@@ -1090,6 +1103,7 @@
self.assertIsInstance(b_final_path, bytes)
self.assertGreater(len(b_final_path), 0)
+
class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
pathmodule = ntpath
attributes = ['relpath']
Index: Python-3.11.14/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.11.14/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst 2025-11-15 19:14:41.390091148 +0100
@@ -0,0 +1 @@
+Fix quadratic complexity in :func:`os.path.expandvars`.

View File

@@ -1,184 +0,0 @@
From c2d345e3e4dc8932e85dace6599e5c69a144c748 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Tue, 20 Jan 2026 15:23:42 -0600
Subject: [PATCH] gh-143919: Reject control characters in http cookies (cherry
picked from commit 95746b3a13a985787ef53b977129041971ed7f70)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Seth Michael Larson <seth@python.org>
Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
Co-authored-by: sobolevn <mail@sobolevn.me>
---
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
@@ -272,9 +272,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': "<SimpleCookie: chips='ahoy' vienna='finger'>",
'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': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
- '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': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=;'>''',
+ '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.

View File

@@ -1,178 +0,0 @@
From 2c84d7875f35d3d1d0fbc170271227cc95752fa5 Mon Sep 17 00:00:00 2001
From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com>
Date: Sat, 17 Jan 2026 10:23:57 -0800
Subject: [PATCH] [3.11] gh-143916: Reject control characters in
wsgiref.headers.Headers (GH-143917) (GH-143973)
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: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Co-authored-by: Seth Michael Larson <seth@python.org>
---
Lib/test/support/__init__.py | 7 ++
Lib/test/test_wsgiref.py | 18 +++++
Lib/wsgiref/headers.py | 34 ++++++----
Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2
4 files changed, 47 insertions(+), 14 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst
Index: Python-3.11.14/Lib/test/support/__init__.py
===================================================================
--- Python-3.11.14.orig/Lib/test/support/__init__.py 2026-02-11 23:22:45.373477280 +0100
+++ Python-3.11.14/Lib/test/support/__init__.py 2026-02-11 23:23:25.641652256 +0100
@@ -2294,3 +2294,10 @@
fails_with_expat_2_6_0 = (unittest.expectedFailure
if is_expat_2_6_0
else lambda test: test)
+
+
+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"]
Index: Python-3.11.14/Lib/test/test_wsgiref.py
===================================================================
--- Python-3.11.14.orig/Lib/test/test_wsgiref.py 2026-02-11 23:22:38.512011986 +0100
+++ Python-3.11.14/Lib/test/test_wsgiref.py 2026-02-11 23:24:19.545119499 +0100
@@ -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,22 @@
'\r\n'
)
+ def testRaisesControlCharacters(self):
+ for c0 in control_characters_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"""
Index: Python-3.11.14/Lib/wsgiref/headers.py
===================================================================
--- Python-3.11.14.orig/Lib/wsgiref/headers.py 2026-02-11 23:22:38.927685306 +0100
+++ Python-3.11.14/Lib/wsgiref/headers.py 2026-02-11 23:24:19.545709612 +0100
@@ -9,6 +9,11 @@
# existence of which force quoting of the parameter value.
import re
tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
+# 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.
@@ -35,12 +40,15 @@
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:
+ 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"
" of type str (got {0})".format(repr(value)))
@@ -53,14 +61,14 @@
"""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):
@@ -87,13 +95,13 @@
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
@@ -148,8 +156,8 @@
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
@@ -172,13 +180,13 @@
"""
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)))
Index: Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.11.14/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst 2026-02-11 23:22:49.891193395 +0100
@@ -0,0 +1,2 @@
+Reject C0 control characters within wsgiref.headers.Headers fields, values,
+and parameters.

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
Python-3.11.15.tar.xz LFS Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -12,9 +12,11 @@ for the definition of this variable.
Doc/library/functions.rst | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
--- a/Doc/conf.py
+++ b/Doc/conf.py
@@ -316,7 +316,8 @@ html_context = {
Index: Python-3.11.15/Doc/conf.py
===================================================================
--- Python-3.11.15.orig/Doc/conf.py 2026-03-03 01:52:57.000000000 +0100
+++ Python-3.11.15/Doc/conf.py 2026-03-06 18:23:39.828089970 +0100
@@ -316,7 +316,8 @@
}
# This 'Last updated on:' timestamp is inserted at the bottom of every page.
@@ -24,9 +26,11 @@ for the definition of this variable.
# Path to find HTML templates.
templates_path = ['tools/templates']
--- a/Doc/library/functions.rst
+++ b/Doc/library/functions.rst
@@ -1356,7 +1356,7 @@ are always available. They are listed h
Index: Python-3.11.15/Doc/library/functions.rst
===================================================================
--- Python-3.11.15.orig/Doc/library/functions.rst 2026-03-03 01:52:57.000000000 +0100
+++ Python-3.11.15/Doc/library/functions.rst 2026-03-06 18:23:39.829089979 +0100
@@ -1356,7 +1356,7 @@
(where :func:`open` is declared), :mod:`os`, :mod:`os.path`, :mod:`tempfile`,
and :mod:`shutil`.

View File

@@ -3,10 +3,10 @@
Misc/NEWS | 2 +-
2 files changed, 1 insertion(+), 4 deletions(-)
Index: Python-3.11.14/Doc/using/configure.rst
Index: Python-3.11.15/Doc/using/configure.rst
===================================================================
--- Python-3.11.14.orig/Doc/using/configure.rst 2025-11-15 19:14:54.096952433 +0100
+++ Python-3.11.14/Doc/using/configure.rst 2025-11-15 19:15:04.439920979 +0100
--- Python-3.11.15.orig/Doc/using/configure.rst 2026-03-06 16:06:43.304945441 +0100
+++ Python-3.11.15/Doc/using/configure.rst 2026-03-06 16:06:55.576758292 +0100
@@ -43,7 +43,6 @@
See :data:`sys.int_info.bits_per_digit <sys.int_info>`.
@@ -29,11 +29,11 @@ Index: Python-3.11.14/Doc/using/configure.rst
.. option:: --enable-framework=INSTALLDIR
Create a Python.framework rather than a traditional Unix install. Optional
Index: Python-3.11.14/Misc/NEWS
Index: Python-3.11.15/Misc/NEWS
===================================================================
--- Python-3.11.14.orig/Misc/NEWS 2025-11-15 19:14:54.096952433 +0100
+++ Python-3.11.14/Misc/NEWS 2025-11-15 19:15:04.445942414 +0100
@@ -9987,7 +9987,7 @@
--- Python-3.11.15.orig/Misc/NEWS 2026-03-06 16:06:43.304945441 +0100
+++ Python-3.11.15/Misc/NEWS 2026-03-06 16:06:55.580059032 +0100
@@ -10081,7 +10081,7 @@
- bpo-40939: Removed documentation for the removed ``PyParser_*`` C API.
- bpo-43795: The list in :ref:`limited-api-list` now shows the public name

View File

@@ -1,3 +1,93 @@
-------------------------------------------------------------------
Fri Mar 6 18:54:51 UTC 2026 - Matej Cepl <mcepl@cepl.eu>
- Update to 3.11.15:
- Security
- gh-144125: BytesGenerator will now refuse to serialize
(write) headers that are unsafely folded or delimited; see
verify_generated_headers. (Contributed by Bas Bloemsaat and
Petr Viktorin in gh-121650).
- gh-143935: 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 (bsc#1257029 CVE-2025-11468).
- gh-143925: Reject control characters in data: URL media
types (bsc#1257046, CVE-2025-15282).
- gh-143919: Reject control characters in http.cookies.Morsel
fields and values (bsc#1257031, CVE-2026-0672).
- gh-143916: Reject C0 control characters within
wsgiref.headers.Headers fields, values, and parameters
(bsc#1257042, CVE-2026-0865).
- gh-142145: 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
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
xml.dom.Document.documentElement() should be used instead
(bsc#1254997, CVE-2025-12084).
- gh-137836: Add support of the “plaintext” element, RAWTEXT
elements “xmp”, “iframe”, “noembed” and “noframes”, and
optionally RAWTEXT element “noscript” in
html.parser.HTMLParser.
- gh-136063: email.message: ensure linear complexity for
legacy HTTP parameters parsing. Patch by Bénédikt Tran.
- gh-136065: Fix quadratic complexity in
os.path.expandvars() (bsc#1252974, CVE-2025-6075).
- gh-119451: Fix a potential memory denial of service in the
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 MemoryError, swapping, out of memory (OOM) killed
processes or containers, or even system crashes
(CVE-2025-13836, bsc#1254400).
- gh-119452: Fix a potential memory denial of service in the
http.server module. When a malicious user is connected to
the CGI server on Windows, it could cause an arbitrary
amount of memory to be allocated. This could have led to
symptoms including a MemoryError, swapping, out of memory
(OOM) killed processes or containers, or even system
crashes.
- gh-119342: Fix a potential memory denial of service in the
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 MemoryError, swapping, out of memory (OOM)
killed processes or containers, or even system crashes
(bsc#1254401, CVE-2025-13837).
- Library
- gh-144833: Fixed a use-after-free in ssl when SSL_new()
returns NULL in newPySSLSocket(). The error was reported
via a dangling pointer after the object had already been
freed.
- gh-144363: Update bundled libexpat to 2.7.4
- gh-90949: Add SetAllocTrackerActivationThreshold() and
SetAllocTrackerMaximumAmplification() to xmlparser objects
to prevent use of disproportional amounts of dynamic memory
from within an Expat parser. Patch by Bénédikt Tran.
- Core and Builtins
- gh-120384: Fix an array out of bounds crash in
list_ass_subscript, which could be invoked via some
specificly tailored input: including concurrent
modification of a list object, where one thread assigns
a slice and another clears it.
- gh-120298: Fix use-after free in list_richcompare_impl
which can be invoked via some specificly tailored evil
input.
Remove upstreamed patches:
- CVE-2025-11468-email-hdr-fold-comment.patch
- CVE-2025-12084-minidom-quad-search.patch
- CVE-2025-13836-http-resp-cont-len.patch
- CVE-2025-13837-plistlib-mailicious-length.patch
- CVE-2025-6075-expandvars-perf-degrad.patch
- CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
- CVE-2026-0865-wsgiref-ctrl-chars.patch
- CVE-2025-15282-urllib-ctrl-chars.patch
-------------------------------------------------------------------
Wed Feb 11 19:09:06 CET 2026 - Matej Cepl <mcepl@suse.com>

View File

@@ -107,7 +107,7 @@
# _md5.cpython-38m-x86_64-linux-gnu.so
%define dynlib() %{sitedir}/lib-dynload/%{1}.cpython-%{abi_tag}-%{archname}-%{_os}%{?_gnu}%{?armsuffix}.so
Name: %{python_pkg_name}%{psuffix}
Version: 3.11.14
Version: 3.11.15
Release: 0
Summary: Python 3 Interpreter
License: Python-2.0