strict: header field validation: stop casefolding

* refusing lowercase and ASCII 0x23 (#) had been partially enforced before
* do not casefold by default, HTTP methods are case sensitive
This commit is contained in:
Paul J. Dorn 2023-12-07 18:46:31 +01:00
parent 42dd4190ac
commit b2846783d7
11 changed files with 105 additions and 8 deletions

View File

@ -2254,5 +2254,49 @@ class StripHeaderSpaces(Setting):
This is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard.
See https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn.
Use with care and only if necessary.
Use with care and only if necessary. May be removed in a future version.
.. versionadded:: 20.0.1
"""
class PermitUnconventionalHTTPMethod(Setting):
name = "permit_unconventional_http_method"
section = "Server Mechanics"
cli = ["--permit-unconventional-http-method"]
validator = validate_bool
action = "store_true"
default = False
desc = """\
Permit HTTP methods not matching conventions, such as IANA registration guidelines
This permits request methods of length less than 3 or more than 20,
methods with lowercase characters or methods containing the # character.
HTTP methods are case sensitive by definition, and merely uppercase by convention.
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"
cli = ["--casefold-http-method"]
validator = validate_bool
action = "store_true"
default = False
desc = """\
Transform received HTTP methods to uppercase
HTTP methods are case sensitive by definition, and merely uppercase by convention.
This option is provided because previous versions of gunicorn defaulted to this behaviour.
Use with care and only if necessary. May be removed in a future version.
.. versionadded:: 22.0.0
"""

View File

@ -21,7 +21,10 @@ MAX_REQUEST_LINE = 8190
MAX_HEADERS = 32768
DEFAULT_MAX_HEADERFIELD_SIZE = 8190
TOKEN_RE = re.compile(r"[!#$%&'*+\-.\^_`|~0-9a-zA-Z]+")
# verbosely on purpose, avoid backslash ambiguity
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+)")
@ -331,10 +334,23 @@ class Request(Message):
if len(bits) != 3:
raise InvalidRequestLine(bytes_to_str(line_bytes))
# Method
if not TOKEN_RE.fullmatch(bits[0]):
raise InvalidRequestMethod(bits[0])
self.method = bits[0].upper()
# Method: RFC9110 Section 9
self.method = bits[0]
# nonstandard restriction, suitable for all IANA registered methods
# partially enforced in previous gunicorn versions
if not self.cfg.permit_unconventional_http_method:
if METHOD_BADCHAR_RE.search(self.method):
raise InvalidRequestMethod(self.method)
if not 3 <= len(bits[0]) <= 20:
raise InvalidRequestMethod(self.method)
# standard restriction: RFC9110 token
if not TOKEN_RE.fullmatch(self.method):
raise InvalidRequestMethod(self.method)
# nonstandard and dangerous
# methods are merely uppercase by convention, no case-insensitive treatment is intended
if self.cfg.casefold_http_method:
self.method = self.method.upper()
# URI
self.uri = bits[1]

View File

@ -0,0 +1,2 @@
bla:rgh /foo HTTP/1.1\r\n
\r\n

View File

@ -0,0 +1,2 @@
from gunicorn.http.errors import InvalidRequestMethod
request = InvalidRequestMethod

View File

@ -0,0 +1,2 @@
-bl /foo HTTP/1.1\r\n
\r\n

View File

@ -0,0 +1,2 @@
from gunicorn.http.errors import InvalidRequestMethod
request = InvalidRequestMethod

View File

@ -1,2 +1,2 @@
-blargh /foo HTTP/1.1\r\n
\r\n
-BLARGH /foo HTTP/1.1\r\n
\r\n

View File

@ -0,0 +1,2 @@
-blargh /foo HTTP/1.1\r\n
\r\n

View File

@ -0,0 +1,13 @@
from gunicorn.config import Config
cfg = Config()
cfg.set("permit_unconventional_http_method", True)
cfg.set("casefold_http_method", True)
request = {
"method": "-BLARGH",
"uri": uri("/foo"),
"version": (1, 1),
"headers": [],
"body": b""
}

View File

@ -0,0 +1,2 @@
-blargh /foo HTTP/1.1\r\n
\r\n

View File

@ -0,0 +1,12 @@
from gunicorn.config import Config
cfg = Config()
cfg.set("permit_unconventional_http_method", True)
request = {
"method": "-blargh",
"uri": uri("/foo"),
"version": (1, 1),
"headers": [],
"body": b""
}