Skip to content

Commit 0e55d09

Browse files
[AutoPR- Security] Patch python3 for CVE-2026-0865, CVE-2025-11468, CVE-2026-0672 [MEDIUM] (#15595)
1 parent ec35977 commit 0e55d09

8 files changed

Lines changed: 432 additions & 21 deletions

File tree

SPECS/python3/CVE-2025-11468.patch

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
From e3c17d8ad56afeb3579052e5aebc6aa5ae115eef Mon Sep 17 00:00:00 2001
2+
From: Seth Michael Larson <seth@python.org>
3+
Date: Mon, 19 Jan 2026 06:38:22 -0600
4+
Subject: [PATCH] gh-143935: Email preserve parens when folding comments
5+
(GH-143936)
6+
7+
Fix a bug in the folding of comments when flattening an email message
8+
using a modern email policy. Comments consisting of a very long sequence of
9+
non-foldable characters could trigger a forced line wrap that omitted the
10+
required leading space on the continuation line, causing the remainder of
11+
the comment to be interpreted as a new header field. This enabled header
12+
injection with carefully crafted inputs.
13+
(cherry picked from commit 17d1490aa97bd6b98a42b1a9b324ead84e7fd8a2)
14+
15+
Co-authored-by: Seth Michael Larson <seth@python.org>
16+
Co-authored-by: Denis Ledoux <dle@odoo.com>
17+
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
18+
Upstream-reference: https://github.com/python/cpython/pull/144036.patch
19+
---
20+
Lib/email/_header_value_parser.py | 15 +++++++++++-
21+
.../test_email/test__header_value_parser.py | 23 +++++++++++++++++++
22+
...-01-16-14-40-31.gh-issue-143935.U2YtKl.rst | 6 +++++
23+
3 files changed, 43 insertions(+), 1 deletion(-)
24+
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
25+
26+
diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py
27+
index 3d845c0..4608f94 100644
28+
--- a/Lib/email/_header_value_parser.py
29+
+++ b/Lib/email/_header_value_parser.py
30+
@@ -101,6 +101,12 @@ def make_quoted_pairs(value):
31+
return str(value).replace('\\', '\\\\').replace('"', '\\"')
32+
33+
34+
+def make_parenthesis_pairs(value):
35+
+ """Escape parenthesis and backslash for use within a comment."""
36+
+ return str(value).replace('\\', '\\\\') \
37+
+ .replace('(', '\\(').replace(')', '\\)')
38+
+
39+
+
40+
def quote_string(value):
41+
escaped = make_quoted_pairs(value)
42+
return f'"{escaped}"'
43+
@@ -933,7 +939,7 @@ class WhiteSpaceTerminal(Terminal):
44+
return ' '
45+
46+
def startswith_fws(self):
47+
- return True
48+
+ return self and self[0] in WSP
49+
50+
51+
class ValueTerminal(Terminal):
52+
@@ -2922,6 +2928,13 @@ def _refold_parse_tree(parse_tree, *, policy):
53+
[ValueTerminal(make_quoted_pairs(p), 'ptext')
54+
for p in newparts] +
55+
[ValueTerminal('"', 'ptext')])
56+
+ if part.token_type == 'comment':
57+
+ newparts = (
58+
+ [ValueTerminal('(', 'ptext')] +
59+
+ [ValueTerminal(make_parenthesis_pairs(p), 'ptext')
60+
+ if p.token_type == 'ptext' else p
61+
+ for p in newparts] +
62+
+ [ValueTerminal(')', 'ptext')])
63+
if not part.as_ew_allowed:
64+
wrap_as_ew_blocked += 1
65+
newparts.append(end_ew_not_allowed)
66+
diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py
67+
index efd1695..8ca170e 100644
68+
--- a/Lib/test/test_email/test__header_value_parser.py
69+
+++ b/Lib/test/test_email/test__header_value_parser.py
70+
@@ -3116,6 +3116,29 @@ class TestFolding(TestEmailBase):
71+
with self.subTest(to=to):
72+
self._test(parser.get_address_list(to)[0], folded, policy=policy)
73+
74+
+ def test_address_list_with_long_unwrapable_comment(self):
75+
+ policy = self.policy.clone(max_line_length=40)
76+
+ cases = [
77+
+ # (to, folded)
78+
+ ('(loremipsumdolorsitametconsecteturadipi)<spy@example.org>',
79+
+ '(loremipsumdolorsitametconsecteturadipi)<spy@example.org>\n'),
80+
+ ('<spy@example.org>(loremipsumdolorsitametconsecteturadipi)',
81+
+ '<spy@example.org>(loremipsumdolorsitametconsecteturadipi)\n'),
82+
+ ('(loremipsum dolorsitametconsecteturadipi)<spy@example.org>',
83+
+ '(loremipsum dolorsitametconsecteturadipi)<spy@example.org>\n'),
84+
+ ('<spy@example.org>(loremipsum dolorsitametconsecteturadipi)',
85+
+ '<spy@example.org>(loremipsum\n dolorsitametconsecteturadipi)\n'),
86+
+ ('(Escaped \\( \\) chars \\\\ in comments stay escaped)<spy@example.org>',
87+
+ '(Escaped \\( \\) chars \\\\ in comments stay\n escaped)<spy@example.org>\n'),
88+
+ ('((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>',
89+
+ '((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>\n'),
90+
+ ('((loremipsum)(loremipsum)(loremipsum) (loremipsum))<spy@example.org>',
91+
+ '((loremipsum)(loremipsum)(loremipsum)\n (loremipsum))<spy@example.org>\n'),
92+
+ ]
93+
+ for (to, folded) in cases:
94+
+ with self.subTest(to=to):
95+
+ self._test(parser.get_address_list(to)[0], folded, policy=policy)
96+
+
97+
# XXX Need tests with comments on various sides of a unicode token,
98+
# and with unicode tokens in the comments. Spaces inside the quotes
99+
# currently don't do the right thing.
100+
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
101+
new file mode 100644
102+
index 0000000..c3d8649
103+
--- /dev/null
104+
+++ b/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
105+
@@ -0,0 +1,6 @@
106+
+Fixed a bug in the folding of comments when flattening an email message
107+
+using a modern email policy. Comments consisting of a very long sequence of
108+
+non-foldable characters could trigger a forced line wrap that omitted the
109+
+required leading space on the continuation line, causing the remainder of
110+
+the comment to be interpreted as a new header field. This enabled header
111+
+injection with carefully crafted inputs.
112+
--
113+
2.45.4
114+

SPECS/python3/CVE-2026-0672.patch

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
From 62498dced866fee86727379378acb20a541f3371 Mon Sep 17 00:00:00 2001
2+
From: Seth Michael Larson <seth@python.org>
3+
Date: Tue, 20 Jan 2026 15:23:42 -0600
4+
Subject: [PATCH] gh-143919: Reject control characters in http cookies (cherry
5+
picked from commit 95746b3a13a985787ef53b977129041971ed7f70)
6+
MIME-Version: 1.0
7+
Content-Type: text/plain; charset=UTF-8
8+
Content-Transfer-Encoding: 8bit
9+
10+
Co-authored-by: Seth Michael Larson <seth@python.org>
11+
Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
12+
Co-authored-by: sobolevn <mail@sobolevn.me>
13+
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
14+
Upstream-reference: https://github.com/python/cpython/pull/144091.patch
15+
---
16+
Doc/library/http.cookies.rst | 4 +-
17+
Lib/http/cookies.py | 25 +++++++--
18+
Lib/test/test_http_cookies.py | 52 +++++++++++++++++--
19+
...-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1 +
20+
4 files changed, 73 insertions(+), 9 deletions(-)
21+
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
22+
23+
diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst
24+
index ad37a0f..317a71a 100644
25+
--- a/Doc/library/http.cookies.rst
26+
+++ b/Doc/library/http.cookies.rst
27+
@@ -272,9 +272,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module.
28+
Set-Cookie: chips=ahoy
29+
Set-Cookie: vienna=finger
30+
>>> C = cookies.SimpleCookie()
31+
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
32+
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
33+
>>> print(C)
34+
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
35+
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
36+
>>> C = cookies.SimpleCookie()
37+
>>> C["oreo"] = "doublestuff"
38+
>>> C["oreo"]["path"] = "/"
39+
diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
40+
index 57791c6..d0a69cb 100644
41+
--- a/Lib/http/cookies.py
42+
+++ b/Lib/http/cookies.py
43+
@@ -87,9 +87,9 @@ within a string. Escaped quotation marks, nested semicolons, and other
44+
such trickeries do not confuse it.
45+
46+
>>> C = cookies.SimpleCookie()
47+
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
48+
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
49+
>>> print(C)
50+
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
51+
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
52+
53+
Each element of the Cookie also supports all of the RFC 2109
54+
Cookie attributes. Here's an example which sets the Path
55+
@@ -170,6 +170,15 @@ _Translator.update({
56+
})
57+
58+
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
59+
+_control_character_re = re.compile(r'[\x00-\x1F\x7F]')
60+
+
61+
+
62+
+def _has_control_character(*val):
63+
+ """Detects control characters within a value.
64+
+ Supports any type, as header values can be any type.
65+
+ """
66+
+ return any(_control_character_re.search(str(v)) for v in val)
67+
+
68+
69+
def _quote(str):
70+
r"""Quote a string for use in a cookie header.
71+
@@ -292,12 +301,16 @@ class Morsel(dict):
72+
K = K.lower()
73+
if not K in self._reserved:
74+
raise CookieError("Invalid attribute %r" % (K,))
75+
+ if _has_control_character(K, V):
76+
+ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
77+
dict.__setitem__(self, K, V)
78+
79+
def setdefault(self, key, val=None):
80+
key = key.lower()
81+
if key not in self._reserved:
82+
raise CookieError("Invalid attribute %r" % (key,))
83+
+ if _has_control_character(key, val):
84+
+ raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
85+
return dict.setdefault(self, key, val)
86+
87+
def __eq__(self, morsel):
88+
@@ -333,6 +346,9 @@ class Morsel(dict):
89+
raise CookieError('Attempt to set a reserved key %r' % (key,))
90+
if not _is_legal_key(key):
91+
raise CookieError('Illegal key %r' % (key,))
92+
+ if _has_control_character(key, val, coded_val):
93+
+ raise CookieError(
94+
+ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))
95+
96+
# It's a good key, so save it.
97+
self._key = key
98+
@@ -486,7 +502,10 @@ class BaseCookie(dict):
99+
result = []
100+
items = sorted(self.items())
101+
for key, value in items:
102+
- result.append(value.output(attrs, header))
103+
+ value_output = value.output(attrs, header)
104+
+ if _has_control_character(value_output):
105+
+ raise CookieError("Control characters are not allowed in cookies")
106+
+ result.append(value_output)
107+
return sep.join(result)
108+
109+
__str__ = output
110+
diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
111+
index 7b3dc0f..f196bcc 100644
112+
--- a/Lib/test/test_http_cookies.py
113+
+++ b/Lib/test/test_http_cookies.py
114+
@@ -17,10 +17,10 @@ class CookieTests(unittest.TestCase):
115+
'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
116+
'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},
117+
118+
- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
119+
- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
120+
- 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
121+
- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
122+
+ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"',
123+
+ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'},
124+
+ 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=;'>''',
125+
+ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'},
126+
127+
# Check illegal cookies that have an '=' char in an unquoted value
128+
{'data': 'keebler=E=mc2',
129+
@@ -563,6 +563,50 @@ class MorselTests(unittest.TestCase):
130+
r'Set-Cookie: key=coded_val; '
131+
r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+')
132+
133+
+ def test_control_characters(self):
134+
+ for c0 in support.control_characters_c0():
135+
+ morsel = cookies.Morsel()
136+
+
137+
+ # .__setitem__()
138+
+ with self.assertRaises(cookies.CookieError):
139+
+ morsel[c0] = "val"
140+
+ with self.assertRaises(cookies.CookieError):
141+
+ morsel["path"] = c0
142+
+
143+
+ # .setdefault()
144+
+ with self.assertRaises(cookies.CookieError):
145+
+ morsel.setdefault("path", c0)
146+
+ with self.assertRaises(cookies.CookieError):
147+
+ morsel.setdefault(c0, "val")
148+
+
149+
+ # .set()
150+
+ with self.assertRaises(cookies.CookieError):
151+
+ morsel.set(c0, "val", "coded-value")
152+
+ with self.assertRaises(cookies.CookieError):
153+
+ morsel.set("path", c0, "coded-value")
154+
+ with self.assertRaises(cookies.CookieError):
155+
+ morsel.set("path", "val", c0)
156+
+
157+
+ def test_control_characters_output(self):
158+
+ # Tests that even if the internals of Morsel are modified
159+
+ # that a call to .output() has control character safeguards.
160+
+ for c0 in support.control_characters_c0():
161+
+ morsel = cookies.Morsel()
162+
+ morsel.set("key", "value", "coded-value")
163+
+ morsel._key = c0 # Override private variable.
164+
+ cookie = cookies.SimpleCookie()
165+
+ cookie["cookie"] = morsel
166+
+ with self.assertRaises(cookies.CookieError):
167+
+ cookie.output()
168+
+
169+
+ morsel = cookies.Morsel()
170+
+ morsel.set("key", "value", "coded-value")
171+
+ morsel._coded_value = c0 # Override private variable.
172+
+ cookie = cookies.SimpleCookie()
173+
+ cookie["cookie"] = morsel
174+
+ with self.assertRaises(cookies.CookieError):
175+
+ cookie.output()
176+
+
177+
178+
def load_tests(loader, tests, pattern):
179+
tests.addTest(doctest.DocTestSuite(cookies))
180+
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
181+
new file mode 100644
182+
index 0000000..788c3e4
183+
--- /dev/null
184+
+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
185+
@@ -0,0 +1 @@
186+
+Reject control characters in :class:`http.cookies.Morsel` fields and values.
187+
--
188+
2.45.4
189+

0 commit comments

Comments
 (0)