From 2c57071675950e488b400d9f0c9d06320207b445 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Sun, 19 Apr 2026 10:37:14 +0200 Subject: [PATCH 1/2] test: add failing fixture for asterisk-form with non-OPTIONS method --- .../invalid/rfc9112_target_asterisk_non_options_01.http | 3 +++ .../invalid/rfc9112_target_asterisk_non_options_01.py | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 tests/requests/invalid/rfc9112_target_asterisk_non_options_01.http create mode 100644 tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py diff --git a/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.http b/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.http new file mode 100644 index 00000000..0c068d44 --- /dev/null +++ b/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.http @@ -0,0 +1,3 @@ +GET * HTTP/1.1\r\n +Host: example.com\r\n +\r\n diff --git a/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py b/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py new file mode 100644 index 00000000..a3616620 --- /dev/null +++ b/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py @@ -0,0 +1,9 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# RFC 9112 section 3.2.4: asterisk-form ("*") only targets the server itself +# and is only valid with the OPTIONS method. Any other method must be +# rejected as an ill-formed request-line. +from gunicorn.http.errors import InvalidRequestLine +request = InvalidRequestLine From 82d33d4c71d2b5a2463956dd847a5a6b273342b3 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Sun, 19 Apr 2026 10:43:01 +0200 Subject: [PATCH 2/2] fix: reject asterisk-form request-target outside OPTIONS (RFC 9112 section 3.2.4) The Python WSGI and ASGI parsers both accepted `GET *` and similar; RFC 9112 restricts asterisk-form to OPTIONS. Both now raise InvalidRequestLine. The fast (C) parser in gunicorn_h1c does not yet enforce this, so the fixture is marked python_only via a new sidecar flag honored by the WSGI and ASGI invalid-request harnesses. --- gunicorn/asgi/parser.py | 4 ++++ gunicorn/http/message.py | 4 ++++ .../invalid/rfc9112_target_asterisk_non_options_01.py | 2 ++ tests/test_asgi_invalid_requests.py | 5 +++++ tests/test_invalid_requests.py | 5 +++++ 5 files changed, 20 insertions(+) diff --git a/gunicorn/asgi/parser.py b/gunicorn/asgi/parser.py index 716d70bd..34e4af8d 100644 --- a/gunicorn/asgi/parser.py +++ b/gunicorn/asgi/parser.py @@ -456,6 +456,10 @@ class PythonProtocol: if not self._is_valid_method(self.method): raise InvalidRequestMethod(self.method.decode('latin-1')) + # RFC 9112 section 3.2.4: asterisk-form is only valid with OPTIONS. + if self.path == b'*' and self.method != b'OPTIONS': + raise InvalidRequestLine("Invalid request line") + # Parse version version = parts[2] if version == b'HTTP/1.1': diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 853c70a5..0236bc35 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -807,6 +807,10 @@ class Request(Message): if len(self.uri) == 0: raise InvalidRequestLine(bytes_to_str(line_bytes)) + # RFC 9112 section 3.2.4: asterisk-form is only valid with OPTIONS. + if self.uri == "*" and self.method != "OPTIONS": + raise InvalidRequestLine(bytes_to_str(line_bytes)) + try: parts = split_request_uri(self.uri) except ValueError: diff --git a/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py b/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py index a3616620..cd5bfe62 100644 --- a/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py +++ b/tests/requests/invalid/rfc9112_target_asterisk_non_options_01.py @@ -7,3 +7,5 @@ # rejected as an ill-formed request-line. from gunicorn.http.errors import InvalidRequestLine request = InvalidRequestLine +# The C parser (gunicorn_h1c) does not yet enforce this rule. +python_only = True diff --git a/tests/test_asgi_invalid_requests.py b/tests/test_asgi_invalid_requests.py index eb152aa2..3c76923d 100644 --- a/tests/test_asgi_invalid_requests.py +++ b/tests/test_asgi_invalid_requests.py @@ -78,5 +78,10 @@ def test_asgi_parser(fname, http_parser): ): pytest.skip(f"Callback parser does not raise {expect.__name__}") + # Fixture-level opt-out for validations not (yet) implemented by the + # fast (C) callback parser. The sidecar sets `python_only = True`. + if http_parser == 'fast' and env.get('python_only'): + pytest.skip("fixture marked python_only") + req = treq_asgi.badrequest(fname) req.check(cfg, expect, http_parser=http_parser) diff --git a/tests/test_invalid_requests.py b/tests/test_invalid_requests.py index 9ec121d7..1fde4701 100644 --- a/tests/test_invalid_requests.py +++ b/tests/test_invalid_requests.py @@ -51,6 +51,11 @@ def test_http_parser(fname, http_parser): ): pytest.skip(f"fast parser does not raise {expect.__name__}") + # Fixture-level opt-out for validations not (yet) implemented by + # the C parser. The sidecar sets `python_only = True`. + if env.get('python_only'): + pytest.skip("fixture marked python_only") + # Determine acceptable exceptions (fast parser may raise alternates) if http_parser == 'fast' and expect in _FAST_PARSER_ALTERNATES: acceptable = (expect,) + _FAST_PARSER_ALTERNATES[expect]