diff --git a/THANKS b/THANKS index 2c2a6de5..09a15fad 100644 --- a/THANKS +++ b/THANKS @@ -178,3 +178,4 @@ WooParadog Xie Shi Yue Du zakdances +Emile Fugulin diff --git a/gunicorn/config.py b/gunicorn/config.py index e8e0f926..d165f256 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -2010,3 +2010,20 @@ class PasteGlobalConf(Setting): .. versionadded:: 19.7 """ + + +class StripHeaderSpaces(Setting): + name = "strip_header_spaces" + section = "Server Mechanics" + cli = ["--strip-header-spaces"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Strip spaces present between the header name and the the ``:``. + + 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. + """ diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 4040c7ae..e5ce4a68 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -90,7 +90,10 @@ class Message(object): if curr.find(":") < 0: raise InvalidHeader(curr.strip()) name, value = curr.split(":", 1) - name = name.rstrip(" \t").upper() + if self.cfg.strip_header_spaces: + name = name.rstrip(" \t").upper() + else: + name = name.upper() if HEADER_RE.search(name): raise InvalidHeaderName(name) @@ -128,9 +131,12 @@ class Message(object): content_length = None for (name, value) in self.headers: if name == "CONTENT-LENGTH": + if content_length is not None: + raise InvalidHeader("CONTENT-LENGTH", req=self) content_length = value elif name == "TRANSFER-ENCODING": - chunked = value.lower() == "chunked" + if value.lower() == "chunked": + chunked = True elif name == "SEC-WEBSOCKET-KEY1": content_length = 8 diff --git a/tests/requests/invalid/020.http b/tests/requests/invalid/020.http new file mode 100644 index 00000000..a699e848 --- /dev/null +++ b/tests/requests/invalid/020.http @@ -0,0 +1,4 @@ +GET /stuff/here?foo=bar HTTP/1.1\r\n +Content-Length : 3\r\n +\r\n +xyz diff --git a/tests/requests/invalid/020.py b/tests/requests/invalid/020.py new file mode 100644 index 00000000..d336fbc8 --- /dev/null +++ b/tests/requests/invalid/020.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHeaderName + +cfg = Config() +request = InvalidHeaderName diff --git a/tests/requests/invalid/021.http b/tests/requests/invalid/021.http new file mode 100644 index 00000000..90e77dd1 --- /dev/null +++ b/tests/requests/invalid/021.http @@ -0,0 +1,5 @@ +GET /stuff/here?foo=bar HTTP/1.1\r\n +Content-Length: 3\r\n +Content-Length: 2\r\n +\r\n +xyz diff --git a/tests/requests/invalid/021.py b/tests/requests/invalid/021.py new file mode 100644 index 00000000..95b0581a --- /dev/null +++ b/tests/requests/invalid/021.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/028.http b/tests/requests/valid/028.http new file mode 100644 index 00000000..9db5ecfb --- /dev/null +++ b/tests/requests/valid/028.http @@ -0,0 +1,4 @@ +GET /stuff/here?foo=bar HTTP/1.1\r\n +Content-Length : 3\r\n +\r\n +xyz \ No newline at end of file diff --git a/tests/requests/valid/028.py b/tests/requests/valid/028.py new file mode 100644 index 00000000..d8254683 --- /dev/null +++ b/tests/requests/valid/028.py @@ -0,0 +1,14 @@ +from gunicorn.config import Config + +cfg = Config() +cfg.set("strip_header_spaces", True) + +request = { + "method": "GET", + "uri": uri("/stuff/here?foo=bar"), + "version": (1, 1), + "headers": [ + ("CONTENT-LENGTH", "3"), + ], + "body": b"xyz" +} \ No newline at end of file diff --git a/tests/requests/valid/029.http b/tests/requests/valid/029.http new file mode 100644 index 00000000..c8611dbd --- /dev/null +++ b/tests/requests/valid/029.http @@ -0,0 +1,7 @@ +GET /stuff/here?foo=bar HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +Transfer-Encoding: identity\r\n +\r\n +5\r\n +hello\r\n +000\r\n diff --git a/tests/requests/valid/029.py b/tests/requests/valid/029.py new file mode 100644 index 00000000..f25449d1 --- /dev/null +++ b/tests/requests/valid/029.py @@ -0,0 +1,14 @@ +from gunicorn.config import Config + +cfg = Config() + +request = { + "method": "GET", + "uri": uri("/stuff/here?foo=bar"), + "version": (1, 1), + "headers": [ + ('TRANSFER-ENCODING', 'chunked'), + ('TRANSFER-ENCODING', 'identity') + ], + "body": b"hello" +} diff --git a/tests/requests/valid/030.http b/tests/requests/valid/030.http new file mode 100644 index 00000000..5d029dd9 --- /dev/null +++ b/tests/requests/valid/030.http @@ -0,0 +1,7 @@ +GET /stuff/here?foo=bar HTTP/1.1\r\n +Transfer-Encoding: identity\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +000\r\n diff --git a/tests/requests/valid/030.py b/tests/requests/valid/030.py new file mode 100644 index 00000000..3e98467b --- /dev/null +++ b/tests/requests/valid/030.py @@ -0,0 +1,14 @@ +from gunicorn.config import Config + +cfg = Config() + +request = { + "method": "GET", + "uri": uri("/stuff/here?foo=bar"), + "version": (1, 1), + "headers": [ + ('TRANSFER-ENCODING', 'identity'), + ('TRANSFER-ENCODING', 'chunked') + ], + "body": b"hello" +}