Skip to content

Commit a3c7d87

Browse files
authored
Merge branch 'master' into fix_ofs_svg_dims2
2 parents c89c1bf + d87e918 commit a3c7d87

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
1616

1717
- Update to newest compatible versions of dependencies.
1818

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

2023
5.8.6 (2023-10-04)
2124
------------------

src/ZPublisher/HTTPRequest.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1427,9 +1427,17 @@ class ZopeFieldStorage(ValueAccessor):
14271427
VALUE_LIMIT = Global("FORM_MEMORY_LIMIT")
14281428

14291429
def __init__(self, fp, environ):
1430-
self.file = fp
14311430
method = environ.get("REQUEST_METHOD", "GET").upper()
14321431
url_qs = environ.get("QUERY_STRING", "")
1432+
content_length = environ.get("CONTENT_LENGTH")
1433+
if content_length:
1434+
try:
1435+
fp.tell()
1436+
except Exception:
1437+
# potentially not preprocessed by the WSGI server
1438+
# enforce ``Content-Length`` specified body length limit
1439+
fp = LimitedFileReader(fp, int(content_length))
1440+
self.file = fp
14331441
post_qs = ""
14341442
hl = []
14351443
content_type = environ.get("CONTENT_TYPE",
@@ -1493,6 +1501,53 @@ def __init__(self, fp, environ):
14931501
add_field(field)
14941502

14951503

1504+
class LimitedFileReader:
1505+
"""File wrapper emulating EOF."""
1506+
1507+
# attributes to be delegated to the file
1508+
DELEGATE = set(["close", "closed", "fileno", "mode", "name"])
1509+
1510+
def __init__(self, fp, limit):
1511+
"""emulate EOF after *limit* bytes have been read.
1512+
1513+
*fp* is a binary file like object without ``seek`` support.
1514+
"""
1515+
self.fp = fp
1516+
assert limit >= 0
1517+
self.limit = limit
1518+
1519+
def _enforce_limit(self, size):
1520+
limit = self.limit
1521+
return limit if size is None or size < 0 else min(size, limit)
1522+
1523+
def read(self, size=-1):
1524+
data = self.fp.read(self._enforce_limit(size))
1525+
self.limit -= len(data)
1526+
return data
1527+
1528+
def readline(self, size=-1):
1529+
data = self.fp.readline(self._enforce_limit(size))
1530+
self.limit -= len(data)
1531+
return data
1532+
1533+
def __iter__(self):
1534+
return self
1535+
1536+
def __next__(self):
1537+
data = self.readline()
1538+
if not data:
1539+
raise StopIteration()
1540+
return data
1541+
1542+
def __del__(self):
1543+
return self.fp.__del__()
1544+
1545+
def __getattr__(self, attr):
1546+
if attr not in self.DELEGATE:
1547+
raise AttributeError(attr)
1548+
return getattr(self.fp, attr)
1549+
1550+
14961551
def _mp_charset(part):
14971552
"""the charset of *part*."""
14981553
content_type = part.headers.get("Content-Type", "")

src/ZPublisher/tests/testHTTPRequest.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from zope.testing.cleanup import cleanUp
3232
from ZPublisher.HTTPRequest import BadRequest
3333
from ZPublisher.HTTPRequest import FileUpload
34+
from ZPublisher.HTTPRequest import LimitedFileReader
3435
from ZPublisher.HTTPRequest import search_type
3536
from ZPublisher.interfaces import IXmlrpcChecker
3637
from ZPublisher.tests.testBaseRequest import TestRequestViewsBase
@@ -1514,6 +1515,15 @@ def test_form_charset(self):
15141515
self.assertEqual(req["x"], "äöü")
15151516
self.assertEqual(req["y"], "äöü")
15161517

1518+
def test_content_length_limitation(self):
1519+
body = b"123abc"
1520+
env = self._makePostEnviron(body)
1521+
env["CONTENT_TYPE"] = "application/octed-stream"
1522+
env["CONTENT_LENGTH"] = "3"
1523+
req = self._makeOne(_Unseekable(BytesIO(body)), env)
1524+
req.processInputs()
1525+
self.assertEqual(req["BODY"], b"123")
1526+
15171527

15181528
class TestHTTPRequestZope3Views(TestRequestViewsBase):
15191529

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

15721582

1583+
class TestLimitedFileReader(unittest.TestCase):
1584+
def test_enforce_limit(self):
1585+
f = LimitedFileReader(BytesIO(), 10)
1586+
enforce = f._enforce_limit
1587+
self.assertEqual(enforce(None), 10)
1588+
self.assertEqual(enforce(-1), 10)
1589+
self.assertEqual(enforce(20), 10)
1590+
self.assertEqual(enforce(5), 5)
1591+
1592+
def test_read(self):
1593+
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
1594+
self.assertEqual(len(f.read()), 10)
1595+
self.assertEqual(len(f.read()), 0)
1596+
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
1597+
self.assertEqual(len(f.read(8)), 8)
1598+
self.assertEqual(len(f.read(3)), 2)
1599+
self.assertEqual(len(f.read(3)), 0)
1600+
1601+
def test_readline(self):
1602+
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
1603+
self.assertEqual(f.readline(), b"123\n")
1604+
self.assertEqual(f.readline(), b"567\n")
1605+
self.assertEqual(f.readline(), b"90")
1606+
self.assertEqual(f.readline(), b"")
1607+
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
1608+
self.assertEqual(f.readline(1), b"1")
1609+
1610+
def test_iteration(self):
1611+
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
1612+
self.assertEqual(list(f), [b"123\n", b"567\n", b"90"])
1613+
1614+
def test_del(self):
1615+
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
1616+
del f
1617+
1618+
def test_delegation(self):
1619+
f = LimitedFileReader(BytesIO(b"123\n567\n901\n"), 10)
1620+
with self.assertRaises(AttributeError):
1621+
f.write
1622+
f.close()
1623+
1624+
15731625
class _Unseekable:
15741626
"""Auxiliary class emulating an unseekable file like object"""
15751627
def __init__(self, file):

0 commit comments

Comments
 (0)