File CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch of Package python3.42754
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/http.cookies.rst | 4
Lib/http/cookies.py | 41 ++++++++
Lib/test/test_http_cookies.py | 46 +++++++++-
Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1
4 files changed, 85 insertions(+), 7 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
Index: Python-3.4.10/Doc/library/http.cookies.rst
===================================================================
--- Python-3.4.10.orig/Doc/library/http.cookies.rst 2019-03-18 17:51:26.000000000 +0100
+++ Python-3.4.10/Doc/library/http.cookies.rst 2026-02-13 00:20:32.065184759 +0100
@@ -226,9 +226,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.4.10/Lib/http/cookies.py
===================================================================
--- Python-3.4.10.orig/Lib/http/cookies.py 2026-02-13 00:14:17.197676635 +0100
+++ Python-3.4.10/Lib/http/cookies.py 2026-02-13 00:20:32.065595022 +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
@@ -222,6 +222,17 @@
'\375' : '\\375', '\376' : '\\376', '\377' : '\\377'
}
+_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, LegalChars=_LegalChars):
r"""Quote a string for use in a cookie header.
@@ -235,7 +246,8 @@
return '"' + _nulljoin(_Translator.get(s, s) for s in str) + '"'
-_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub
+_unquote_sub = re.compile(r"\\(?:([0-3][0-7][0-7])|(.))").sub
+
def _unquote_replace(m):
if m.group(1):
@@ -243,6 +255,7 @@
else:
return m.group(2)
+
def _unquote(str):
# If there aren't any doublequotes,
# then there can't be any special characters. See RFC 2109.
@@ -263,6 +276,7 @@
#
return _unquote_sub(_unquote_replace, str)
+
# The _getdate() routine is used to set the expiration time in the cookie's HTTP
# header. By default, _getdate() returns the current time in the appropriate
# "expires" format for a Set-Cookie header. The one optional argument is an
@@ -331,6 +345,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)
def isReservedKey(self, K):
@@ -341,6 +359,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 any(c not in LegalChars for c in key):
raise CookieError("Illegal key value: %s" % key)
@@ -481,7 +511,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.4.10/Lib/test/test_http_cookies.py
===================================================================
--- Python-3.4.10.orig/Lib/test/test_http_cookies.py 2026-02-13 00:14:17.198357842 +0100
+++ Python-3.4.10/Lib/test/test_http_cookies.py 2026-02-13 00:20:32.065843379 +0100
@@ -1,6 +1,12 @@
# Simple test suite for http/cookies.py
-from test.support import run_unittest, run_doctest, check_warnings, requires_resource
+from test.support import (
+ run_unittest,
+ run_doctest,
+ check_warnings,
+ requires_resource,
+ control_characters_c0,
+)
import unittest
from http import cookies
import pickle
@@ -297,6 +303,44 @@
self.assertRaises(cookies.CookieError,
M.set, i, '%s_value' % i, '%s_value' % i)
+ def test_control_characters(self):
+ for c0 in control_characters_c0():
+ morsel = cookies.Morsel()
+
+ # .__setitem__()
+ with self.assertRaises(cookies.CookieError):
+ morsel[c0] = "val"
+ with self.assertRaises(cookies.CookieError):
+ morsel["path"] = c0
+
+ # .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 control_characters_c0():
+ morsel = cookies.Morsel()
+ morsel.set("key", "value", "coded-value")
+ morsel.key = c0 # Override internal 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 internal variable.
+ cookie = cookies.SimpleCookie()
+ cookie["cookie"] = morsel
+ with self.assertRaises(cookies.CookieError):
+ cookie.output()
+
def test_main():
run_unittest(CookieTests, MorselTests)
Index: Python-3.4.10/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.4.10/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst 2026-02-13 00:20:32.066497693 +0100
@@ -0,0 +1 @@
+Reject control characters in :class:`http.cookies.Morsel` fields and values.