File CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch of Package python.42782
From 57c5ecd7e61fbb24e7de76eafd95332bd0ae4dea 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] [3.10] 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/cookie.rst | 4
Lib/Cookie.py | 41 ++++++++-
Lib/test/support/__init__.py | 9 +-
Lib/test/test_cookie.py | 44 +++++++++-
Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1
5 files changed, 91 insertions(+), 8 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
Index: Python-2.7.18/Doc/library/cookie.rst
===================================================================
--- Python-2.7.18.orig/Doc/library/cookie.rst 2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Doc/library/cookie.rst 2026-02-13 23:53:40.669828966 +0100
@@ -265,9 +265,9 @@
Set-Cookie: chips=ahoy
Set-Cookie: vienna=finger
>>> C = Cookie.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 = Cookie.SimpleCookie()
>>> C["oreo"] = "doublestuff"
>>> C["oreo"]["path"] = "/"
Index: Python-2.7.18/Lib/Cookie.py
===================================================================
--- Python-2.7.18.orig/Lib/Cookie.py 2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Lib/Cookie.py 2026-02-13 23:56:53.140889038 +0100
@@ -96,9 +96,9 @@
such trickeries do not confuse it.
>>> C = Cookie.SmartCookie()
- >>> 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
@@ -309,6 +309,20 @@
_idmap = ''.join(chr(x) for x in xrange(256))
+_control_character_re = re.compile(r'[\x00-\x1F\x7F]')
+_legal_key_re = re.compile('[%s]+' % re.escape(_LegalChars))
+
+def _is_legal_key(s):
+ m = _legal_key_re.match(s)
+ return m if m and m.end() == len(s) else None
+
+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, LegalChars=_LegalChars,
idmap=_idmap, translate=string.translate):
#
@@ -441,6 +455,10 @@
K = K.lower()
if not K in self._reserved:
raise CookieError("Invalid Attribute %s" % K)
+ if _has_control_character(K, V):
+ raise CookieError(
+ "Control characters are not allowed in cookies {!r} {!r}".format(K, V)
+ )
dict.__setitem__(self, K, V)
# end __setitem__
@@ -455,6 +473,18 @@
# Second we make sure it only contains legal characters
if key.lower() in self._reserved:
raise CookieError("Attempt to set a reserved key: %s" % key)
+ # The cookie value can contain control characters as long as its
+ # network representation (coded_val) does not. The output path checks
+ # for control characters again.
+ if _has_control_character(key, coded_val):
+ raise CookieError(
+ "Control characters are not allowed in cookies %r %r %r"
+ % (
+ key,
+ val,
+ coded_val,
+ )
+ )
if "" != translate(key, idmap, LegalChars):
raise CookieError("Illegal key value: %s" % key)
@@ -605,8 +635,11 @@
result = []
items = self.items()
items.sort()
- for K,V in items:
- result.append( V.output(attrs, header) )
+ for key, value in items:
+ 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)
# end output
Index: Python-2.7.18/Lib/test/support/__init__.py
===================================================================
--- Python-2.7.18.orig/Lib/test/support/__init__.py 2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Lib/test/support/__init__.py 2026-02-13 23:53:40.671595581 +0100
@@ -45,7 +45,8 @@
"check_impl_detail", "get_attribute", "py3k_bytes",
"import_fresh_module", "threading_cleanup", "reap_children",
"strip_python_stderr", "IPV6_ENABLED", "run_with_tz",
- "SuppressCrashReport"]
+ "SuppressCrashReport",
+ "control_characters_c0"]
class Error(Exception):
"""Base class for regression test exceptions."""
@@ -2175,3 +2176,9 @@
def restore(self):
for signum, handler in self.handlers.items():
self.signal.signal(signum, handler)
+
+def control_characters_c0():
+ """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-2.7.18/Lib/test/test_cookie.py
===================================================================
--- Python-2.7.18.orig/Lib/test/test_cookie.py 2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Lib/test/test_cookie.py 2026-02-13 23:53:40.671828970 +0100
@@ -1,6 +1,10 @@
# Simple test suite for Cookie.py
-from test.test_support import run_unittest, run_doctest, check_warnings
+from test.test_support import (
+ run_unittest,
+ run_doctest,
+ check_warnings,
+ control_characters_c0)
import unittest
import Cookie
import pickle
@@ -168,6 +172,44 @@
C1 = pickle.loads(pickle.dumps(C, protocol=proto))
self.assertEqual(C1.output(), expected_output)
+ def test_control_characters(self):
+ for c0 in control_characters_c0():
+ morsel = Cookie.Morsel()
+
+ # .__setitem__()
+ with self.assertRaises(Cookie.CookieError):
+ morsel[c0] = "val"
+ with self.assertRaises(Cookie.CookieError):
+ morsel["path"] = c0
+
+ # .set()
+ with self.assertRaises(Cookie.CookieError):
+ morsel.set(c0, "val", "coded-value")
+ with self.assertRaises(Cookie.CookieError):
+ morsel.set("path", c0, "coded-value")
+ with self.assertRaises(Cookie.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 control_characters_c0():
+ morsel = Cookie.Morsel()
+ morsel.set("key", "value", "coded-value")
+ morsel.key = c0 # Override internal variable.
+ cookie = Cookie.SimpleCookie()
+ cookie["cookie"] = morsel
+ with self.assertRaises(Cookie.CookieError):
+ cookie.output()
+
+ morsel = Cookie.Morsel()
+ morsel.set("key", "value", "coded-value")
+ morsel.coded_value = c0 # Override internal variable.
+ cookie = Cookie.SimpleCookie()
+ cookie["cookie"] = morsel
+ with self.assertRaises(Cookie.CookieError):
+ cookie.output()
+
def test_main():
run_unittest(CookieTests)
Index: Python-2.7.18/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-2.7.18/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst 2026-02-13 23:53:40.672202173 +0100
@@ -0,0 +1 @@
+Reject control characters in :class:`http.cookies.Morsel` fields and values.