https://github.com/python/cpython/commit/95746b3a13a985787ef53b977129041971ed7f70
commit: 95746b3a13a985787ef53b977129041971ed7f70
branch: main
author: Seth Michael Larson <[email protected]>
committer: sethmlarson <[email protected]>
date: 2026-01-20T21:23:42Z
summary:

gh-143919: Reject control characters in http cookies

Co-authored-by: Bartosz SÅ‚awecki <[email protected]>
Co-authored-by: sobolevn <[email protected]>

files:
A Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
M Doc/library/http.cookies.rst
M Lib/http/cookies.py
M Lib/test/test_http_cookies.py

diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst
index 88e978d7f5eafb..50b65459d2f699 100644
--- a/Doc/library/http.cookies.rst
+++ b/Doc/library/http.cookies.rst
@@ -294,9 +294,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 74349bb63d66e2..917280037d4dbb 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.
@@ -294,12 +303,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):
@@ -335,6 +348,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
@@ -488,7 +504,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 c2ed30831b2e0e..7d072d5fd67ca7 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',
@@ -594,6 +594,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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to