From a9270e3f9aceee307b1757fbe643970f5fc84d1b Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Sun, 19 Apr 2026 11:41:00 +0200 Subject: [PATCH] fix: reject forbidden trailer field-names (RFC 9110 section 6.5.1) Host, Content-Length, Transfer-Encoding, Trailer, Authorization, and TE are not allowed in trailer sections; accepting them enables smuggling and routing confusion. Both WSGI and ASGI Python parsers now raise InvalidHeaderName when any of these appears in a trailer. --- gunicorn/asgi/parser.py | 20 ++++++++++++++++++++ gunicorn/http/message.py | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/gunicorn/asgi/parser.py b/gunicorn/asgi/parser.py index 3f4c8bea..9e3f70f2 100644 --- a/gunicorn/asgi/parser.py +++ b/gunicorn/asgi/parser.py @@ -29,6 +29,18 @@ class InvalidProxyHeader(ParseError): PP_V2_SIGNATURE = b"\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A" +# RFC 9110 section 6.5.1: fields forbidden in trailers because they alter +# routing, framing, or authentication. +RFC9110_6_5_1_FORBIDDEN_TRAILER = frozenset(( + b"host", + b"content-length", + b"transfer-encoding", + b"trailer", + b"authorization", + b"te", +)) + + class PPCommand(IntEnum): """PROXY protocol v2 commands.""" LOCAL = 0x0 @@ -756,6 +768,14 @@ class PythonProtocol: self._on_message_complete() return True + # RFC 9110 section 6.5.1: reject fields that must not appear + # in trailers. + colon = line.find(b':') + if colon > 0: + name = line[:colon].strip(b' \t').lower() + if name in RFC9110_6_5_1_FORBIDDEN_TRAILER: + raise InvalidHeaderName(name.decode('latin-1')) + return False def _is_valid_method(self, method): diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 7e835f9f..b27f40a5 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -132,6 +132,18 @@ METHOD_BADCHAR_RE = re.compile("[a-z#]") VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)") RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]") +# RFC 9110 section 6.5.1: fields forbidden in trailers because they alter +# routing, framing, or authentication. Using the uppercased names stored +# by parse_headers. +RFC9110_6_5_1_FORBIDDEN_TRAILER = frozenset(( + "HOST", + "CONTENT-LENGTH", + "TRANSFER-ENCODING", + "TRAILER", + "AUTHORIZATION", + "TE", +)) + def _ip_in_allow_list(ip_str, allow_list, networks): """Check if IP address is in the allow list. @@ -235,6 +247,10 @@ class Message: # b"\xDF".decode("latin-1").upper().encode("ascii") == b"SS" name = name.upper() + # RFC 9110 section 6.5.1 + if from_trailer and name in RFC9110_6_5_1_FORBIDDEN_TRAILER: + raise InvalidHeaderName(name) + value = [value.strip(" \t")] # Consume value continuation lines..