diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index d24aaab1327409..a1e334c7ffd853 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -849,6 +849,13 @@ def write(self, b): self.assertIsNotNone(h.status) self.assertIsNotNone(h.environ) + def testRaisesControlCharacters(self): + for c0 in control_characters_c0(): + with self.subTest(c0): + base = BaseHandler() + headers = [('x','y')] + self.assertRaises(ValueError, base.start_response, f"{c0}", headers) + class TestModule(unittest.TestCase): def test_deprecated__version__(self): diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 9353fb678625b3..dfb712cdb5f8cb 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -3,7 +3,7 @@ from .util import FileWrapper, guess_scheme, is_hop_by_hop from .headers import Headers -import sys, os, time +import sys, os, time, re __all__ = [ 'BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler', @@ -16,6 +16,9 @@ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +_name_disallowed_re = re.compile(r'[\x00-\x1F\x7F]') +_value_disallowed_re = re.compile(r'[\x00-\x08\x0A-\x1F\x7F]') + def format_date_time(timestamp): year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( @@ -237,13 +240,13 @@ def start_response(self, status, headers,exc_info=None): self.status = status self.headers = self.headers_class(headers) - status = self._convert_string_type(status, "Status") + status = self._convert_string_type(status, "Status", name=True) self._validate_status(status) if __debug__: for name, val in headers: - name = self._convert_string_type(name, "Header name") - val = self._convert_string_type(val, "Header value") + name = self._convert_string_type(name, "Header name", name=True) + val = self._convert_string_type(val, "Header value", name=False) assert not is_hop_by_hop(name),\ f"Hop-by-hop header, '{name}: {val}', not allowed" @@ -257,9 +260,12 @@ def _validate_status(self, status): if status[3] != " ": raise AssertionError("Status message must have a space after code") - def _convert_string_type(self, value, title): + def _convert_string_type(self, value, title, *, name): """Convert/check value type.""" if type(value) is str: + regex = (_name_disallowed_re if name else _value_disallowed_re) + if regex.search(value): + raise ValueError("Control characters not allowed in header names, values and statuses") return value raise AssertionError( "{0} must be of type str (got {1})".format(title, repr(value)) diff --git a/Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst b/Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst new file mode 100644 index 00000000000000..133a02a0b3fefd --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst @@ -0,0 +1 @@ +Disallow usage of control characters in header names, values and statuses in ``Lib/wsgiref/handlers.py`` for security. Patch by Benedikt Johannes.