From 3b0169bcc20ce29483e9349cad378450857f3f76 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 31 Jan 2026 22:26:50 +0100 Subject: [PATCH 01/30] Disallow usage of control characters in status, headers and values for security --- Lib/wsgiref/handlers.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 9353fb678625b3..ab8eca177dd477 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -16,6 +16,9 @@ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +_name_disallowed = re.compile(r'[\x00-\x1F\x7F]') +_value_disallowed = 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=False) 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,11 @@ 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=True): """Convert/check value type.""" if type(value) is str: + if (_name_disallowed if name else _value_disallowed).search(value): + raise ValueError("Control characters not allowed in headers and values") return value raise AssertionError( "{0} must be of type str (got {1})".format(title, repr(value)) From 9414df2b3f8709457f7501ca9e1b4e5c6a194d53 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 31 Jan 2026 22:48:56 +0100 Subject: [PATCH 02/30] Add missing import of "re" --- Lib/wsgiref/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index ab8eca177dd477..80957b816f6ba9 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', From 49ddbca5fcdab3348b1c8a8b4776121b42cacb70 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:56:55 +0000 Subject: [PATCH 03/30] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst 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..3f62efabdff5fd --- /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 status, headers and values in ``Lib/wsgiref/handlers.py`` for security. Patch by Benedikt Johannes. From 5dd863bfe40188f19388620c386106d997521be9 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 16:40:22 +0100 Subject: [PATCH 04/30] Update Lib/wsgiref/handlers.py Co-authored-by: Victor Stinner --- Lib/wsgiref/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 80957b816f6ba9..a14e3e0bdbf9f3 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -260,7 +260,7 @@ 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, *, name=True): + def _convert_string_type(self, value, title, *, name): """Convert/check value type.""" if type(value) is str: if (_name_disallowed if name else _value_disallowed).search(value): From 3daaa721face42296adb26d61bfe439d0d2e0eca Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 16:47:34 +0100 Subject: [PATCH 05/30] Update Lib/wsgiref/handlers.py Co-authored-by: Victor Stinner --- Lib/wsgiref/handlers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index a14e3e0bdbf9f3..b4b7600362ca17 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -263,7 +263,8 @@ def _validate_status(self, status): def _convert_string_type(self, value, title, *, name): """Convert/check value type.""" if type(value) is str: - if (_name_disallowed if name else _value_disallowed).search(value): + regex = (_name_disallowed_re if name else _value_disallowed_re) + if regex.search(value): raise ValueError("Control characters not allowed in headers and values") return value raise AssertionError( From 8b149dffb61685df06d18dbb113e6c2dddc96c81 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 16:56:58 +0100 Subject: [PATCH 06/30] Update handlers.py --- Lib/wsgiref/handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index b4b7600362ca17..87c4d9ac370117 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -16,8 +16,8 @@ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] -_name_disallowed = re.compile(r'[\x00-\x1F\x7F]') -_value_disallowed = re.compile(r'[\x00-\x08\x0A-\x1F\x7F]') +_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) From 010fd50132ff3a0bbe2b9e0a9de7196f0cf40087 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 19:28:17 +0100 Subject: [PATCH 07/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index d24aaab1327409..3a27c3b12d9f1c 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -849,6 +849,17 @@ 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() + # HTAB (\x09) is allowed in values, but not in names. + if c0 == "\t": + base["key"] = f"val{c0}" + base.start_response(f"key{c0}", headers) + else: + self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) + class TestModule(unittest.TestCase): def test_deprecated__version__(self): From 8c9a6917bada7c8bc29cf52dadb65ebe1b2ce21d Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 19:40:49 +0100 Subject: [PATCH 08/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 3a27c3b12d9f1c..31243c3c00e77c 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -853,6 +853,7 @@ def testRaisesControlCharacters(self): for c0 in control_characters_c0(): with self.subTest(c0): base = BaseHandler() + headers = Headers() # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": base["key"] = f"val{c0}" From f301791e9d6e175b9fb7b6f394a9cc9c6fc2146c Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 19:50:01 +0100 Subject: [PATCH 09/30] Update test_wsgiref.py I'm not adding c0 at add_header as this is already caught by the other PR of seth and if there are other "versions" of inputting headers it should be the same as in seth's PR (and it's only for debug mode) --- Lib/test/test_wsgiref.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 31243c3c00e77c..8101d92941565f 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,6 +854,8 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = Headers() + headers["key"] = f"val{c0}" + headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": base["key"] = f"val{c0}" From e3b78a0a3ba972446663c333b3a3e8edc460e3d2 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 19:57:08 +0100 Subject: [PATCH 10/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 8101d92941565f..69aad47b62b198 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,7 +854,6 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = Headers() - headers["key"] = f"val{c0}" headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": From 24cfb006368914d800aef689d027dd2fa4ef139a Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 20:12:44 +0100 Subject: [PATCH 11/30] Test whether if statement is reachable --- Lib/test/test_wsgiref.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 69aad47b62b198..962f37ea98321b 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -857,10 +857,7 @@ def testRaisesControlCharacters(self): headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": - base["key"] = f"val{c0}" - base.start_response(f"key{c0}", headers) - else: - self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) + raise exception class TestModule(unittest.TestCase): From 75a89b8afa6316ba2782f0d74d1cf71f2f53af24 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 20:23:30 +0100 Subject: [PATCH 12/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 962f37ea98321b..2193b395ab9519 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -857,7 +857,7 @@ def testRaisesControlCharacters(self): headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": - raise exception + raise TypeError("If this is not triggered it's not reachable") class TestModule(unittest.TestCase): From e32266677474c5b484f36d4b2936d3bf3a54f66c Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 20:37:40 +0100 Subject: [PATCH 13/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 2193b395ab9519..8dd1eb32c8a050 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,10 +854,14 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = Headers() + headers['key'] = f"val{c0}" headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": - raise TypeError("If this is not triggered it's not reachable") + base['key'] = f"val{c0}" + base.start_response(f"key{c0}", headers) + else: + self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) class TestModule(unittest.TestCase): From d731520bff96b5698664ab9edfa40dab48cdbdb2 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 20:47:31 +0100 Subject: [PATCH 14/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 8dd1eb32c8a050..bf78692c4e52a3 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,7 +854,7 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = Headers() - headers['key'] = f"val{c0}" + headers['key'] = f"val" headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": From c039ef2f185fda6f3759fdb0ff26f74af8f4d935 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 21:02:36 +0100 Subject: [PATCH 15/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index bf78692c4e52a3..9ab37c64a6a5c0 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -853,8 +853,8 @@ def testRaisesControlCharacters(self): for c0 in control_characters_c0(): with self.subTest(c0): base = BaseHandler() - headers = Headers() - headers['key'] = f"val" + test = [('x','y')] + headers = Headers(test[:]) headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": From edb54a2ee754c7f9beec85887d82dc57e282531f Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 21:36:29 +0100 Subject: [PATCH 16/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 9ab37c64a6a5c0..42108dda41261b 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -853,8 +853,7 @@ def testRaisesControlCharacters(self): for c0 in control_characters_c0(): with self.subTest(c0): base = BaseHandler() - test = [('x','y')] - headers = Headers(test[:]) + headers = [('x','y')] headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": From b4912450b9e6f185349f4f4f1175062e6636c68c Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 21:45:56 +0100 Subject: [PATCH 17/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 42108dda41261b..4e13580f645284 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,10 +854,8 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = [('x','y')] - headers.add_header("key", "val") # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": - base['key'] = f"val{c0}" base.start_response(f"key{c0}", headers) else: self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) From 379937e1d062269571c9da253ebbcb78b676e156 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 22:28:04 +0100 Subject: [PATCH 18/30] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 4e13580f645284..6bfb5f005762ab 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -856,7 +856,7 @@ def testRaisesControlCharacters(self): headers = [('x','y')] # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": - base.start_response(f"key{c0}", headers) + self.assertRaises(AssertionError, base.start_response, f"key{c0}", headers) else: self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) From db12d861bbe1e8f2da7f431cba5c047bc21f6945 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 22:29:41 +0100 Subject: [PATCH 19/30] Use more strict name=True for status because it shouldn't IMO include HTAB at all --- Lib/wsgiref/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 87c4d9ac370117..bf26c2b9ef7167 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -240,7 +240,7 @@ 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", name=False) + status = self._convert_string_type(status, "Status", name=True) self._validate_status(status) if __debug__: From f206bf3e91022fa8c87c11ab36f53f15ad15ac80 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Wed, 4 Feb 2026 22:33:07 +0100 Subject: [PATCH 20/30] Change it back at first to see if test passes (and then change it back again and change test) --- Lib/wsgiref/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index bf26c2b9ef7167..87c4d9ac370117 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -240,7 +240,7 @@ 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", name=True) + status = self._convert_string_type(status, "Status", name=False) self._validate_status(status) if __debug__: From b899f69e6274d852d0d0f46292d8400ff30fef8a Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 15:33:09 +0100 Subject: [PATCH 21/30] Test if assertRaise raises an error that there is a mistake if no error is raised (this actually makes sense) --- Lib/wsgiref/handlers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 87c4d9ac370117..c12e7a3eef77b4 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -264,8 +264,6 @@ 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 headers and values") return value raise AssertionError( "{0} must be of type str (got {1})".format(title, repr(value)) From 2d1b89089086cbe86251b54280fd45516b11d6a9 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 15:48:24 +0100 Subject: [PATCH 22/30] this is just a temporary check as described above --- Lib/test/test_wsgiref.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 6bfb5f005762ab..ec2791b6374793 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -856,9 +856,9 @@ def testRaisesControlCharacters(self): headers = [('x','y')] # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": - self.assertRaises(AssertionError, base.start_response, f"key{c0}", headers) + self.assertRaises(AssertionError, base.start_response, f"200 OK", headers) else: - self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) + self.assertRaises(ValueError, base.start_response, f"200 OK", headers) class TestModule(unittest.TestCase): From df5cfdf19b6502eacfc49cbcd38a01dc9fcab4ff Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 15:55:33 +0100 Subject: [PATCH 23/30] Change this back --- Lib/test/test_wsgiref.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index ec2791b6374793..6bfb5f005762ab 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -856,9 +856,9 @@ def testRaisesControlCharacters(self): headers = [('x','y')] # HTAB (\x09) is allowed in values, but not in names. if c0 == "\t": - self.assertRaises(AssertionError, base.start_response, f"200 OK", headers) + self.assertRaises(AssertionError, base.start_response, f"key{c0}", headers) else: - self.assertRaises(ValueError, base.start_response, f"200 OK", headers) + self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) class TestModule(unittest.TestCase): From fb527db179f548bbaedc649c668d0ae41f29f64b Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 15:56:33 +0100 Subject: [PATCH 24/30] Update handlers.py --- Lib/wsgiref/handlers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index c12e7a3eef77b4..969fad1419e280 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -264,6 +264,8 @@ 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 headers, values and statuses") return value raise AssertionError( "{0} must be of type str (got {1})".format(title, repr(value)) From e84de9a36037ed7741a968abbecb4873aaef4e1b Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 15:58:51 +0100 Subject: [PATCH 25/30] Remove this because I use the more strict one for status without the HTAB because it seems to me to have no use case in status --- Lib/test/test_wsgiref.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 6bfb5f005762ab..358d996206f238 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,11 +854,7 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = [('x','y')] - # HTAB (\x09) is allowed in values, but not in names. - if c0 == "\t": - self.assertRaises(AssertionError, base.start_response, f"key{c0}", headers) - else: - self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) + self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) class TestModule(unittest.TestCase): From 95701e4a2d5c5b2c7dac2a23c5fe85c39246e940 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 15:59:52 +0100 Subject: [PATCH 26/30] Remove f"keys" --- Lib/test/test_wsgiref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 358d996206f238..21dedb8a4eeb55 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,7 +854,7 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = [('x','y')] - self.assertRaises(ValueError, base.start_response, f"key{c0}", headers) + self.assertRaises(ValueError, base.start_response, {c0}, headers) class TestModule(unittest.TestCase): From 82d7f7a454a199a29babce7c598b7bab44a6012e Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 16:12:32 +0100 Subject: [PATCH 27/30] Add string again without keys --- Lib/test/test_wsgiref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 21dedb8a4eeb55..a1e334c7ffd853 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -854,7 +854,7 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = [('x','y')] - self.assertRaises(ValueError, base.start_response, {c0}, headers) + self.assertRaises(ValueError, base.start_response, f"{c0}", headers) class TestModule(unittest.TestCase): From 76d011e92a5ec4f2e3690c21315532e80c89ce9a Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 16:35:16 +0100 Subject: [PATCH 28/30] Update handlers.py --- Lib/wsgiref/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 969fad1419e280..b57e5d3c14e190 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -240,7 +240,7 @@ 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", name=False) + status = self._convert_string_type(status, "Status", name=True) self._validate_status(status) if __debug__: From 5a4448bb37eeb38871283c49c02185281cf2e12f Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 18:41:35 +0100 Subject: [PATCH 29/30] Update handlers.py --- Lib/wsgiref/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index b57e5d3c14e190..dfb712cdb5f8cb 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -265,7 +265,7 @@ def _convert_string_type(self, value, title, *, name): 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 headers, values and statuses") + 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)) From ecff2b9a6553dc6405ba69bd977372eca8491a33 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Feb 2026 18:42:51 +0100 Subject: [PATCH 30/30] Update 2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst --- .../Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3f62efabdff5fd..133a02a0b3fefd 100644 --- 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 @@ -1 +1 @@ -Disallow usage of control characters in status, headers and values in ``Lib/wsgiref/handlers.py`` for security. Patch by Benedikt Johannes. +Disallow usage of control characters in header names, values and statuses in ``Lib/wsgiref/handlers.py`` for security. Patch by Benedikt Johannes.