Skip to content

Commit 7ebe442

Browse files
pajodkenballus
andcommitted
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 <[email protected]>
1 parent f550111 commit 7ebe442

File tree

8 files changed

+43
-1
lines changed

8 files changed

+43
-1
lines changed

gunicorn/config.py

+20
Original file line numberDiff line numberDiff line change
@@ -2282,6 +2282,26 @@ class PermitUnconventionalHTTPMethod(Setting):
22822282
"""
22832283

22842284

2285+
class PermitUnconventionalHTTPVersion(Setting):
2286+
name = "permit_unconventional_http_version"
2287+
section = "Server Mechanics"
2288+
cli = ["--permit-unconventional-http-version"]
2289+
validator = validate_bool
2290+
action = "store_true"
2291+
default = False
2292+
desc = """\
2293+
Permit HTTP version not matching conventions of 2023
2294+
2295+
This disables the refusal of likely malformed request lines.
2296+
It is unusual to specify HTTP 1 versions other than 1.0 and 1.1.
2297+
2298+
This option is provided to diagnose backwards-incompatible changes.
2299+
Use with care and only if necessary. May be removed in a future version.
2300+
2301+
.. versionadded:: 22.0.0
2302+
"""
2303+
2304+
22852305
class CasefoldHTTPMethod(Setting):
22862306
name = "casefold_http_method"
22872307
section = "Server Mechanics"

gunicorn/http/message.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
RFC9110_5_6_2_TOKEN_SPECIALS = r"!#$%&'*+-.^_`|~"
2727
TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS)))
2828
METHOD_BADCHAR_RE = re.compile("[a-z#]")
29-
VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)")
29+
# usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions
30+
VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)")
3031

3132

3233
class Message(object):
@@ -438,6 +439,10 @@ def parse_request_line(self, line_bytes):
438439
if match is None:
439440
raise InvalidHTTPVersion(bits[2])
440441
self.version = (int(match.group(1)), int(match.group(2)))
442+
if not (1, 0) <= self.version < (2, 0):
443+
# if ever relaxing this, carefully review Content-Encoding processing
444+
if not self.cfg.permit_unconventional_http_version:
445+
raise InvalidHTTPVersion(self.version)
441446

442447
def set_body_reader(self):
443448
super().set_body_reader()

tests/requests/invalid/prefix_06.http

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
GET /the/future HTTP/1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1\r\n
2+
Content-Length: 7\r\n
3+
\r\n
4+
Old Man

tests/requests/invalid/prefix_06.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from gunicorn.config import Config
2+
from gunicorn.http.errors import InvalidHTTPVersion
3+
4+
cfg = Config()
5+
request = InvalidHTTPVersion
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GET /foo HTTP/0.99\r\n
2+
\r\n

tests/requests/invalid/version_01.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from gunicorn.http.errors import InvalidHTTPVersion
2+
request = InvalidHTTPVersion
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GET /foo HTTP/2.0\r\n
2+
\r\n

tests/requests/invalid/version_02.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from gunicorn.http.errors import InvalidHTTPVersion
2+
request = InvalidHTTPVersion

0 commit comments

Comments
 (0)