diff --git a/gunicorn/config.py b/gunicorn/config.py index 144acaec..a117a73e 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -2254,7 +2254,7 @@ 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. May be removed in a future version. + Use with care and only if necessary. Deprecated; scheduled for removal in 25.0.0 .. versionadded:: 20.0.1 """ @@ -2274,9 +2274,13 @@ class PermitUnconventionalHTTPMethod(Setting): 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. + If unset, Gunicorn will apply nonstandard restrictions and cause 400 response status + in cases where otherwise 501 status is expected. While this option does modify that + behaviour, it should not be depended upon to guarantee standards-compliant behaviour. + Rather, it is provided temporarily, to assist in diagnosing backwards-incompatible + changes around the incomplete application of those restrictions. - Use with care and only if necessary. May be removed in a future version. + Use with care and only if necessary. Temporary; scheduled for removal in 24.0.0 .. versionadded:: 22.0.0 """ @@ -2296,7 +2300,8 @@ class PermitUnconventionalHTTPVersion(Setting): 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. + Use with care and only if necessary. Temporary; the precise effect of this option may + change in a future version, or it may be removed altogether. .. versionadded:: 22.0.0 """ @@ -2316,7 +2321,7 @@ class CasefoldHTTPMethod(Setting): 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. + Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0 .. versionadded:: 22.0.0 """ @@ -2378,7 +2383,16 @@ class TolerateDangerousFraming(Setting): This is known to induce vulnerabilities, but not strictly forbidden by RFC9112. - Use with care and only if necessary. May be removed in a future version. + In any case, the connection is closed after the malformed request, + as it is unclear if and at which boundary additional requests start. + + Use with care and only if necessary. + Temporary; will be changed or removed in a future version. .. versionadded:: 22.0.0 + .. versionchanged: 22.1.0 + The newly added rejection of invalid and dangerous characters CR, LF and NUL in + header field values is also controlled with this setting. rfc9110 permits both + rejecting and SP-replacing. With this option set, Gunicorn passes the field value + unchanged. With this option unset, Gunicorn rejects the request. """ diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 88ffa5a2..3f172a5a 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -28,6 +28,7 @@ TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIAL METHOD_BADCHAR_RE = re.compile("[a-z#]") # usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)") +RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]") class Message(object): @@ -121,6 +122,12 @@ class Message(object): value.append(curr.strip("\t ")) value = " ".join(value) + if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value): + if not self.cfg.tolerate_dangerous_framing: + raise InvalidHeader(name) + # value = RFC9110_5_5_INVALID_AND_DANGEROUS.sub(" ", value) + self.force_close() + if header_length > self.limit_request_field_size > 0: raise LimitRequestHeaders("limit request headers fields size") diff --git a/tests/requests/invalid/invalid_field_value_01.http b/tests/requests/invalid/invalid_field_value_01.http new file mode 100644 index 00000000..0cae8306 --- /dev/null +++ b/tests/requests/invalid/invalid_field_value_01.http @@ -0,0 +1,7 @@ +GET / HTTP/1.1\r\n +Host: x\r\n +Newline: a\n +Content-Length: 26\r\n +GET / HTTP/1.1\n +Host: x\r\n +\r\n diff --git a/tests/requests/invalid/invalid_field_value_01.py b/tests/requests/invalid/invalid_field_value_01.py new file mode 100644 index 00000000..95b0581a --- /dev/null +++ b/tests/requests/invalid/invalid_field_value_01.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHeader + +cfg = Config() +request = InvalidHeader diff --git a/tests/requests/valid/invalid_field_value_01_compat.http b/tests/requests/valid/invalid_field_value_01_compat.http new file mode 100644 index 00000000..eab2b5f1 --- /dev/null +++ b/tests/requests/valid/invalid_field_value_01_compat.http @@ -0,0 +1,8 @@ +GET / HTTP/1.1\r\n +Host: x\r\n +Newline: a\n +Content-Length: 26\r\n +X-Forwarded-By: broken-proxy\r\n\r\n +GET / HTTP/1.1\n +Host: x\r\n +\r\n diff --git a/tests/requests/valid/invalid_field_value_01_compat.py b/tests/requests/valid/invalid_field_value_01_compat.py new file mode 100644 index 00000000..9621bc0f --- /dev/null +++ b/tests/requests/valid/invalid_field_value_01_compat.py @@ -0,0 +1,18 @@ +from gunicorn.config import Config + +cfg = Config() +cfg.set("tolerate_dangerous_framing", True) + +req1 = { + "method": "GET", + "uri": uri("/"), + "version": (1, 1), + "headers": [ + ("HOST", "x"), + ("NEWLINE", "a\nContent-Length: 26"), + ("X-FORWARDED-BY", "broken-proxy"), + ], + "body": b"" +} + +request = [req1]