File CVE-2026-0865-wsgiref-ctrl-chars.patch of Package python.42782
From 123bfbbe9074ef7fa28e1e7b25575665296560fa Mon Sep 17 00:00:00 2001
From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com>
Date: Sat, 17 Jan 2026 10:23:57 -0800
Subject: [PATCH] [3.10] gh-143916: Reject control characters in
wsgiref.headers.Headers (GH-143917) (GH-143973)
gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917)
* Add 'test.support' fixture for C0 control characters
* gh-143916: Reject control characters in wsgiref.headers.Headers
(cherry picked from commit f7fceed79ca1bceae8dbe5ba5bc8928564da7211)
(cherry picked from commit 22e4d55285cee52bc4dbe061324e5f30bd4dee58)
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Co-authored-by: Seth Michael Larson <seth@python.org>
---
Lib/test/test_wsgiref.py | 14 +++-
Lib/wsgiref/headers.py | 31 +++++++---
Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2
3 files changed, 37 insertions(+), 10 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst
Index: Python-2.7.18/Lib/test/test_wsgiref.py
===================================================================
--- Python-2.7.18.orig/Lib/test/test_wsgiref.py 2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Lib/test/test_wsgiref.py 2026-02-13 21:48:59.446031367 +0100
@@ -13,7 +13,7 @@
import re
import sys
-from test import support
+from test.support import run_unittest, control_characters_c0
class MockServer(WSGIServer):
"""Non-socket HTTP server"""
@@ -354,6 +354,16 @@
)
+ def testRaisesControlCharacters(self):
+ headers = Headers([])
+ for c0 in control_characters_c0():
+ self.assertRaises(ValueError, headers.__setitem__, "key{}".format(c0), "val")
+ self.assertRaises(ValueError, headers.__setitem__, "key", "val{}".format(c0))
+ self.assertRaises(ValueError, headers.add_header, "key{}".format(c0), "val", param="param")
+ self.assertRaises(ValueError, headers.add_header, "key", "val{}".format(c0), param="param")
+ self.assertRaises(ValueError, headers.add_header, "key", "val", param="param{}".format(c0))
+
+
class ErrorHandler(BaseCGIHandler):
"""Simple handler subclass for testing BaseHandler"""
@@ -595,7 +605,7 @@
def test_main():
- support.run_unittest(__name__)
+ run_unittest(__name__)
if __name__ == "__main__":
test_main()
Index: Python-2.7.18/Lib/wsgiref/headers.py
===================================================================
--- Python-2.7.18.orig/Lib/wsgiref/headers.py 2020-04-19 23:13:39.000000000 +0200
+++ Python-2.7.18/Lib/wsgiref/headers.py 2026-02-13 21:58:38.977730950 +0100
@@ -11,6 +11,7 @@
# existence of which force quoting of the parameter value.
import re
tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
+_control_chars_re = re.compile(r'[\x00-\x1F\x7F]')
def _formatparam(param, value=None, quote=1):
"""Convenience function to format and return a key=value pair.
@@ -40,17 +41,27 @@
"""Return the total number of headers, including duplicates."""
return len(self._headers)
+ def _convert_string_type(self, value):
+ """Convert/check value type."""
+ if type(value) is str:
+ if _control_chars_re.search(value):
+ raise ValueError("Control characters not allowed in headers")
+ return value
+ raise AssertionError("Header names/values must be"
+ " of type str (got {0})".format(repr(value)))
+
def __setitem__(self, name, val):
"""Set the value of a header."""
del self[name]
- self._headers.append((name, val))
+ self._headers.append((self._convert_string_type(name),
+ self._convert_string_type(val)))
def __delitem__(self,name):
"""Delete all occurrences of a header, if present.
Does *not* raise an exception if the header is missing.
"""
- name = name.lower()
+ name = self._convert_string_type(name.lower())
self._headers[:] = [kv for kv in self._headers if kv[0].lower() != name]
def __getitem__(self,name):
@@ -79,13 +90,13 @@
fields deleted and re-inserted are always appended to the header list.
If no fields exist with the given name, returns an empty list.
"""
- name = name.lower()
+ name = self._convert_string_type(name.lower())
return [kv[1] for kv in self._headers if kv[0].lower()==name]
def get(self,name,default=None):
"""Get the first header value for 'name', or return 'default'"""
- name = name.lower()
+ name = self._convert_string_type(name.lower())
for k,v in self._headers:
if k.lower()==name:
return v
@@ -137,7 +148,8 @@
and value 'value'."""
result = self.get(name)
if result is None:
- self._headers.append((name,value))
+ self._headers.append((self._convert_string_type(name),
+ self._convert_string_type(value)))
return value
else:
return result
@@ -160,10 +172,13 @@
"""
parts = []
if _value is not None:
- parts.append(_value)
+ parts.append(self._convert_string_type(_value))
for k, v in _params.items():
+ k = self._convert_string_type(k)
if v is None:
parts.append(k.replace('_', '-'))
else:
- parts.append(_formatparam(k.replace('_', '-'), v))
- self._headers.append((_name, "; ".join(parts)))
+ parts.append(_formatparam(k.replace('_', '-'),
+ self._convert_string_type(v)))
+ self._headers.append((self._convert_string_type(_name),
+ "; ".join(parts)))
Index: Python-2.7.18/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-2.7.18/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst 2026-02-13 21:48:59.446547460 +0100
@@ -0,0 +1,2 @@
+Reject C0 control characters within wsgiref.headers.Headers fields, values,
+and parameters.