Skip to content

Commit

Permalink
strict HTTP version validation
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
pajod and kenballus committed Dec 15, 2023
1 parent 0c254a7 commit f5280b2
Show file tree
Hide file tree
Showing 8 changed files with 43 additions and 1 deletion.
20 changes: 20 additions & 0 deletions gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 6 additions & 1 deletion gunicorn/http/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
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):
Expand Down Expand Up @@ -438,6 +439,10 @@ def parse_request_line(self, line_bytes):
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()
Expand Down
4 changes: 4 additions & 0 deletions tests/requests/invalid/prefix_06.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
GET /the/future HTTP/1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1\r\n
Content-Length: 7\r\n
\r\n
Old Man
5 changes: 5 additions & 0 deletions tests/requests/invalid/prefix_06.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from gunicorn.config import Config
from gunicorn.http.errors import InvalidHTTPVersion

cfg = Config()
request = InvalidHTTPVersion
2 changes: 2 additions & 0 deletions tests/requests/invalid/version_01.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GET /foo HTTP/0.99\r\n
\r\n
2 changes: 2 additions & 0 deletions tests/requests/invalid/version_01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from gunicorn.http.errors import InvalidHTTPVersion
request = InvalidHTTPVersion
2 changes: 2 additions & 0 deletions tests/requests/invalid/version_02.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GET /foo HTTP/2.0\r\n
\r\n
2 changes: 2 additions & 0 deletions tests/requests/invalid/version_02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from gunicorn.http.errors import InvalidHTTPVersion
request = InvalidHTTPVersion

0 comments on commit f5280b2

Please sign in to comment.