diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index aa87fd6afcac5c..2d14cade188698 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -3216,3 +3216,10 @@ def linked_to_musl(): return _linked_to_musl _linked_to_musl = tuple(map(int, version.split('.'))) return _linked_to_musl + + +def control_characters_c0() -> list[str]: + """Returns a list of C0 control characters as strings. + C0 control characters defined as the byte range 0x00-0x1F, and 0x7F. + """ + return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"] diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index e04a4d2c2218a3..0bf9e947b5e48e 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -1,6 +1,6 @@ from unittest import mock from test import support -from test.support import socket_helper +from test.support import socket_helper, control_characters_c0 from test.test_httpservers import NoLogRequestHandler from unittest import TestCase from wsgiref.util import setup_testing_defaults @@ -503,6 +503,16 @@ def testExtras(self): '\r\n' ) + def testRaisesControlCharacters(self): + headers = Headers() + for c0 in control_characters_c0(): + self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") + self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") + self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") + self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") + self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") + + class ErrorHandler(BaseCGIHandler): """Simple handler subclass for testing BaseHandler""" diff --git a/Lib/wsgiref/headers.py b/Lib/wsgiref/headers.py index c78879f80c7df2..e180a623cb2c30 100644 --- a/Lib/wsgiref/headers.py +++ b/Lib/wsgiref/headers.py @@ -9,6 +9,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. @@ -41,6 +42,8 @@ def __init__(self, headers=None): 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))) diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst new file mode 100644 index 00000000000000..44bd0b27059f94 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst @@ -0,0 +1,2 @@ +Reject C0 control characters within wsgiref.headers.Headers fields, values, +and parameters.