forked from pool/python311
CVE-2025-11468: preserving parens when folding comments in email headers (bsc#1257029, gh#python/cpython#143935). CVE-2025-11468-email-hdr-fold-comment.patch CVE-2026-0672: rejects control characters in http cookies. (bsc#1257031, gh#python/cpython#143919) CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch CVE-2026-0865: rejecting control characters in wsgiref.headers.Headers, which could be abused for injecting false HTTP headers. (bsc#1257042, gh#python/cpython#143916) CVE-2026-0865-wsgiref-ctrl-chars.patch CVE-2025-15366: basically the same as the previous patch for IMAP protocol. (bsc#1257044, gh#python/cpython#143921) CVE-2025-15366-imap-ctrl-chars.patch CVE-2025-15282: basically the same as the previous patch for urllib library. (bsc#1257046, gh#python/cpython#143925) CVE-2025-15282-urllib-ctrl-chars.patch CVE-2025-15367: basically the same as the previous patch for poplib library. (bsc#1257041, gh#python/cpython#143923) CVE-2025-15367-poplib-ctrl-chars.patch CVE-2025-12781: fix decoding with non-standard Base64 alphabet (bsc#1257108, gh#python/cpython#125346) CVE-2025-12781-b64decode-alt-chars.patch
185 lines
8.0 KiB
Diff
185 lines
8.0 KiB
Diff
From c2d345e3e4dc8932e85dace6599e5c69a144c748 Mon Sep 17 00:00:00 2001
|
|
From: Seth Michael Larson <seth@python.org>
|
|
Date: Tue, 20 Jan 2026 15:23:42 -0600
|
|
Subject: [PATCH] gh-143919: Reject control characters in http cookies (cherry
|
|
picked from commit 95746b3a13a985787ef53b977129041971ed7f70)
|
|
MIME-Version: 1.0
|
|
Content-Type: text/plain; charset=UTF-8
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
Co-authored-by: Seth Michael Larson <seth@python.org>
|
|
Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
|
|
Co-authored-by: sobolevn <mail@sobolevn.me>
|
|
---
|
|
Doc/library/http.cookies.rst | 4 +-
|
|
Lib/http/cookies.py | 25 +++++++--
|
|
Lib/test/test_http_cookies.py | 52 +++++++++++++++++--
|
|
...-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1 +
|
|
4 files changed, 73 insertions(+), 9 deletions(-)
|
|
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
|
|
|
|
diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst
|
|
index e91972fe621a48..e2abb31149ff10 100644
|
|
--- a/Doc/library/http.cookies.rst
|
|
+++ b/Doc/library/http.cookies.rst
|
|
@@ -272,9 +272,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module.
|
|
Set-Cookie: chips=ahoy
|
|
Set-Cookie: vienna=finger
|
|
>>> C = cookies.SimpleCookie()
|
|
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
|
|
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
|
|
>>> print(C)
|
|
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
|
|
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
|
|
>>> C = cookies.SimpleCookie()
|
|
>>> C["oreo"] = "doublestuff"
|
|
>>> C["oreo"]["path"] = "/"
|
|
diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
|
|
index 2c1f021d0abede..5cfa7a8072c7f7 100644
|
|
--- a/Lib/http/cookies.py
|
|
+++ b/Lib/http/cookies.py
|
|
@@ -87,9 +87,9 @@
|
|
such trickeries do not confuse it.
|
|
|
|
>>> C = cookies.SimpleCookie()
|
|
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
|
|
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
|
|
>>> print(C)
|
|
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
|
|
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
|
|
|
|
Each element of the Cookie also supports all of the RFC 2109
|
|
Cookie attributes. Here's an example which sets the Path
|
|
@@ -170,6 +170,15 @@ class CookieError(Exception):
|
|
})
|
|
|
|
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
|
+_control_character_re = re.compile(r'[\x00-\x1F\x7F]')
|
|
+
|
|
+
|
|
+def _has_control_character(*val):
|
|
+ """Detects control characters within a value.
|
|
+ Supports any type, as header values can be any type.
|
|
+ """
|
|
+ return any(_control_character_re.search(str(v)) for v in val)
|
|
+
|
|
|
|
def _quote(str):
|
|
r"""Quote a string for use in a cookie header.
|
|
@@ -292,12 +301,16 @@ def __setitem__(self, K, V):
|
|
K = K.lower()
|
|
if not K in self._reserved:
|
|
raise CookieError("Invalid attribute %r" % (K,))
|
|
+ if _has_control_character(K, V):
|
|
+ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
|
|
dict.__setitem__(self, K, V)
|
|
|
|
def setdefault(self, key, val=None):
|
|
key = key.lower()
|
|
if key not in self._reserved:
|
|
raise CookieError("Invalid attribute %r" % (key,))
|
|
+ if _has_control_character(key, val):
|
|
+ raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
|
|
return dict.setdefault(self, key, val)
|
|
|
|
def __eq__(self, morsel):
|
|
@@ -333,6 +346,9 @@ def set(self, key, val, coded_val):
|
|
raise CookieError('Attempt to set a reserved key %r' % (key,))
|
|
if not _is_legal_key(key):
|
|
raise CookieError('Illegal key %r' % (key,))
|
|
+ if _has_control_character(key, val, coded_val):
|
|
+ raise CookieError(
|
|
+ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))
|
|
|
|
# It's a good key, so save it.
|
|
self._key = key
|
|
@@ -484,7 +500,10 @@ def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
|
|
result = []
|
|
items = sorted(self.items())
|
|
for key, value in items:
|
|
- result.append(value.output(attrs, header))
|
|
+ value_output = value.output(attrs, header)
|
|
+ if _has_control_character(value_output):
|
|
+ raise CookieError("Control characters are not allowed in cookies")
|
|
+ result.append(value_output)
|
|
return sep.join(result)
|
|
|
|
__str__ = output
|
|
diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
|
|
index 8879902a6e2f41..2438c57ef40458 100644
|
|
--- a/Lib/test/test_http_cookies.py
|
|
+++ b/Lib/test/test_http_cookies.py
|
|
@@ -17,10 +17,10 @@ def test_basic(self):
|
|
'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
|
|
'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},
|
|
|
|
- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
|
|
- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
|
|
- 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
|
|
- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
|
|
+ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"',
|
|
+ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'},
|
|
+ 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=;'>''',
|
|
+ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'},
|
|
|
|
# Check illegal cookies that have an '=' char in an unquoted value
|
|
{'data': 'keebler=E=mc2',
|
|
@@ -517,6 +517,50 @@ def test_repr(self):
|
|
r'Set-Cookie: key=coded_val; '
|
|
r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+')
|
|
|
|
+ def test_control_characters(self):
|
|
+ for c0 in support.control_characters_c0():
|
|
+ morsel = cookies.Morsel()
|
|
+
|
|
+ # .__setitem__()
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ morsel[c0] = "val"
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ morsel["path"] = c0
|
|
+
|
|
+ # .setdefault()
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ morsel.setdefault("path", c0)
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ morsel.setdefault(c0, "val")
|
|
+
|
|
+ # .set()
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ morsel.set(c0, "val", "coded-value")
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ morsel.set("path", c0, "coded-value")
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ morsel.set("path", "val", c0)
|
|
+
|
|
+ def test_control_characters_output(self):
|
|
+ # Tests that even if the internals of Morsel are modified
|
|
+ # that a call to .output() has control character safeguards.
|
|
+ for c0 in support.control_characters_c0():
|
|
+ morsel = cookies.Morsel()
|
|
+ morsel.set("key", "value", "coded-value")
|
|
+ morsel._key = c0 # Override private variable.
|
|
+ cookie = cookies.SimpleCookie()
|
|
+ cookie["cookie"] = morsel
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ cookie.output()
|
|
+
|
|
+ morsel = cookies.Morsel()
|
|
+ morsel.set("key", "value", "coded-value")
|
|
+ morsel._coded_value = c0 # Override private variable.
|
|
+ cookie = cookies.SimpleCookie()
|
|
+ cookie["cookie"] = morsel
|
|
+ with self.assertRaises(cookies.CookieError):
|
|
+ cookie.output()
|
|
+
|
|
|
|
def load_tests(loader, tests, pattern):
|
|
tests.addTest(doctest.DocTestSuite(cookies))
|
|
diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
|
|
new file mode 100644
|
|
index 00000000000000..788c3e4ac2ebf7
|
|
--- /dev/null
|
|
+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
|
|
@@ -0,0 +1 @@
|
|
+Reject control characters in :class:`http.cookies.Morsel` fields and values.
|