From 50ceb41813caa85828b22ac2072fea6e181e95ce Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Fri, 6 Mar 2026 13:22:21 +0100 Subject: [PATCH] gh-144370: Disallow usage of control characters in status in wsgiref.handlers for security (#144371) Disallow usage of control characters in status in wsgiref.handlers to prevent HTTP header injections. Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Victor Stinner (cherry picked from commit d931725bc850cd096f6703bc285e885f1e015f05) --- Lib/test/test_wsgiref.py | 19 +++++++++++++++++++ Lib/wsgiref/handlers.py | 4 +++- Misc/ACKS | 1 + ...-01-31-21-56-54.gh-issue-144370.fp9m8t.rst | 2 ++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 7c6c72f6e81384..a2b07f6aa345d7 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -879,6 +879,25 @@ 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() + 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": + with self.assertRaises(ValueError): + base.start_response('200 OK', [('x', c0)]) + else: + base.start_response('200 OK', [('x', c0)]) + if __name__ == "__main__": unittest.main() diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py index 31360e58785ac6..df30af39293d2c 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 +from .headers import Headers, _name_disallowed_re import sys, os, time @@ -238,6 +238,8 @@ 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") + if _name_disallowed_re.search(status): + raise ValueError("Control characters are not allowed in status") assert len(status)>=4,"Status must be at least 4 characters" assert status[:3].isdigit(), "Status message must begin w/3-digit code" assert status[3]==" ", "Status message must have a space after code" diff --git a/Misc/ACKS b/Misc/ACKS index 399b1d2cf58c9a..e12b6ea25d3d2d 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1006,6 +1006,7 @@ Wolfgang Langner Detlef Lannert Rémi Lapeyre Soren Larsen +Seth Michael Larson Amos Latteier Piers Lauder Ben Laurie 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..2d13a0611322c5 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-01-31-21-56-54.gh-issue-144370.fp9m8t.rst @@ -0,0 +1,2 @@ +Disallow usage of control characters in status in :mod:`wsgiref.handlers` to prevent HTTP header injections. +Patch by Benedikt Johannes.