diff --git a/CHANGES.rst b/CHANGES.rst
index 432f245de..2aa73244f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -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)
 ------------------
diff --git a/src/ZPublisher/HTTPRequest.py b/src/ZPublisher/HTTPRequest.py
index 20421a14b..947a9a88e 100644
--- a/src/ZPublisher/HTTPRequest.py
+++ b/src/ZPublisher/HTTPRequest.py
@@ -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",
@@ -1493,6 +1501,53 @@ class ZopeFieldStorage(ValueAccessor):
             add_field(field)
 
 
+class LimitedFileReader:
+    """File wrapper emulating EOF."""
+
+    # attributes to be delegated to the file
+    DELEGATE = set(["close", "closed", "fileno", "mode", "name"])
+
+    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", "")

diff --git a/src/ZPublisher/tests/testHTTPRequest.py b/src/ZPublisher/tests/testHTTPRequest.py
index d7fe18e4d..0dafba669 100644
--- a/src/ZPublisher/tests/testHTTPRequest.py
+++ b/src/ZPublisher/tests/testHTTPRequest.py
@@ -31,6 +31,7 @@ from zope.publisher.interfaces.http import IHTTPRequest
 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
@@ -1514,6 +1515,15 @@ class HTTPRequestTests(unittest.TestCase, HTTPRequestFactoryMixin):
         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):
 
@@ -1570,6 +1580,48 @@ class TestSearchType(unittest.TestCase):
         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):

Zope 5.8.1 breaks using `wsgiref`
### What I did:

I am using Python's stdlib `wsgiref` in selenium tests to start a simple WSGI server.

### What I expect to happen:

Using `wsgiref` should be possible.

### What actually happened:

`wsgiref` uses a `socket` as `stdin` for the WSGI environment.
And Zope 5.8.1+ uses `read()` to get the data from this socket, the execution is blocked and later on runs into a timeout.

Previous versions (via `cgi` module) called `readline()` on the socket which does not block.

(Sorry I cannot provide a minimal test example as it involves too much custom code. Additionally I did not find an easy way to start a Zope instance using `wsgiref` – the examples for using a different WSGI server in the docs seem to require some preparation.)

### What version of Python and Zope/Addons I am using:

* Zope 5.8.6
* MacOS 13.6
* Python 3.11
src/ZPublisher/tests/testHTTPRequest.py::RecordTests::test__str__returns_native_string src/ZPublisher/tests/testHTTPRequest.py::RecordTests::test_copy src/ZPublisher/tests/testHTTPRequest.py::RecordTests::test_dict_methods src/ZPublisher/tests/testHTTPRequest.py::RecordTests::test_dict_special_methods src/ZPublisher/tests/testHTTPRequest.py::RecordTests::test_eq src/ZPublisher/tests/testHTTPRequest.py::RecordTests::test_repr src/ZPublisher/tests/testHTTPRequest.py::RecordTests::test_str src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test___bobo_traverse___raises src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test___str____password_field src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test__authUserPW_non_ascii src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test__authUserPW_simple src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test__authUserPW_with_embedded_colon src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test__str__returns_native_string src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_bytes_converter src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_clone_doesnt_re_clean_environ src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_clone_keeps_only_last_PARENT src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_clone_keeps_preserves__auth src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_clone_preserves_direct_interfaces src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_clone_preserves_request_subclass src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_clone_preserves_response_class src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_clone_updates_method_to_GET src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_close_removes_stdin_references src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_content_length_limitation src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_debug_in_qs_gets_form_var src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_debug_not_in_qs_still_gets_attr src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_debug_override_via_form_other src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_field_charset src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_form_charset src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_form_urlencoded src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getClientAddr_one_trusted_proxy src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getClientAddr_trusted_proxy_last src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getClientAddr_trusted_proxy_no_REMOTE_ADDR src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getClientAddr_wo_trusted_proxy src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getHeader_case_insensitive src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getHeader_exact src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getHeader_literal_turns_off_case_normalization src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getHeader_nonesuch src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getHeader_nonesuch_with_default src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getHeader_underscore_is_dash src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_getVirtualRoot src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_get_with_body_and_query_string_ignores_body src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_issue_1095 src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_locale_fallback src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_locale_in_qs src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_locale_property_accessor src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_locale_property_override_via_form_other src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_locale_semantics src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_method_GET src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_method_POST src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_no_docstring_on_instance src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_parses_json_cookies src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_BODY src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_BODY_unseekable src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_SOAP src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_SOAP_query_string src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_seekable_form_data src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_unseekable_form_data src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_unspecified_file src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_cookie_parsing src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_defaults src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_defaults_w_taints src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_dotted_name_as_tuple src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_large_input_gets_tempfile src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_marshalling_into_sequences src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_records_w_sequences src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_records_w_sequences_tainted src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_simple_containers src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_simple_containers_w_taints src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_simple_marshalling src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_simple_marshalling_w_taints src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_tainted_attribute_raises src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_tainted_values_cleans_exceptions src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_unicode_conversions src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_unicode_w_taints src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_w_urlencoded_and_qs src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_with_file_upload_gets_iterator src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_wo_marshalling src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_wo_marshalling_w_Taints src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_wo_query_string src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_xmlrpc src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_xmlrpc_controlled_allowed src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_xmlrpc_controlled_disallowed src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_xmlrpc_method src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_xmlrpc_query_string src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_processInputs_xmlrpc_with_args src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_put_with_body_and_query_string src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_put_with_body_limit src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_resolve_url_doesnt_send_endrequestevent src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_resolve_url_errorhandling src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_text__password_field src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_url_scheme src/ZPublisher/tests/testHTTPRequest.py::HTTPRequestTests::test_webdav_source_port_available src/ZPublisher/tests/testHTTPRequest.py::TestHTTPRequestZope3Views::test_no_traversal_of_view_request_attribute src/ZPublisher/tests/testHTTPRequest.py::TestSearchType::test_image_control src/ZPublisher/tests/testHTTPRequest.py::TestSearchType::test_leftmost src/ZPublisher/tests/testHTTPRequest.py::TestSearchType::test_special src/ZPublisher/tests/testHTTPRequest.py::TestSearchType::test_type src/ZPublisher/tests/testHTTPRequest.py::TestLimitedFileReader::test_del src/ZPublisher/tests/testHTTPRequest.py::TestLimitedFileReader::test_delegation src/ZPublisher/tests/testHTTPRequest.py::TestLimitedFileReader::test_enforce_limit src/ZPublisher/tests/testHTTPRequest.py::TestLimitedFileReader::test_iteration src/ZPublisher/tests/testHTTPRequest.py::TestLimitedFileReader::test_read src/ZPublisher/tests/testHTTPRequest.py::TestLimitedFileReader::test_readline