Skip to content

Commit 7e78f17

Browse files
Ihar Voitkaclaude
andcommitted
Add python3.14 package (side-by-side with python3)
Ship Python 3.14.4 as a new SxS package under SPECS/python3.14/. The default python3 (3.12) package is not modified; python3.14 installs strictly under versioned paths (/usr/bin/python3.14, /usr/lib/python3.14/, libpython3.14.so.1.0) and removes unversioned symlinks/pkgconfig/man pages in %install so it cannot collide with python3. Follows the SPECS/nodejs24 precedent for side-by-side major-version packages. CVE patches triaged against the python3 (3.12) set: cgi3.patch dropped (cgi removed in 3.13, PEP 594); all CVE-2025-* patches dropped (fixed upstream in 3.14.4, GA 2026-04-07); CVE-2026-0672, CVE-2026-0865, CVE-2026-1299 and CVE-2026-4519 carried since their fixes post-date 3.14.4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4c1ecd6 commit 7e78f17

9 files changed

Lines changed: 777 additions & 1 deletion

File tree

LICENSES-AND-NOTICES/SPECS/LICENSES-MAP.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSES-AND-NOTICES/SPECS/data/licenses.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3177,6 +3177,7 @@
31773177
"coreos-cloudinit",
31783178
"coreos-init",
31793179
"python-nocaselist",
3180+
"python3.14",
31803181
"update-ssh-keys"
31813182
]
31823183
},
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)