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.. diff --git a/tests/requests/invalid/rfc9110_trailer_forbidden_cl_01.http b/tests/requests/invalid/rfc9110_trailer_forbidden_cl_01.http new file mode 100644 index 00000000..7562e1a8 --- /dev/null +++ b/tests/requests/invalid/rfc9110_trailer_forbidden_cl_01.http @@ -0,0 +1,9 @@ +POST /p HTTP/1.1\r\n +Host: example.com\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +0\r\n +Content-Length: 99\r\n +\r\n diff --git a/tests/requests/invalid/rfc9110_trailer_forbidden_cl_01.py b/tests/requests/invalid/rfc9110_trailer_forbidden_cl_01.py new file mode 100644 index 00000000..b0f8ccae --- /dev/null +++ b/tests/requests/invalid/rfc9110_trailer_forbidden_cl_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 6.5.1: Content-Length in trailers is a classic +# smuggling vector; origin must reject. +from gunicorn.http.errors import InvalidHeaderName +request = InvalidHeaderName +python_only = True diff --git a/tests/requests/invalid/rfc9110_trailer_forbidden_host_01.http b/tests/requests/invalid/rfc9110_trailer_forbidden_host_01.http new file mode 100644 index 00000000..348082e7 --- /dev/null +++ b/tests/requests/invalid/rfc9110_trailer_forbidden_host_01.http @@ -0,0 +1,9 @@ +POST /p HTTP/1.1\r\n +Host: example.com\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +0\r\n +Host: evil.example.com\r\n +\r\n diff --git a/tests/requests/invalid/rfc9110_trailer_forbidden_host_01.py b/tests/requests/invalid/rfc9110_trailer_forbidden_host_01.py new file mode 100644 index 00000000..fe897329 --- /dev/null +++ b/tests/requests/invalid/rfc9110_trailer_forbidden_host_01.py @@ -0,0 +1,11 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# RFC 9110 section 6.5.1: certain header fields must not be sent in +# trailers because they alter routing or message framing (e.g. Host, +# Content-Length, Transfer-Encoding). Accepting them enables smuggling. +from gunicorn.http.errors import InvalidHeaderName +request = InvalidHeaderName +# The C parser (gunicorn_h1c) does not yet enforce this rule. +python_only = True diff --git a/tests/requests/invalid/rfc9110_trailer_forbidden_te_01.http b/tests/requests/invalid/rfc9110_trailer_forbidden_te_01.http new file mode 100644 index 00000000..e5c98d16 --- /dev/null +++ b/tests/requests/invalid/rfc9110_trailer_forbidden_te_01.http @@ -0,0 +1,9 @@ +POST /p HTTP/1.1\r\n +Host: example.com\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +0\r\n +Transfer-Encoding: chunked\r\n +\r\n diff --git a/tests/requests/invalid/rfc9110_trailer_forbidden_te_01.py b/tests/requests/invalid/rfc9110_trailer_forbidden_te_01.py new file mode 100644 index 00000000..caa2f5ac --- /dev/null +++ b/tests/requests/invalid/rfc9110_trailer_forbidden_te_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 6.5.1: Transfer-Encoding in trailers alters framing +# and must not be accepted. +from gunicorn.http.errors import InvalidHeaderName +request = InvalidHeaderName +python_only = True