Skip to content

Commit

Permalink
honor a request's Content-Length
Browse files Browse the repository at this point in the history
  • Loading branch information
d-maurer committed Oct 12, 2023
1 parent c070668 commit 90a3cab
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst

- Update to newest compatible versions of dependencies.

- Honor a request's `Content-Length`
(`#1171 <https://github.com/zopefoundation/Zope/issues/1171>`_).


5.8.6 (2023-10-04)
------------------
Expand Down
57 changes: 56 additions & 1 deletion src/ZPublisher/HTTPRequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1427,9 +1427,17 @@ class ZopeFieldStorage(ValueAccessor):
VALUE_LIMIT = Global("FORM_MEMORY_LIMIT")

def __init__(self, fp, environ):
self.file = fp
method = environ.get("REQUEST_METHOD", "GET").upper()
url_qs = environ.get("QUERY_STRING", "")
content_length = environ.get("CONTENT_LENGTH")
if content_length:
try:
fp.tell()
except Exception:
# potentially not preprocessed by the WSGI server
# enforce ``Content-Length`` specified body length limit
fp = LimitedFileReader(fp, int(content_length))
self.file = fp
post_qs = ""
hl = []
content_type = environ.get("CONTENT_TYPE",
Expand Down Expand Up @@ -1493,6 +1501,53 @@ def __init__(self, fp, environ):
add_field(field)


class LimitedFileReader:
"""File wrapper emulating EOF."""

# attributes to be delegated to the file
DELEGATE = set("close closed fileno mode name".split())

def __init__(self, fp, limit):
"""emulate EOF after *limit* bytes have been read.
*fp* is a binary file like object without ``seek`` support.
"""
self.fp = fp
assert limit >= 0
self.limit = limit

def _enforce_limit(self, size):
limit = self.limit
return limit if size is None or size < 0 else min(size, limit)

def read(self, size=-1):
data = self.fp.read(self._enforce_limit(size))
self.limit -= len(data)
return data

def readline(self, size=-1):
data = self.fp.readline(self._enforce_limit(size))
self.limit -= len(data)
return data

def __iter__(self):
return self

def __next__(self):
data = self.readline()
if not data:
raise StopIteration()
return data

def __del__(self):
return self.fp.__del__()

def __getattr__(self, attr):
if attr not in self.DELEGATE:
raise AttributeError(attr)
return getattr(self.fp, attr)


def _mp_charset(part):
"""the charset of *part*."""
content_type = part.headers.get("Content-Type", "")
Expand Down
52 changes: 52 additions & 0 deletions src/ZPublisher/tests/testHTTPRequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from zope.testing.cleanup import cleanUp
from ZPublisher.HTTPRequest import BadRequest
from ZPublisher.HTTPRequest import FileUpload
from ZPublisher.HTTPRequest import LimitedFileReader
from ZPublisher.HTTPRequest import search_type
from ZPublisher.interfaces import IXmlrpcChecker
from ZPublisher.tests.testBaseRequest import TestRequestViewsBase
Expand Down Expand Up @@ -1514,6 +1515,15 @@ def test_form_charset(self):
self.assertEqual(req["x"], "äöü")
self.assertEqual(req["y"], "äöü")

def test_content_length_limitation(self):
body = b"123abc"
env = self._makePostEnviron(body)
env["CONTENT_TYPE"] = "application/octed-stream"
env["CONTENT_LENGTH"] = "3"
req = self._makeOne(_Unseekable(BytesIO(body)), env)
req.processInputs()
self.assertEqual(req["BODY"], b"123")


class TestHTTPRequestZope3Views(TestRequestViewsBase):

Expand Down Expand Up @@ -1570,6 +1580,48 @@ def test_special(self):
self.check("abc:a-_0b", ":a-_0b")


class TestLimitedFileReader(unittest.TestCase):
def test_enforce_limit(self):
f = LimitedFileReader(BytesIO(), 10)
enforce = f._enforce_limit
self.assertEqual(enforce(None), 10)
self.assertEqual(enforce(-1), 10)
self.assertEqual(enforce(20), 10)
self.assertEqual(enforce(5), 5)

def test_read(self):
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
self.assertEqual(len(f.read()), 10)
self.assertEqual(len(f.read()), 0)
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
self.assertEqual(len(f.read(8)), 8)
self.assertEqual(len(f.read(3)), 2)
self.assertEqual(len(f.read(3)), 0)

def test_readline(self):
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
self.assertEqual(f.readline(), b"123\n")
self.assertEqual(f.readline(), b"567\n")
self.assertEqual(f.readline(), b"90")
self.assertEqual(f.readline(), b"")
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
self.assertEqual(f.readline(1), b"1")

def test_iteration(self):
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
self.assertEqual(list(f), [b"123\n", b"567\n", b"90"])

def test_del(self):
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
del f

def test_delegation(self):
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
with self.assertRaises(AttributeError):
f.write
f.close()


class _Unseekable:
"""Auxiliary class emulating an unseekable file like object"""
def __init__(self, file):
Expand Down

0 comments on commit 90a3cab

Please sign in to comment.