From 2fa7e8ab13b136e4e93c386db6fa6af9df0ccaac Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Wed, 31 Jul 2024 02:30:52 +0200 Subject: [PATCH] detect incomplete body before reading next request --- gunicorn/http/body.py | 12 ++++++++++++ gunicorn/http/errors.py | 5 +++++ gunicorn/http/parser.py | 11 +++++++---- gunicorn/workers/base.py | 3 +++ tests/requests/valid/099.http | 4 ++-- tests/requests/valid/099.py | 2 +- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/gunicorn/http/body.py b/gunicorn/http/body.py index 78f03214a..84879b771 100644 --- a/gunicorn/http/body.py +++ b/gunicorn/http/body.py @@ -15,6 +15,7 @@ def __init__(self, req, unreader): self.req = req self.parser = self.parse_chunked(unreader) self.buf = io.BytesIO() + self.finished = False def read(self, size): if not isinstance(size, int): @@ -91,6 +92,8 @@ def parse_chunk_size(self, unreader, data=None): chunk_size = chunk_size.rstrip(b" \t") if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size): raise InvalidChunkSize(chunk_size) + if len(chunk_size) == 0: + raise InvalidChunkSize(chunk_size) chunk_size = int(chunk_size, 16) if chunk_size == 0: @@ -98,6 +101,7 @@ def parse_chunk_size(self, unreader, data=None): self.parse_trailers(unreader, rest_chunk) except NoMoreData: pass + self.finished = True return (0, None) return (chunk_size, rest_chunk) @@ -112,6 +116,7 @@ class LengthReader(object): def __init__(self, unreader, length): self.unreader = unreader self.length = length + self.finished = (length == 0) def read(self, size): if not isinstance(size, int): @@ -135,6 +140,9 @@ def read(self, size): ret, rest = buf[:size], buf[size:] self.unreader.unread(rest) self.length -= size + assert self.length >= 0 + if self.length == 0: + self.finished = True return ret @@ -192,6 +200,10 @@ def __next__(self): next = __next__ + @property + def finished(self): + return self.reader.finished + def getsize(self, size): if size is None: return sys.maxsize diff --git a/gunicorn/http/errors.py b/gunicorn/http/errors.py index 1e3c5e752..315c1aafb 100644 --- a/gunicorn/http/errors.py +++ b/gunicorn/http/errors.py @@ -22,6 +22,11 @@ def __str__(self): return "No more data after: %r" % self.buf +class IncompleteBody(ParseException): + def __str__(self): + return "Incomplete Request Body" + + class ConfigurationProblem(ParseException): def __init__(self, info): self.info = info diff --git a/gunicorn/http/parser.py b/gunicorn/http/parser.py index 5d689f06a..594cd6b56 100644 --- a/gunicorn/http/parser.py +++ b/gunicorn/http/parser.py @@ -3,6 +3,7 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. +from gunicorn.http.errors import IncompleteBody from gunicorn.http.message import Request from gunicorn.http.unreader import SocketUnreader, IterUnreader @@ -27,15 +28,17 @@ def __iter__(self): return self def __next__(self): - # Stop if HTTP dictates a stop. - if self.mesg and self.mesg.should_close(): - raise StopIteration() - # Discard any unread body of the previous message if self.mesg: data = self.mesg.body.read(8192) while data: data = self.mesg.body.read(8192) + if not self.mesg.body.finished: + raise IncompleteBody() + + # Stop if HTTP dictates a stop. + if self.mesg.should_close(): + raise StopIteration() # Parse the next request self.req_count += 1 diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py index f97d923c7..28642e523 100644 --- a/gunicorn/workers/base.py +++ b/gunicorn/workers/base.py @@ -15,6 +15,7 @@ from gunicorn import util from gunicorn.http.errors import ( + IncompleteBody, ForbiddenProxyRequest, InvalidHeader, InvalidHeaderName, InvalidHTTPVersion, InvalidProxyLine, InvalidRequestLine, @@ -233,6 +234,8 @@ def handle_error(self, req, client, addr, exc): reason = "Request Header Fields Too Large" mesg = "Error parsing headers: '%s'" % str(exc) status_int = 431 + elif isinstance(exc, IncompleteBody): + mesg = "'%s'" % str(exc) elif isinstance(exc, InvalidProxyLine): mesg = "'%s'" % str(exc) elif isinstance(exc, ForbiddenProxyRequest): diff --git a/tests/requests/valid/099.http b/tests/requests/valid/099.http index 969356d09..26534bc56 100644 --- a/tests/requests/valid/099.http +++ b/tests/requests/valid/099.http @@ -7,7 +7,7 @@ Accept-Encoding: gzip, deflate\r\n Cookie: csrftoken=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=YYYYYYYYYYYYYYYYYYYYYYYYYYYY\r\n Connection: keep-alive\r\n Content-Type: multipart/form-data; boundary=---------------------------320761477111544\r\n -Content-Length: 17914\r\n +Content-Length: 8599\r\n \r\n -----------------------------320761477111544\r\n Content-Disposition: form-data; name="csrfmiddlewaretoken"\r\n @@ -265,4 +265,4 @@ Content-Disposition: form-data; name="foobar_manager_record_domain-8-TOTAL_FORMS Content-Disposition: form-data; name="foobar_manager_record_domain-8-INITIAL_FORMS"\r\n \r\n 0\r\n ----------------------\r\n \ No newline at end of file +---------------------\r\n diff --git a/tests/requests/valid/099.py b/tests/requests/valid/099.py index e4256f669..cf3eb3a11 100644 --- a/tests/requests/valid/099.py +++ b/tests/requests/valid/099.py @@ -11,7 +11,7 @@ ("COOKIE", "csrftoken=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=YYYYYYYYYYYYYYYYYYYYYYYYYYYY"), ("CONNECTION", "keep-alive"), ("CONTENT-TYPE", "multipart/form-data; boundary=---------------------------320761477111544"), - ("CONTENT-LENGTH", "17914"), + ("CONTENT-LENGTH", "8599"), ], "body": b"""-----------------------------320761477111544 Content-Disposition: form-data; name="csrfmiddlewaretoken"