forked from pool/python312
Update to 3.12.13:
- Python 3.12.13 final
- Release date: 2026-03-03
- 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.
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:
@@ -1,108 +0,0 @@
|
||||
From 3900a2cb7d8321629717b8483179263a968bf552 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 ++++++
|
||||
Lib/test/test_email/test__header_value_parser.py | 23 ++++++++++
|
||||
Misc/NEWS.d/next/Security/2026-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
|
||||
|
||||
Index: Python-3.12.12/Lib/email/_header_value_parser.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/email/_header_value_parser.py 2026-02-06 00:08:35.667684507 +0100
|
||||
+++ Python-3.12.12/Lib/email/_header_value_parser.py 2026-02-06 00:08:40.746286971 +0100
|
||||
@@ -101,6 +101,12 @@
|
||||
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}"'
|
||||
@@ -933,7 +939,7 @@
|
||||
return ' '
|
||||
|
||||
def startswith_fws(self):
|
||||
- return True
|
||||
+ return self and self[0] in WSP
|
||||
|
||||
|
||||
class ValueTerminal(Terminal):
|
||||
@@ -2922,6 +2928,13 @@
|
||||
[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)
|
||||
Index: Python-3.12.12/Lib/test/test_email/test__header_value_parser.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_email/test__header_value_parser.py 2026-02-06 00:08:37.343959625 +0100
|
||||
+++ Python-3.12.12/Lib/test/test_email/test__header_value_parser.py 2026-02-06 00:08:40.747102748 +0100
|
||||
@@ -3141,6 +3141,29 @@
|
||||
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.
|
||||
Index: Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
|
||||
===================================================================
|
||||
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst 2026-02-06 00:08:40.747576711 +0100
|
||||
@@ -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.
|
||||
@@ -1,94 +0,0 @@
|
||||
From df996ce4b9860f8d0c391d876ffe6d0143132a81 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 +--------
|
||||
...5-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 1 +
|
||||
3 files changed, 20 insertions(+), 8 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
|
||||
|
||||
diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
|
||||
index 699265ccadc7f9..a83cb88ef9be79 100644
|
||||
--- a/Lib/test/test_minidom.py
|
||||
+++ b/Lib/test/test_minidom.py
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import copy
|
||||
import pickle
|
||||
+import time
|
||||
import io
|
||||
from test import support
|
||||
import unittest
|
||||
@@ -176,6 +177,23 @@ def testAppendChild(self):
|
||||
self.confirm(dom.documentElement.childNodes[-1].data == "Hello")
|
||||
dom.unlink()
|
||||
|
||||
+ def testAppendChildNoQuadraticComplexity(self):
|
||||
+ impl = getDOMImplementation()
|
||||
+
|
||||
+ newdoc = impl.createDocument(None, "some_tag", None)
|
||||
+ top_element = newdoc.documentElement
|
||||
+ children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)]
|
||||
+ element = top_element
|
||||
+
|
||||
+ start = time.time()
|
||||
+ for child in children:
|
||||
+ element.appendChild(child)
|
||||
+ element = child
|
||||
+ end = time.time()
|
||||
+
|
||||
+ # This example used to take at least 30 seconds.
|
||||
+ self.assertLess(end - start, 1)
|
||||
+
|
||||
def testAppendChildFragment(self):
|
||||
dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes()
|
||||
dom.documentElement.appendChild(frag)
|
||||
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
|
||||
index ef8a159833bbc0..83f717eeb5d043 100644
|
||||
--- a/Lib/xml/dom/minidom.py
|
||||
+++ b/Lib/xml/dom/minidom.py
|
||||
@@ -292,13 +292,6 @@ def _append_child(self, node):
|
||||
childNodes.append(node)
|
||||
node.parentNode = self
|
||||
|
||||
-def _in_document(node):
|
||||
- # return True iff node is part of a document tree
|
||||
- while node is not None:
|
||||
- if node.nodeType == Node.DOCUMENT_NODE:
|
||||
- return True
|
||||
- node = node.parentNode
|
||||
- return False
|
||||
|
||||
def _write_data(writer, data):
|
||||
"Writes datachars to writer."
|
||||
@@ -1539,7 +1532,7 @@ def _clear_id_cache(node):
|
||||
if node.nodeType == Node.DOCUMENT_NODE:
|
||||
node._id_cache.clear()
|
||||
node._id_search_stack = None
|
||||
- elif _in_document(node):
|
||||
+ elif node.ownerDocument:
|
||||
node.ownerDocument._id_cache.clear()
|
||||
node.ownerDocument._id_search_stack= None
|
||||
|
||||
diff --git a/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
|
||||
new file mode 100644
|
||||
index 00000000000000..440bc7794c69ef
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
|
||||
@@ -0,0 +1 @@
|
||||
+Remove quadratic behavior in ``xml.minidom`` node ID cache clearing.
|
||||
@@ -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.12.12/Doc/library/base64.rst
|
||||
Index: Python-3.12.13/Doc/library/base64.rst
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Doc/library/base64.rst 2025-10-09 13:07:00.000000000 +0200
|
||||
+++ Python-3.12.12/Doc/library/base64.rst 2026-02-10 22:15:41.801235355 +0100
|
||||
--- Python-3.12.13.orig/Doc/library/base64.rst 2026-03-03 13:39:30.000000000 +0100
|
||||
+++ Python-3.12.13/Doc/library/base64.rst 2026-03-06 20:24:47.155493487 +0100
|
||||
@@ -74,15 +74,20 @@
|
||||
A :exc:`binascii.Error` exception is raised
|
||||
if *s* is incorrectly padded.
|
||||
@@ -52,10 +52,10 @@ Index: Python-3.12.12/Doc/library/base64.rst
|
||||
|
||||
.. function:: b32encode(s)
|
||||
|
||||
Index: Python-3.12.12/Lib/base64.py
|
||||
Index: Python-3.12.13/Lib/base64.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/base64.py 2026-02-10 22:15:02.534016402 +0100
|
||||
+++ Python-3.12.12/Lib/base64.py 2026-02-10 22:15:41.801591556 +0100
|
||||
--- Python-3.12.13.orig/Lib/base64.py 2026-03-06 20:21:12.189779055 +0100
|
||||
+++ Python-3.12.13/Lib/base64.py 2026-03-06 20:24:47.156270051 +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.12.12/Lib/base64.py
|
||||
|
||||
|
||||
|
||||
Index: Python-3.12.12/Lib/test/test_base64.py
|
||||
Index: Python-3.12.13/Lib/test/test_base64.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_base64.py 2026-02-10 22:15:04.364274059 +0100
|
||||
+++ Python-3.12.12/Lib/test/test_base64.py 2026-02-10 22:17:42.445725550 +0100
|
||||
--- Python-3.12.13.orig/Lib/test/test_base64.py 2026-03-06 20:21:14.072714339 +0100
|
||||
+++ Python-3.12.13/Lib/test/test_base64.py 2026-03-06 20:24:47.156769814 +0100
|
||||
@@ -232,6 +232,25 @@
|
||||
b'\xd3V\xbeo\xf7\x1d')
|
||||
self.check_decode_type_errors(base64.urlsafe_b64decode)
|
||||
@@ -181,10 +181,10 @@ Index: Python-3.12.12/Lib/test/test_base64.py
|
||||
|
||||
def test_b32encode(self):
|
||||
eq = self.assertEqual
|
||||
Index: Python-3.12.12/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst
|
||||
Index: Python-3.12.13/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.12.12/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst 2026-02-10 22:15:41.802353823 +0100
|
||||
+++ Python-3.12.13/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst 2026-03-06 20:24:47.157181851 +0100
|
||||
@@ -0,0 +1,5 @@
|
||||
+Accepting ``+`` and ``/`` characters with an alternative alphabet in
|
||||
+:func:`base64.b64decode` and :func:`base64.urlsafe_b64decode` is now
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
From b3a7998115e195c40e00cfa662bcaa899d937c05 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 ++++++++++
|
||||
Misc/NEWS.d/next/Security/2024-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
|
||||
|
||||
Index: Python-3.12.12/Lib/http/client.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/http/client.py 2025-12-18 19:10:59.442847463 +0100
|
||||
+++ Python-3.12.12/Lib/http/client.py 2025-12-18 19:11:10.279358227 +0100
|
||||
@@ -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 @@
|
||||
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."""
|
||||
Index: Python-3.12.12/Lib/test/test_httplib.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_httplib.py 2025-12-18 19:11:00.972941560 +0100
|
||||
+++ Python-3.12.12/Lib/test/test_httplib.py 2025-12-18 19:11:10.279958169 +0100
|
||||
@@ -1452,6 +1452,72 @@
|
||||
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
|
||||
Index: Python-3.12.12/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst
|
||||
===================================================================
|
||||
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ Python-3.12.12/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst 2025-12-18 19:11:10.280437075 +0100
|
||||
@@ -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,159 +0,0 @@
|
||||
From fa6d8d0f5d00fd629c5df6aabadc82d5f2a76099 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.12] 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 +++++++++-
|
||||
Misc/NEWS.d/next/Security/2024-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
|
||||
|
||||
Index: Python-3.12.12/Lib/plistlib.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/plistlib.py 2025-10-09 13:07:00.000000000 +0200
|
||||
+++ Python-3.12.12/Lib/plistlib.py 2025-12-19 20:12:03.018693606 +0100
|
||||
@@ -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 @@
|
||||
|
||||
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 @@
|
||||
|
||||
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
|
||||
Index: Python-3.12.12/Lib/test/test_plistlib.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_plistlib.py 2025-10-09 13:07:00.000000000 +0200
|
||||
+++ Python-3.12.12/Lib/test/test_plistlib.py 2025-12-19 20:12:03.018920871 +0100
|
||||
@@ -841,8 +841,7 @@
|
||||
|
||||
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 @@
|
||||
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 @@
|
||||
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):
|
||||
Index: Python-3.12.12/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst
|
||||
===================================================================
|
||||
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ Python-3.12.12/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst 2025-12-19 20:12:03.019148207 +0100
|
||||
@@ -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.
|
||||
@@ -1,64 +0,0 @@
|
||||
From f336af1c4b103297de1322bbe00f920f3e58b899 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 1/2] [3.12] 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 +++++
|
||||
Misc/NEWS.d/next/Security/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
|
||||
|
||||
Index: Python-3.12.12/Lib/test/test_urllib.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_urllib.py 2026-02-10 22:15:06.355594386 +0100
|
||||
+++ Python-3.12.12/Lib/test/test_urllib.py 2026-02-10 22:18:23.820760279 +0100
|
||||
@@ -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
|
||||
@@ -688,6 +689,13 @@
|
||||
# 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"""
|
||||
Index: Python-3.12.12/Lib/urllib/request.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/urllib/request.py 2026-02-10 22:15:06.739534061 +0100
|
||||
+++ Python-3.12.12/Lib/urllib/request.py 2026-02-10 22:18:23.819325524 +0100
|
||||
@@ -1655,6 +1655,11 @@
|
||||
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"):
|
||||
Index: Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst
|
||||
===================================================================
|
||||
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst 2026-02-10 22:18:23.819830797 +0100
|
||||
@@ -0,0 +1 @@
|
||||
+Reject control characters in ``data:`` URL media types.
|
||||
@@ -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.12.13/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.12.13.orig/Lib/imaplib.py 2026-03-06 20:21:13.076797807 +0100
|
||||
+++ Python-3.12.13/Lib/imaplib.py 2026-03-06 20:25:14.549622415 +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.12.13/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.12.13/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst 2026-03-06 20:25:14.550097371 +0100
|
||||
@@ -0,0 +1 @@
|
||||
+Reject control characters in IMAP commands.
|
||||
|
||||
@@ -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.12.12/Lib/poplib.py
|
||||
Index: Python-3.12.13/Lib/poplib.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/poplib.py 2026-02-11 23:46:29.442215955 +0100
|
||||
+++ Python-3.12.12/Lib/poplib.py 2026-02-11 23:46:46.713251058 +0100
|
||||
--- Python-3.12.13.orig/Lib/poplib.py 2026-03-06 20:21:13.491889752 +0100
|
||||
+++ Python-3.12.13/Lib/poplib.py 2026-03-06 20:25:33.388287765 +0100
|
||||
@@ -122,6 +122,8 @@
|
||||
def _putcmd(self, line):
|
||||
if self._debugging: print('*cmd*', repr(line))
|
||||
@@ -22,10 +22,10 @@ Index: Python-3.12.12/Lib/poplib.py
|
||||
self._putline(line)
|
||||
|
||||
|
||||
Index: Python-3.12.12/Lib/test/test_poplib.py
|
||||
Index: Python-3.12.13/Lib/test/test_poplib.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_poplib.py 2026-02-11 23:46:31.636412813 +0100
|
||||
+++ Python-3.12.12/Lib/test/test_poplib.py 2026-02-11 23:46:46.713442229 +0100
|
||||
--- Python-3.12.13.orig/Lib/test/test_poplib.py 2026-03-06 20:21:15.497803691 +0100
|
||||
+++ Python-3.12.13/Lib/test/test_poplib.py 2026-03-06 20:25:33.389775036 +0100
|
||||
@@ -17,6 +17,7 @@
|
||||
from test.support import threading_helper
|
||||
from test.support import asynchat
|
||||
@@ -48,9 +48,9 @@ Index: Python-3.12.12/Lib/test/test_poplib.py
|
||||
@requires_ssl
|
||||
def test_stls_capa(self):
|
||||
capa = self.client.capa()
|
||||
Index: Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst
|
||||
Index: Python-3.12.13/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.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst 2026-02-11 23:46:46.713620996 +0100
|
||||
+++ Python-3.12.13/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst 2026-03-06 20:25:33.390103395 +0100
|
||||
@@ -0,0 +1 @@
|
||||
+Reject control characters in POP3 commands.
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
From 122a417f8000eb904a3cac9aacf4de8d288d7148 Mon Sep 17 00:00:00 2001
|
||||
From: Serhiy Storchaka <storchaka@gmail.com>
|
||||
Date: Fri, 31 Oct 2025 15:49:51 +0200
|
||||
Subject: [PATCH 1/2] [3.12] 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 ++-
|
||||
...-05-30-22-33-27.gh-issue-136065.bu337o.rst | 1 +
|
||||
5 files changed, 93 insertions(+), 113 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
|
||||
|
||||
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
|
||||
index 1bef630bd9c0ff..393d358b0afb8c 100644
|
||||
--- a/Lib/ntpath.py
|
||||
+++ b/Lib/ntpath.py
|
||||
@@ -409,17 +409,23 @@ def expanduser(path):
|
||||
# XXX With COMMAND.COM you can use any characters in a variable name,
|
||||
# XXX except '^|<>='.
|
||||
|
||||
+_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)"
|
||||
+_varsub = None
|
||||
+_varsubb = None
|
||||
+
|
||||
def expandvars(path):
|
||||
"""Expand shell variables of the forms $var, ${var} and %var%.
|
||||
|
||||
Unknown variables are left unchanged."""
|
||||
path = os.fspath(path)
|
||||
+ global _varsub, _varsubb
|
||||
if isinstance(path, bytes):
|
||||
if b'$' not in path and b'%' not in path:
|
||||
return path
|
||||
- import string
|
||||
- varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
|
||||
- quote = b'\''
|
||||
+ if not _varsubb:
|
||||
+ import re
|
||||
+ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
|
||||
+ sub = _varsubb
|
||||
percent = b'%'
|
||||
brace = b'{'
|
||||
rbrace = b'}'
|
||||
@@ -428,94 +434,44 @@ def expandvars(path):
|
||||
else:
|
||||
if '$' not in path and '%' not in path:
|
||||
return path
|
||||
- import string
|
||||
- varchars = string.ascii_letters + string.digits + '_-'
|
||||
- quote = '\''
|
||||
+ if not _varsub:
|
||||
+ import re
|
||||
+ _varsub = re.compile(_varpattern, re.ASCII).sub
|
||||
+ sub = _varsub
|
||||
percent = '%'
|
||||
brace = '{'
|
||||
rbrace = '}'
|
||||
dollar = '$'
|
||||
environ = os.environ
|
||||
- res = path[:0]
|
||||
- index = 0
|
||||
- pathlen = len(path)
|
||||
- while index < pathlen:
|
||||
- c = path[index:index+1]
|
||||
- if c == quote: # no expansion within single quotes
|
||||
- path = path[index + 1:]
|
||||
- pathlen = len(path)
|
||||
- try:
|
||||
- index = path.index(c)
|
||||
- res += c + path[:index + 1]
|
||||
- except ValueError:
|
||||
- res += c + path
|
||||
- index = pathlen - 1
|
||||
- elif c == percent: # variable or '%'
|
||||
- if path[index + 1:index + 2] == percent:
|
||||
- res += c
|
||||
- index += 1
|
||||
- else:
|
||||
- path = path[index+1:]
|
||||
- pathlen = len(path)
|
||||
- try:
|
||||
- index = path.index(percent)
|
||||
- except ValueError:
|
||||
- res += percent + path
|
||||
- index = pathlen - 1
|
||||
- else:
|
||||
- var = path[:index]
|
||||
- try:
|
||||
- if environ is None:
|
||||
- value = os.fsencode(os.environ[os.fsdecode(var)])
|
||||
- else:
|
||||
- value = environ[var]
|
||||
- except KeyError:
|
||||
- value = percent + var + percent
|
||||
- res += value
|
||||
- elif c == dollar: # variable or '$$'
|
||||
- if path[index + 1:index + 2] == dollar:
|
||||
- res += c
|
||||
- index += 1
|
||||
- elif path[index + 1:index + 2] == brace:
|
||||
- path = path[index+2:]
|
||||
- pathlen = len(path)
|
||||
- try:
|
||||
- index = path.index(rbrace)
|
||||
- except ValueError:
|
||||
- res += dollar + brace + path
|
||||
- index = pathlen - 1
|
||||
- else:
|
||||
- var = path[:index]
|
||||
- try:
|
||||
- if environ is None:
|
||||
- value = os.fsencode(os.environ[os.fsdecode(var)])
|
||||
- else:
|
||||
- value = environ[var]
|
||||
- except KeyError:
|
||||
- value = dollar + brace + var + rbrace
|
||||
- res += value
|
||||
- else:
|
||||
- var = path[:0]
|
||||
- index += 1
|
||||
- c = path[index:index + 1]
|
||||
- while c and c in varchars:
|
||||
- var += c
|
||||
- index += 1
|
||||
- c = path[index:index + 1]
|
||||
- try:
|
||||
- if environ is None:
|
||||
- value = os.fsencode(os.environ[os.fsdecode(var)])
|
||||
- else:
|
||||
- value = environ[var]
|
||||
- except KeyError:
|
||||
- value = dollar + var
|
||||
- res += value
|
||||
- if c:
|
||||
- index -= 1
|
||||
+
|
||||
+ def repl(m):
|
||||
+ lastindex = m.lastindex
|
||||
+ if lastindex is None:
|
||||
+ return m[0]
|
||||
+ name = m[lastindex]
|
||||
+ if lastindex == 1:
|
||||
+ if name == percent:
|
||||
+ return name
|
||||
+ if not name.endswith(percent):
|
||||
+ return m[0]
|
||||
+ name = name[:-1]
|
||||
else:
|
||||
- res += c
|
||||
- index += 1
|
||||
- return res
|
||||
+ if name == dollar:
|
||||
+ return name
|
||||
+ if name.startswith(brace):
|
||||
+ if not name.endswith(rbrace):
|
||||
+ return m[0]
|
||||
+ name = name[1:-1]
|
||||
+
|
||||
+ try:
|
||||
+ if environ is None:
|
||||
+ return os.fsencode(os.environ[os.fsdecode(name)])
|
||||
+ else:
|
||||
+ return environ[name]
|
||||
+ except KeyError:
|
||||
+ return m[0]
|
||||
+
|
||||
+ return sub(repl, path)
|
||||
|
||||
|
||||
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
|
||||
diff --git a/Lib/posixpath.py b/Lib/posixpath.py
|
||||
index 90a6f545f90529..6306f14f53491b 100644
|
||||
--- a/Lib/posixpath.py
|
||||
+++ b/Lib/posixpath.py
|
||||
@@ -314,42 +314,41 @@ def expanduser(path):
|
||||
# This expands the forms $variable and ${variable} only.
|
||||
# Non-existent variables are left unchanged.
|
||||
|
||||
-_varprog = None
|
||||
-_varprogb = None
|
||||
+_varpattern = r'\$(\w+|\{[^}]*\}?)'
|
||||
+_varsub = None
|
||||
+_varsubb = None
|
||||
|
||||
def expandvars(path):
|
||||
"""Expand shell variables of form $var and ${var}. Unknown variables
|
||||
are left unchanged."""
|
||||
path = os.fspath(path)
|
||||
- global _varprog, _varprogb
|
||||
+ global _varsub, _varsubb
|
||||
if isinstance(path, bytes):
|
||||
if b'$' not in path:
|
||||
return path
|
||||
- if not _varprogb:
|
||||
+ if not _varsubb:
|
||||
import re
|
||||
- _varprogb = re.compile(br'\$(\w+|\{[^}]*\})', re.ASCII)
|
||||
- search = _varprogb.search
|
||||
+ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
|
||||
+ sub = _varsubb
|
||||
start = b'{'
|
||||
end = b'}'
|
||||
environ = getattr(os, 'environb', None)
|
||||
else:
|
||||
if '$' not in path:
|
||||
return path
|
||||
- if not _varprog:
|
||||
+ if not _varsub:
|
||||
import re
|
||||
- _varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII)
|
||||
- search = _varprog.search
|
||||
+ _varsub = re.compile(_varpattern, re.ASCII).sub
|
||||
+ sub = _varsub
|
||||
start = '{'
|
||||
end = '}'
|
||||
environ = os.environ
|
||||
- i = 0
|
||||
- while True:
|
||||
- m = search(path, i)
|
||||
- if not m:
|
||||
- break
|
||||
- i, j = m.span(0)
|
||||
- name = m.group(1)
|
||||
- if name.startswith(start) and name.endswith(end):
|
||||
+
|
||||
+ def repl(m):
|
||||
+ name = m[1]
|
||||
+ if name.startswith(start):
|
||||
+ if not name.endswith(end):
|
||||
+ return m[0]
|
||||
name = name[1:-1]
|
||||
try:
|
||||
if environ is None:
|
||||
@@ -357,13 +356,11 @@ def expandvars(path):
|
||||
else:
|
||||
value = environ[name]
|
||||
except KeyError:
|
||||
- i = j
|
||||
+ return m[0]
|
||||
else:
|
||||
- tail = path[j:]
|
||||
- path = path[:i] + value
|
||||
- i = len(path)
|
||||
- path += tail
|
||||
- return path
|
||||
+ return value
|
||||
+
|
||||
+ return sub(repl, path)
|
||||
|
||||
|
||||
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B.
|
||||
diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py
|
||||
index 3eefb722b81e55..1cec587e875888 100644
|
||||
--- a/Lib/test/test_genericpath.py
|
||||
+++ b/Lib/test/test_genericpath.py
|
||||
@@ -7,6 +7,7 @@
|
||||
import sys
|
||||
import unittest
|
||||
import warnings
|
||||
+from test import support
|
||||
from test.support import is_emscripten
|
||||
from test.support import os_helper
|
||||
from test.support import warnings_helper
|
||||
@@ -443,6 +444,19 @@ def check(value, expected):
|
||||
os.fsencode('$bar%s bar' % nonascii))
|
||||
check(b'$spam}bar', os.fsencode('%s}bar' % nonascii))
|
||||
|
||||
+ @support.requires_resource('cpu')
|
||||
+ def test_expandvars_large(self):
|
||||
+ expandvars = self.pathmodule.expandvars
|
||||
+ with os_helper.EnvironmentVarGuard() as env:
|
||||
+ env.clear()
|
||||
+ env["A"] = "B"
|
||||
+ n = 100_000
|
||||
+ self.assertEqual(expandvars('$A'*n), 'B'*n)
|
||||
+ self.assertEqual(expandvars('${A}'*n), 'B'*n)
|
||||
+ self.assertEqual(expandvars('$A!'*n), 'B!'*n)
|
||||
+ self.assertEqual(expandvars('${A}A'*n), 'BA'*n)
|
||||
+ self.assertEqual(expandvars('${'*10*n), '${'*10*n)
|
||||
+
|
||||
def test_abspath(self):
|
||||
self.assertIn("foo", self.pathmodule.abspath("foo"))
|
||||
with warnings.catch_warnings():
|
||||
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
|
||||
index ced9dc4fffb41e..72f89ebb21c478 100644
|
||||
--- a/Lib/test/test_ntpath.py
|
||||
+++ b/Lib/test/test_ntpath.py
|
||||
@@ -7,8 +7,7 @@
|
||||
import unittest
|
||||
import warnings
|
||||
from ntpath import ALLOW_MISSING
|
||||
-from test.support import cpython_only, os_helper
|
||||
-from test.support import TestFailed, is_emscripten
|
||||
+from test.support import os_helper, is_emscripten
|
||||
from test.support.os_helper import FakePath
|
||||
from test import test_genericpath
|
||||
from tempfile import TemporaryFile
|
||||
@@ -58,7 +57,7 @@ def tester(fn, wantResult):
|
||||
fn = fn.replace("\\", "\\\\")
|
||||
gotResult = eval(fn)
|
||||
if wantResult != gotResult and _norm(wantResult) != _norm(gotResult):
|
||||
- raise TestFailed("%s should return: %s but returned: %s" \
|
||||
+ raise support.TestFailed("%s should return: %s but returned: %s" \
|
||||
%(str(fn), str(wantResult), str(gotResult)))
|
||||
|
||||
# then with bytes
|
||||
@@ -74,7 +73,7 @@ def tester(fn, wantResult):
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
gotResult = eval(fn)
|
||||
if _norm(wantResult) != _norm(gotResult):
|
||||
- raise TestFailed("%s should return: %s but returned: %s" \
|
||||
+ raise support.TestFailed("%s should return: %s but returned: %s" \
|
||||
%(str(fn), str(wantResult), repr(gotResult)))
|
||||
|
||||
|
||||
@@ -882,6 +881,19 @@ def check(value, expected):
|
||||
check('%spam%bar', '%sbar' % nonascii)
|
||||
check('%{}%bar'.format(nonascii), 'ham%sbar' % nonascii)
|
||||
|
||||
+ @support.requires_resource('cpu')
|
||||
+ def test_expandvars_large(self):
|
||||
+ expandvars = ntpath.expandvars
|
||||
+ with os_helper.EnvironmentVarGuard() as env:
|
||||
+ env.clear()
|
||||
+ env["A"] = "B"
|
||||
+ n = 100_000
|
||||
+ self.assertEqual(expandvars('%A%'*n), 'B'*n)
|
||||
+ self.assertEqual(expandvars('%A%A'*n), 'BA'*n)
|
||||
+ self.assertEqual(expandvars("''"*n + '%%'), "''"*n + '%')
|
||||
+ self.assertEqual(expandvars("%%"*n), "%"*n)
|
||||
+ self.assertEqual(expandvars("$$"*n), "$"*n)
|
||||
+
|
||||
def test_expanduser(self):
|
||||
tester('ntpath.expanduser("test")', 'test')
|
||||
|
||||
@@ -1207,7 +1219,7 @@ def test_con_device(self):
|
||||
self.assertTrue(os.path.exists(r"\\.\CON"))
|
||||
|
||||
@unittest.skipIf(sys.platform != 'win32', "Fast paths are only for win32")
|
||||
- @cpython_only
|
||||
+ @support.cpython_only
|
||||
def test_fast_paths_in_use(self):
|
||||
# There are fast paths of these functions implemented in posixmodule.c.
|
||||
# Confirm that they are being used, and not the Python fallbacks in
|
||||
diff --git a/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
|
||||
new file mode 100644
|
||||
index 00000000000000..1d152bb5318380
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
|
||||
@@ -0,0 +1 @@
|
||||
+Fix quadratic complexity in :func:`os.path.expandvars`.
|
||||
|
||||
From 27eb90f676ced2a2a6f2a3ea9d0555d045a0d976 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
|
||||
Date: Fri, 31 Oct 2025 17:32:52 +0100
|
||||
Subject: [PATCH 2/2] Oops
|
||||
|
||||
---
|
||||
Lib/test/test_ntpath.py | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
|
||||
index 72f89ebb21c478..7c42d4f5894788 100644
|
||||
--- a/Lib/test/test_ntpath.py
|
||||
+++ b/Lib/test/test_ntpath.py
|
||||
@@ -7,6 +7,7 @@
|
||||
import unittest
|
||||
import warnings
|
||||
from ntpath import ALLOW_MISSING
|
||||
+from test import support
|
||||
from test.support import os_helper, is_emscripten
|
||||
from test.support.os_helper import FakePath
|
||||
from test import test_genericpath
|
||||
@@ -1,183 +0,0 @@
|
||||
From 9729bdf210967c806f3364c76663fc2ade58e389 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 +++++++++-
|
||||
Misc/NEWS.d/next/Security/2026-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
|
||||
|
||||
Index: Python-3.12.12/Doc/library/http.cookies.rst
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Doc/library/http.cookies.rst 2025-10-09 13:07:00.000000000 +0200
|
||||
+++ Python-3.12.12/Doc/library/http.cookies.rst 2026-02-10 22:17:53.970413039 +0100
|
||||
@@ -272,9 +272,9 @@
|
||||
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"] = "/"
|
||||
Index: Python-3.12.12/Lib/http/cookies.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/http/cookies.py 2026-02-10 22:15:03.102916960 +0100
|
||||
+++ Python-3.12.12/Lib/http/cookies.py 2026-02-10 22:17:53.970584713 +0100
|
||||
@@ -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 @@
|
||||
})
|
||||
|
||||
_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 @@
|
||||
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 @@
|
||||
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
|
||||
@@ -486,7 +502,10 @@
|
||||
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
|
||||
Index: Python-3.12.12/Lib/test/test_http_cookies.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_http_cookies.py 2026-02-10 22:15:05.120676729 +0100
|
||||
+++ Python-3.12.12/Lib/test/test_http_cookies.py 2026-02-10 22:17:53.970780833 +0100
|
||||
@@ -17,10 +17,10 @@
|
||||
'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',
|
||||
@@ -563,6 +563,50 @@
|
||||
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))
|
||||
Index: Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
|
||||
===================================================================
|
||||
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ Python-3.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst 2026-02-10 22:17:53.970970320 +0100
|
||||
@@ -0,0 +1 @@
|
||||
+Reject control characters in :class:`http.cookies.Morsel` fields and values.
|
||||
@@ -1,96 +0,0 @@
|
||||
From 03a7342c9e2bbdab20a6d882bf334608dc7a5d4b 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.12] 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 | 12 +++++++++-
|
||||
Lib/wsgiref/headers.py | 3 ++
|
||||
Misc/NEWS.d/next/Security/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
|
||||
|
||||
Index: Python-3.12.12/Lib/test/support/__init__.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/support/__init__.py 2026-02-10 22:15:04.163026806 +0100
|
||||
+++ Python-3.12.12/Lib/test/support/__init__.py 2026-02-10 22:17:57.451276662 +0100
|
||||
@@ -2591,3 +2591,10 @@
|
||||
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"]
|
||||
Index: Python-3.12.12/Lib/test/test_wsgiref.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/test/test_wsgiref.py 2026-02-10 22:15:06.432657502 +0100
|
||||
+++ Python-3.12.12/Lib/test/test_wsgiref.py 2026-02-10 22:17:57.451566378 +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,16 @@
|
||||
'\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"""
|
||||
|
||||
Index: Python-3.12.12/Lib/wsgiref/headers.py
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Lib/wsgiref/headers.py 2026-02-10 22:15:06.773169140 +0100
|
||||
+++ Python-3.12.12/Lib/wsgiref/headers.py 2026-02-10 22:17:57.451729575 +0100
|
||||
@@ -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 _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)))
|
||||
Index: Python-3.12.12/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.12.12/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst 2026-02-10 22:17:57.451848490 +0100
|
||||
@@ -0,0 +1,2 @@
|
||||
+Reject C0 control characters within wsgiref.headers.Headers fields, values,
|
||||
+and parameters.
|
||||
BIN
Python-3.12.12.tar.xz
LFS
BIN
Python-3.12.12.tar.xz
LFS
Binary file not shown.
@@ -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-----
|
||||
File diff suppressed because one or more lines are too long
BIN
Python-3.12.13.tar.xz
LFS
Normal file
BIN
Python-3.12.13.tar.xz
LFS
Normal file
Binary file not shown.
18
Python-3.12.13.tar.xz.asc
Normal file
18
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-----
|
||||
1
Python-3.12.13.tar.xz.sigstore
Normal file
1
Python-3.12.13.tar.xz.sigstore
Normal file
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
||||
Misc/NEWS | 2 +-
|
||||
2 files changed, 1 insertion(+), 3 deletions(-)
|
||||
|
||||
Index: Python-3.12.12/Doc/using/configure.rst
|
||||
Index: Python-3.12.13/Doc/using/configure.rst
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Doc/using/configure.rst 2025-10-09 13:07:00.000000000 +0200
|
||||
+++ Python-3.12.12/Doc/using/configure.rst 2025-11-15 16:08:56.030736737 +0100
|
||||
--- Python-3.12.13.orig/Doc/using/configure.rst 2026-03-06 20:22:08.230317302 +0100
|
||||
+++ Python-3.12.13/Doc/using/configure.rst 2026-03-06 20:22:22.242697945 +0100
|
||||
@@ -640,13 +640,11 @@
|
||||
|
||||
See ``Mac/README.rst``.
|
||||
@@ -21,11 +21,11 @@ Index: Python-3.12.12/Doc/using/configure.rst
|
||||
.. option:: --enable-framework=INSTALLDIR
|
||||
|
||||
Create a Python.framework rather than a traditional Unix install. Optional
|
||||
Index: Python-3.12.12/Misc/NEWS
|
||||
Index: Python-3.12.13/Misc/NEWS
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Misc/NEWS 2025-10-09 13:07:00.000000000 +0200
|
||||
+++ Python-3.12.12/Misc/NEWS 2025-11-15 16:08:56.040736728 +0100
|
||||
@@ -15234,7 +15234,7 @@
|
||||
--- Python-3.12.13.orig/Misc/NEWS 2026-03-06 20:22:08.230317302 +0100
|
||||
+++ Python-3.12.13/Misc/NEWS 2026-03-06 20:22:22.248451941 +0100
|
||||
@@ -15317,7 +15317,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
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
Index: Python-3.12.12/Modules/readline.c
|
||||
---
|
||||
Modules/readline.c | 23 +++++++++++++++++++++++
|
||||
1 file changed, 23 insertions(+)
|
||||
|
||||
Index: Python-3.12.13/Modules/readline.c
|
||||
===================================================================
|
||||
--- Python-3.12.12.orig/Modules/readline.c 2025-11-19 20:17:29.982940492 +0100
|
||||
+++ Python-3.12.12/Modules/readline.c 2025-11-19 20:36:46.627307300 +0100
|
||||
--- Python-3.12.13.orig/Modules/readline.c 2026-03-03 13:39:30.000000000 +0100
|
||||
+++ Python-3.12.13/Modules/readline.c 2026-03-06 20:22:54.135627806 +0100
|
||||
@@ -145,6 +145,8 @@
|
||||
|
||||
#define readlinestate_global ((readlinestate *)PyModule_GetState(PyState_FindModule(&readlinemodule)))
|
||||
|
||||
@@ -1,3 +1,86 @@
|
||||
-------------------------------------------------------------------
|
||||
Fri Mar 6 19:20:34 UTC 2026 - Matej Cepl <mcepl@cepl.eu>
|
||||
|
||||
- Update to 3.12.13:
|
||||
- Python 3.12.13 final
|
||||
- Release date: 2026-03-03
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Fri Feb 6 00:07:20 CET 2026 - Matej Cepl <mcepl@suse.com>
|
||||
|
||||
|
||||
@@ -118,7 +118,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.12.12
|
||||
Version: 3.12.13
|
||||
Release: 0
|
||||
Summary: Python 3 Interpreter
|
||||
License: Python-2.0
|
||||
|
||||
Reference in New Issue
Block a user