From 7ebe442d089a6fe0c51abb19a598b3d0d6a6d128 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Thu, 7 Dec 2023 09:56:49 +0100 Subject: [PATCH] strict HTTP version validation Note: This is unrelated to a reverse proxy potentially talking HTTP/3 to clients. This is about the HTTP protocol version spoken to Gunicorn, which is HTTP/1.0 or HTTP/1.1. Little legitimate need for processing HTTP 1 requests with ambiguous version numbers. Broadly refuse. Co-authored-by: Ben Kallus --- gunicorn/config.py | 20 ++++++++++++++++++++ gunicorn/http/message.py | 7 ++++++- tests/requests/invalid/prefix_06.http | 4 ++++ tests/requests/invalid/prefix_06.py | 5 +++++ tests/requests/invalid/version_01.http | 2 ++ tests/requests/invalid/version_01.py | 2 ++ tests/requests/invalid/version_02.http | 2 ++ tests/requests/invalid/version_02.py | 2 ++ 8 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/requests/invalid/prefix_06.http create mode 100644 tests/requests/invalid/prefix_06.py create mode 100644 tests/requests/invalid/version_01.http create mode 100644 tests/requests/invalid/version_01.py create mode 100644 tests/requests/invalid/version_02.http create mode 100644 tests/requests/invalid/version_02.py diff --git a/gunicorn/config.py b/gunicorn/config.py index be9bb001..e7e4fac5 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -2282,6 +2282,26 @@ class PermitUnconventionalHTTPMethod(Setting): """ +class PermitUnconventionalHTTPVersion(Setting): + name = "permit_unconventional_http_version" + section = "Server Mechanics" + cli = ["--permit-unconventional-http-version"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Permit HTTP version not matching conventions of 2023 + + This disables the refusal of likely malformed request lines. + It is unusual to specify HTTP 1 versions other than 1.0 and 1.1. + + This option is provided to diagnose backwards-incompatible changes. + Use with care and only if necessary. May be removed in a future version. + + .. versionadded:: 22.0.0 + """ + + class CasefoldHTTPMethod(Setting): name = "casefold_http_method" section = "Server Mechanics" diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 4e1c2fd5..5e8d2427 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -26,7 +26,8 @@ DEFAULT_MAX_HEADERFIELD_SIZE = 8190 RFC9110_5_6_2_TOKEN_SPECIALS = r"!#$%&'*+-.^_`|~" TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS))) METHOD_BADCHAR_RE = re.compile("[a-z#]") -VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)") +# usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions +VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)") class Message(object): @@ -438,6 +439,10 @@ class Request(Message): if match is None: raise InvalidHTTPVersion(bits[2]) self.version = (int(match.group(1)), int(match.group(2))) + if not (1, 0) <= self.version < (2, 0): + # if ever relaxing this, carefully review Content-Encoding processing + if not self.cfg.permit_unconventional_http_version: + raise InvalidHTTPVersion(self.version) def set_body_reader(self): super().set_body_reader() diff --git a/tests/requests/invalid/prefix_06.http b/tests/requests/invalid/prefix_06.http new file mode 100644 index 00000000..536c586a --- /dev/null +++ b/tests/requests/invalid/prefix_06.http @@ -0,0 +1,4 @@ +GET /the/future HTTP/1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1\r\n +Content-Length: 7\r\n +\r\n +Old Man diff --git a/tests/requests/invalid/prefix_06.py b/tests/requests/invalid/prefix_06.py new file mode 100644 index 00000000..b2286d42 --- /dev/null +++ b/tests/requests/invalid/prefix_06.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHTTPVersion + +cfg = Config() +request = InvalidHTTPVersion diff --git a/tests/requests/invalid/version_01.http b/tests/requests/invalid/version_01.http new file mode 100644 index 00000000..0a222ce1 --- /dev/null +++ b/tests/requests/invalid/version_01.http @@ -0,0 +1,2 @@ +GET /foo HTTP/0.99\r\n +\r\n diff --git a/tests/requests/invalid/version_01.py b/tests/requests/invalid/version_01.py new file mode 100644 index 00000000..760840b6 --- /dev/null +++ b/tests/requests/invalid/version_01.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHTTPVersion +request = InvalidHTTPVersion diff --git a/tests/requests/invalid/version_02.http b/tests/requests/invalid/version_02.http new file mode 100644 index 00000000..6d29ac5f --- /dev/null +++ b/tests/requests/invalid/version_02.http @@ -0,0 +1,2 @@ +GET /foo HTTP/2.0\r\n +\r\n diff --git a/tests/requests/invalid/version_02.py b/tests/requests/invalid/version_02.py new file mode 100644 index 00000000..760840b6 --- /dev/null +++ b/tests/requests/invalid/version_02.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHTTPVersion +request = InvalidHTTPVersion