From 3b0169bcc20ce29483e9349cad378450857f3f76 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 31 Jan 2026 22:26:50 +0100 Subject: [PATCH 01/59] 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/59] 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/59] =?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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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. From 8dc51e7f8508a694ad9bad794259454a1cced942 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Thu, 12 Feb 2026 22:09:47 +0100 Subject: [PATCH 31/59] Use regexes out of headers --- 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 dfb712cdb5f8cb..3486df82677958 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -1,9 +1,9 @@ """Base classes for server/gateway implementations""" from .util import FileWrapper, guess_scheme, is_hop_by_hop -from .headers import Headers +from .headers import Headers, _name_disallowed_re, _value_disallowed_re -import sys, os, time, re +import sys, os, time __all__ = [ 'BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler', From aec77f04f635ce6e81c0243d105d17fbf5b4e863 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Thu, 12 Feb 2026 22:19:06 +0100 Subject: [PATCH 32/59] Remove unused regexes --- Lib/wsgiref/handlers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 3486df82677958..7e7f10e4bee19d 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -16,9 +16,6 @@ "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" % ( From c7b66c456b5c1370abe2efbe0957f86be6b2187b Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 13 Feb 2026 13:54:26 +0100 Subject: [PATCH 33/59] 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 7e7f10e4bee19d..9a4a6c8441bbe1 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -262,7 +262,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 header names, values and statuses") + raise ValueError("Control characters not allowed in headers and status") return value raise AssertionError( "{0} must be of type str (got {1})".format(title, repr(value)) From e763355068585604db474494ed8d9442a6931038 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 13 Feb 2026 13:55:07 +0100 Subject: [PATCH 34/59] Update Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst Co-authored-by: Victor Stinner --- .../Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst | 4 +++- 1 file changed, 3 insertions(+), 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 133a02a0b3fefd..842f4b0032d306 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,3 @@ -Disallow usage of control characters in header names, values and statuses in ``Lib/wsgiref/handlers.py`` for security. Patch by Benedikt Johannes. +Disallow usage of control characters in headers and status +in :mod:`wsgiref.handlers`` to prevent header injection. +Patch by Benedikt Johannes. From 497567628bfa37ef6a7194856ed7648331985928 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 13 Feb 2026 13:56:08 +0100 Subject: [PATCH 35/59] Change this to injections because it's not only header injection, but also status injection --- .../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 842f4b0032d306..449f4c653e637f 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,3 +1,3 @@ Disallow usage of control characters in headers and status -in :mod:`wsgiref.handlers`` to prevent header injection. +in :mod:`wsgiref.handlers`` to prevent injections. Patch by Benedikt Johannes. From ab34b36ca83bc48f785302c45e91b33f792330cc Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 13 Feb 2026 13:58:20 +0100 Subject: [PATCH 36/59] 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 449f4c653e637f..b427f77ae3dad9 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,3 +1,3 @@ Disallow usage of control characters in headers and status -in :mod:`wsgiref.handlers`` to prevent injections. +in :mod:``wsgiref.handlers`` to prevent injections. Patch by Benedikt Johannes. From f0a59ef3f05b8891990b99b71f035bbd0248edf5 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 13 Feb 2026 13:58:29 +0100 Subject: [PATCH 37/59] 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 b427f77ae3dad9..988dd7356d6bb6 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,3 +1,3 @@ Disallow usage of control characters in headers and status -in :mod:``wsgiref.handlers`` to prevent injections. +in :mod:`wsgiref.handlers` to prevent injections. Patch by Benedikt Johannes. From e369b59bc47f9cdd10359b4cd3fa56cd23164683 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 13 Feb 2026 14:28:25 +0100 Subject: [PATCH 38/59] Added Seth Michael Larson to ACKS --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 4295eff8c1e225..ae91d4ece44f38 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1078,6 +1078,7 @@ Wolfgang Langner Detlef Lannert Rémi Lapeyre Soren Larsen +Seth Michael Larson Amos Latteier Keenan Lau Piers Lauder From d952a166702804e6b7582b9ee973979c86eb2dbb Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 00:43:26 +0100 Subject: [PATCH 39/59] Update handlers.py --- Lib/wsgiref/handlers.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 9a4a6c8441bbe1..bf1e5a18f06b88 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -236,14 +236,25 @@ def start_response(self, status, headers,exc_info=None): raise AssertionError("Headers already set!") self.status = status + + # Do not change the next line unless you know you are + # doing because it indirectly prevents injections via C0 control + # characters in the following lines via raising a ValueError + # inside headers_class. self.headers = self.headers_class(headers) - status = self._convert_string_type(status, "Status", name=True) + + status = self._convert_string_type(status, "Status") + + regex = (_name_disallowed_re if name else _value_disallowed_re) + if regex.search(value): + raise ValueError("Control characters are not allowed in headers and status") + self._validate_status(status) if __debug__: for name, val in headers: - name = self._convert_string_type(name, "Header name", name=True) - val = self._convert_string_type(val, "Header value", name=False) + name = self._convert_string_type(name, "Header name") + val = self._convert_string_type(val, "Header value") assert not is_hop_by_hop(name),\ f"Hop-by-hop header, '{name}: {val}', not allowed" @@ -260,9 +271,6 @@ def _validate_status(self, status): 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 status") return value raise AssertionError( "{0} must be of type str (got {1})".format(title, repr(value)) From c0e05d01cd34c99531e1abb7371f71e523514b67 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 00:44:17 +0100 Subject: [PATCH 40/59] 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 bf1e5a18f06b88..de2aa3e3ca5b25 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -268,7 +268,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): + def _convert_string_type(self, value, title): """Convert/check value type.""" if type(value) is str: return value From 76b7444ded18aa6665cecca2b7b308dc6f37188e Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 00:52:01 +0100 Subject: [PATCH 41/59] 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 de2aa3e3ca5b25..1239ea50881578 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -246,7 +246,7 @@ def start_response(self, status, headers,exc_info=None): status = self._convert_string_type(status, "Status") regex = (_name_disallowed_re if name else _value_disallowed_re) - if regex.search(value): + if regex.search(status): raise ValueError("Control characters are not allowed in headers and status") self._validate_status(status) From dd791e71b4ae2e652eb473edcfde3535673bb6fa Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 01:04:49 +0100 Subject: [PATCH 42/59] Update handlers.py --- Lib/wsgiref/handlers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 1239ea50881578..196abf51de884d 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -1,7 +1,7 @@ """Base classes for server/gateway implementations""" from .util import FileWrapper, guess_scheme, is_hop_by_hop -from .headers import Headers, _name_disallowed_re, _value_disallowed_re +from .headers import Headers, _name_disallowed_re import sys, os, time @@ -245,8 +245,7 @@ def start_response(self, status, headers,exc_info=None): status = self._convert_string_type(status, "Status") - regex = (_name_disallowed_re if name else _value_disallowed_re) - if regex.search(status): + if _name_disallowed_re.search(status): raise ValueError("Control characters are not allowed in headers and status") self._validate_status(status) From adb3742d164a9fee6bb4146f401a13121c870572 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 03:17:15 +0100 Subject: [PATCH 43/59] Update 2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst --- .../Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 988dd7356d6bb6..8a08e30eccf145 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,3 +1,2 @@ -Disallow usage of control characters in headers and status -in :mod:`wsgiref.handlers` to prevent injections. +Disallow usage of control characters in statuses in :mod:`wsgiref.handlers` to prevent injections. Patch by Benedikt Johannes. From 2ecd9be2d23375aec81b39e4731ddfd25cb83b92 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 15:05:39 +0100 Subject: [PATCH 44/59] Update Lib/test/test_wsgiref.py Co-authored-by: Victor Stinner --- 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 67d14a0e6358f2..6cc6466b59ec4d 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -860,7 +860,7 @@ def testRaisesControlCharacters(self): with self.subTest(c0): base = BaseHandler() headers = [('x','y')] - self.assertRaises(ValueError, base.start_response, f"{c0}", headers) + self.assertRaises(ValueError, base.start_response, c0, headers) class TestModule(unittest.TestCase): From 0a1dd00b6ebbbee77ddf70bea5f76a1beb05fee4 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 15:09:10 +0100 Subject: [PATCH 45/59] Apply suggestion from @vstinner 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 196abf51de884d..ff9e0363c41054 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -246,7 +246,7 @@ def start_response(self, status, headers,exc_info=None): status = self._convert_string_type(status, "Status") if _name_disallowed_re.search(status): - raise ValueError("Control characters are not allowed in headers and status") + raise ValueError("Control characters are not allowed in status") self._validate_status(status) From 3f150314a7f262696dd8c08a002568a457987b79 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 15:14:21 +0100 Subject: [PATCH 46/59] More usual wording --- 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 ff9e0363c41054..5b6e766cd6f154 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -237,8 +237,8 @@ def start_response(self, status, headers,exc_info=None): self.status = status - # Do not change the next line unless you know you are - # doing because it indirectly prevents injections via C0 control + # The next line should not be changed because it + # indirectly prevents injections via C0 control # characters in the following lines via raising a ValueError # inside headers_class. self.headers = self.headers_class(headers) From 19916baadece65c8604628ed3732e10cd1f1ce4c Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 15:15:28 +0100 Subject: [PATCH 47/59] We use "headers" (plural) in header.py, so we use "statuses" (plural) also in this case --- 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 5b6e766cd6f154..b11d473b7f0b7d 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -246,7 +246,7 @@ def start_response(self, status, headers,exc_info=None): status = self._convert_string_type(status, "Status") if _name_disallowed_re.search(status): - raise ValueError("Control characters are not allowed in status") + raise ValueError("Control characters are not allowed in statuses") self._validate_status(status) From 43d4da8ad9c05b7eb597d420cdade8b51d7c23db Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 15:18:14 +0100 Subject: [PATCH 48/59] Update handlers.py --- Lib/wsgiref/handlers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index b11d473b7f0b7d..3e0668d49a1cfe 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -244,10 +244,6 @@ def start_response(self, status, headers,exc_info=None): self.headers = self.headers_class(headers) status = self._convert_string_type(status, "Status") - - if _name_disallowed_re.search(status): - raise ValueError("Control characters are not allowed in statuses") - self._validate_status(status) if __debug__: @@ -266,6 +262,8 @@ def _validate_status(self, status): raise AssertionError("Status message must begin w/3-digit code") if status[3] != " ": raise AssertionError("Status message must have a space after code") + if _name_disallowed_re.search(status): + raise ValueError("Control characters are not allowed in statuses") def _convert_string_type(self, value, title): """Convert/check value type.""" From d7deb29baaa7f62fc3b181d54c934c48ec59ece6 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sat, 14 Feb 2026 15:47:07 +0100 Subject: [PATCH 49/59] 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 3e0668d49a1cfe..91f855cdaf6a60 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -256,14 +256,14 @@ def start_response(self, status, headers,exc_info=None): return self.write def _validate_status(self, status): + if _name_disallowed_re.search(status): + raise ValueError("Control characters are not allowed in statuses") if len(status) < 4: raise AssertionError("Status must be at least 4 characters") if not status[:3].isdigit(): raise AssertionError("Status message must begin w/3-digit code") if status[3] != " ": raise AssertionError("Status message must have a space after code") - if _name_disallowed_re.search(status): - raise ValueError("Control characters are not allowed in statuses") def _convert_string_type(self, value, title): """Convert/check value type.""" From 6b13a3ef7d6e3a407f41e5159375084c88f601d3 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 14:29:50 +0100 Subject: [PATCH 50/59] Update handlers.py --- Lib/wsgiref/handlers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 91f855cdaf6a60..7a4508a669848e 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -237,10 +237,6 @@ def start_response(self, status, headers,exc_info=None): self.status = status - # The next line should not be changed because it - # indirectly prevents injections via C0 control - # characters in the following lines via raising a ValueError - # inside headers_class. self.headers = self.headers_class(headers) status = self._convert_string_type(status, "Status") From c5cfc3f05eba2d66d5c5f055512a6310bc3823d4 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 14:36:11 +0100 Subject: [PATCH 51/59] Add tests for headers --- Lib/test/test_wsgiref.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 6cc6466b59ec4d..9f7ba641bce3ff 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -859,8 +859,14 @@ def testRaisesControlCharacters(self): for c0 in control_characters_c0(): with self.subTest(c0): base = BaseHandler() - headers = [('x','y')] - self.assertRaises(ValueError, base.start_response, c0, headers) + statusLegit = '200 OK' + statusWithControlCharacters1 = c0 + headersLegit = [('x', 'y')] + headersWithControlCharacters1 = [(c0, 'y')] + headersWithControlCharacters2 = [('x', c0)] + self.assertRaises(ValueError, base.start_response, c0, headersLegit) + self.assertRaises(ValueError, base.start_response, statusLegit, headersWithControlCharacters1) + self.assertRaises(ValueError, base.start_response, statusLegit, headersWithControlCharacters2) class TestModule(unittest.TestCase): From 3a404e152e05d86a20f8c62f87d2119cd2b5c92c Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 14:37:23 +0100 Subject: [PATCH 52/59] Update handlers.py --- Lib/wsgiref/handlers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 7a4508a669848e..0df5706b9d2f1c 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -236,9 +236,7 @@ def start_response(self, status, headers,exc_info=None): raise AssertionError("Headers already set!") self.status = status - self.headers = self.headers_class(headers) - status = self._convert_string_type(status, "Status") self._validate_status(status) From 35d032789e338ae5bd80eaa6ef12140a3ba77101 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 15:07:48 +0100 Subject: [PATCH 53/59] Update test_wsgiref.py --- Lib/test/test_wsgiref.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 9f7ba641bce3ff..1f547481ba706c 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -858,15 +858,15 @@ def write(self, b): def testRaisesControlCharacters(self): for c0 in control_characters_c0(): with self.subTest(c0): - base = BaseHandler() + base1, base2, base3 = [BaseHandler() for _ in range(3)] statusLegit = '200 OK' statusWithControlCharacters1 = c0 headersLegit = [('x', 'y')] headersWithControlCharacters1 = [(c0, 'y')] headersWithControlCharacters2 = [('x', c0)] - self.assertRaises(ValueError, base.start_response, c0, headersLegit) - self.assertRaises(ValueError, base.start_response, statusLegit, headersWithControlCharacters1) - self.assertRaises(ValueError, base.start_response, statusLegit, headersWithControlCharacters2) + self.assertRaises(ValueError, base1.start_response, c0, headersLegit) + self.assertRaises(ValueError, base2.start_response, statusLegit, headersWithControlCharacters1) + self.assertRaises(ValueError, base3.start_response, statusLegit, headersWithControlCharacters2) class TestModule(unittest.TestCase): From 8642a38cbb93b68108523f584263c0accf7a6b0f Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 16:40:54 +0100 Subject: [PATCH 54/59] 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 1f547481ba706c..3f0a6d77160590 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -866,7 +866,11 @@ def testRaisesControlCharacters(self): headersWithControlCharacters2 = [('x', c0)] self.assertRaises(ValueError, base1.start_response, c0, headersLegit) self.assertRaises(ValueError, base2.start_response, statusLegit, headersWithControlCharacters1) - self.assertRaises(ValueError, base3.start_response, statusLegit, headersWithControlCharacters2) + # HTAB (\x09) is allowed in header values, but not in names. + if c0 != "\t": + self.assertRaises(ValueError, base3.start_response, statusLegit, headersWithControlCharacters2) + else: + base.start_response(statusLegit, headersWithControlCharacters2) class TestModule(unittest.TestCase): From 673cbc19b446703b662365dda72fd06c9b5222ba Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 16:53:37 +0100 Subject: [PATCH 55/59] 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 3f0a6d77160590..b065bc735bff2a 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -870,7 +870,7 @@ def testRaisesControlCharacters(self): if c0 != "\t": self.assertRaises(ValueError, base3.start_response, statusLegit, headersWithControlCharacters2) else: - base.start_response(statusLegit, headersWithControlCharacters2) + base3.start_response(statusLegit, headersWithControlCharacters2) class TestModule(unittest.TestCase): From 563de46858fe5043dde3ec06baa4fafc8862b674 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 22:50:18 +0100 Subject: [PATCH 56/59] 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 0df5706b9d2f1c..b82862deea7d74 100644 --- a/Lib/wsgiref/handlers.py +++ b/Lib/wsgiref/handlers.py @@ -251,7 +251,7 @@ def start_response(self, status, headers,exc_info=None): def _validate_status(self, status): if _name_disallowed_re.search(status): - raise ValueError("Control characters are not allowed in statuses") + raise ValueError("Control characters are not allowed in status") if len(status) < 4: raise AssertionError("Status must be at least 4 characters") if not status[:3].isdigit(): From d016dbe4119480ec2acbf04851301ca160572451 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 22:51:23 +0100 Subject: [PATCH 57/59] Update Lib/test/test_wsgiref.py Co-authored-by: Victor Stinner --- Lib/test/test_wsgiref.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index b065bc735bff2a..3379df37d38ca8 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -858,19 +858,21 @@ def write(self, b): def testRaisesControlCharacters(self): for c0 in control_characters_c0(): with self.subTest(c0): - base1, base2, base3 = [BaseHandler() for _ in range(3)] - statusLegit = '200 OK' - statusWithControlCharacters1 = c0 - headersLegit = [('x', 'y')] - headersWithControlCharacters1 = [(c0, 'y')] - headersWithControlCharacters2 = [('x', c0)] - self.assertRaises(ValueError, base1.start_response, c0, headersLegit) - self.assertRaises(ValueError, base2.start_response, statusLegit, headersWithControlCharacters1) + base = BaseHandler() + with self.assertRaises(ValueError): + base.start_response(c0, [('x', 'y')]) + + base = BaseHandler() + with self.assertRaises(ValueError): + base.start_response('200 OK', [(c0, 'y')]) + # HTAB (\x09) is allowed in header values, but not in names. + base = BaseHandler() if c0 != "\t": - self.assertRaises(ValueError, base3.start_response, statusLegit, headersWithControlCharacters2) + with self.assertRaises(ValueError): + base.start_response('200 OK', [('x', c0)]) else: - base3.start_response(statusLegit, headersWithControlCharacters2) + base.start_response('200 OK', [('x', c0)]) class TestModule(unittest.TestCase): From 8dd1536c80fe2470f5ba0d5d98520c267130d809 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 22:54:08 +0100 Subject: [PATCH 58/59] 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 8a08e30eccf145..2c52bcb513cfe0 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,2 +1,2 @@ -Disallow usage of control characters in statuses in :mod:`wsgiref.handlers` to prevent injections. +Disallow usage of control characters in status in :mod:`wsgiref.handlers` to prevent injections. Patch by Benedikt Johannes. From bc7fb3feb1a71f4d00368577800379409652d49d Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Mon, 16 Feb 2026 22:56:33 +0100 Subject: [PATCH 59/59] Update Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst Co-authored-by: Victor Stinner --- .../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 2c52bcb513cfe0..2d13a0611322c5 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,2 +1,2 @@ -Disallow usage of control characters in status in :mod:`wsgiref.handlers` to prevent injections. +Disallow usage of control characters in status in :mod:`wsgiref.handlers` to prevent HTTP header injections. Patch by Benedikt Johannes.