From 826bfc7e8806e41c52b343ec7f9544c40e1f6b8c Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Sun, 19 Apr 2026 12:05:00 +0200 Subject: [PATCH 1/2] test: add failing fixtures for control chars in header value --- .../invalid/rfc9110_field_value_ctl_bel_01.http | 4 ++++ .../requests/invalid/rfc9110_field_value_ctl_bel_01.py | 10 ++++++++++ .../invalid/rfc9110_field_value_ctl_del_01.http | 4 ++++ .../requests/invalid/rfc9110_field_value_ctl_del_01.py | 9 +++++++++ 4 files changed, 27 insertions(+) create mode 100644 tests/requests/invalid/rfc9110_field_value_ctl_bel_01.http create mode 100644 tests/requests/invalid/rfc9110_field_value_ctl_bel_01.py create mode 100644 tests/requests/invalid/rfc9110_field_value_ctl_del_01.http create mode 100644 tests/requests/invalid/rfc9110_field_value_ctl_del_01.py diff --git a/tests/requests/invalid/rfc9110_field_value_ctl_bel_01.http b/tests/requests/invalid/rfc9110_field_value_ctl_bel_01.http new file mode 100644 index 00000000..de9cee5e --- /dev/null +++ b/tests/requests/invalid/rfc9110_field_value_ctl_bel_01.http @@ -0,0 +1,4 @@ +GET /foo HTTP/1.1\r\n +Host: example.com\r\n +X-Value: plain\x07injected\r\n +\r\n diff --git a/tests/requests/invalid/rfc9110_field_value_ctl_bel_01.py b/tests/requests/invalid/rfc9110_field_value_ctl_bel_01.py new file mode 100644 index 00000000..4203cfe9 --- /dev/null +++ b/tests/requests/invalid/rfc9110_field_value_ctl_bel_01.py @@ -0,0 +1,10 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# RFC 9110 section 5.5: field-value characters are field-vchar (VCHAR + +# obs-text) plus SP/HTAB. Control characters other than HTAB must not +# appear, to prevent log/response injection and parser confusion. +from gunicorn.http.errors import InvalidHeader +request = InvalidHeader +python_only = True diff --git a/tests/requests/invalid/rfc9110_field_value_ctl_del_01.http b/tests/requests/invalid/rfc9110_field_value_ctl_del_01.http new file mode 100644 index 00000000..88760579 --- /dev/null +++ b/tests/requests/invalid/rfc9110_field_value_ctl_del_01.http @@ -0,0 +1,4 @@ +GET /foo HTTP/1.1\r\n +Host: example.com\r\n +X-Value: plain\x7finjected\r\n +\r\n diff --git a/tests/requests/invalid/rfc9110_field_value_ctl_del_01.py b/tests/requests/invalid/rfc9110_field_value_ctl_del_01.py new file mode 100644 index 00000000..cd6cd1aa --- /dev/null +++ b/tests/requests/invalid/rfc9110_field_value_ctl_del_01.py @@ -0,0 +1,9 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# RFC 9110 section 5.5: DEL (0x7F) is a control character and not a VCHAR; +# it must not appear in a field-value. +from gunicorn.http.errors import InvalidHeader +request = InvalidHeader +python_only = True From 2073e13dc80969074954cabd61c01d5e1f427d7f Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Sun, 19 Apr 2026 12:07:16 +0200 Subject: [PATCH 2/2] fix: reject control characters in header field-value (RFC 9110 section 5.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit field-vchar = VCHAR / obs-text; only SP and HTAB are permitted beyond that. Previous validation only caught NUL/CR/LF, leaving BEL, DEL, FF, and other C0/C1 controls accepted — a log/response injection risk. Now rejected across the WSGI and ASGI Python parsers. --- gunicorn/asgi/parser.py | 7 +++++-- gunicorn/http/message.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/gunicorn/asgi/parser.py b/gunicorn/asgi/parser.py index 9e3f70f2..2fc4a3d1 100644 --- a/gunicorn/asgi/parser.py +++ b/gunicorn/asgi/parser.py @@ -804,8 +804,11 @@ class PythonProtocol: return True def _has_invalid_header_chars(self, value): - """Check for NUL, CR, LF in header value.""" - return b'\x00' in value or b'\r' in value or b'\n' in value + """RFC 9110 section 5.5: only VCHAR, SP, HTAB, and obs-text allowed.""" + for c in value: + if c <= 0x08 or 0x0a <= c <= 0x1f or c == 0x7f: + return True + return False class CallbackRequest: diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index b27f40a5..1cc94fb7 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -130,7 +130,10 @@ TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIAL METHOD_BADCHAR_RE = re.compile("[a-z#]") # usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)") -RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]") +# RFC 9110 section 5.5: field-vchar = VCHAR / obs-text; SP and HTAB are the +# only non-VCHAR bytes allowed in a field-value. Anything else in the +# control range (0x00-0x1F except HTAB, plus DEL 0x7F) must be rejected. +RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]") # RFC 9110 section 6.5.1: fields forbidden in trailers because they alter # routing, framing, or authentication. Using the uppercased names stored