Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

detect incomplete body before reading next request #3256

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions gunicorn/http/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -91,13 +92,16 @@ 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:
try:
self.parse_trailers(unreader, rest_chunk)
except NoMoreData:
pass
self.finished = True
return (0, None)
return (chunk_size, rest_chunk)

Expand All @@ -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):
Expand All @@ -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


Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions gunicorn/http/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions gunicorn/http/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe adding a reason there?


# Stop if HTTP dictates a stop.
if self.mesg.should_close():
raise StopIteration()

# Parse the next request
self.req_count += 1
Expand Down
3 changes: 3 additions & 0 deletions gunicorn/workers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from gunicorn import util
from gunicorn.http.errors import (
IncompleteBody,
ForbiddenProxyRequest, InvalidHeader,
InvalidHeaderName, InvalidHTTPVersion,
InvalidProxyLine, InvalidRequestLine,
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions tests/requests/valid/099.http
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
---------------------\r\n
2 changes: 1 addition & 1 deletion tests/requests/valid/099.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading