]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-131724: Add a new max_response_headers param to HTTP/HTTPSConnection (GH-136814)
authorAlexander Urieles <aeurielesn@users.noreply.github.com>
Sun, 20 Jul 2025 13:53:54 +0000 (15:53 +0200)
committerGitHub <noreply@github.com>
Sun, 20 Jul 2025 13:53:54 +0000 (13:53 +0000)
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Doc/library/http.client.rst
Doc/whatsnew/3.15.rst
Lib/http/client.py
Lib/test/test_httplib.py
Misc/ACKS
Misc/NEWS.d/next/Library/2025-07-19-15-40-47.gh-issue-131724.LS59nA.rst [new file with mode: 0644]

index 2835c8d0eb711e33efa76ee56075cc7cf5ca7b09..07f5ebf57c9b543491b60579e88cbeaee6171d77 100644 (file)
@@ -34,7 +34,7 @@ The module provides the following classes:
 
 
 .. class:: HTTPConnection(host, port=None[, timeout], source_address=None, \
-                          blocksize=8192)
+                          blocksize=8192, max_response_headers=None)
 
    An :class:`HTTPConnection` instance represents one transaction with an HTTP
    server.  It should be instantiated by passing it a host and optional port
@@ -46,7 +46,9 @@ The module provides the following classes:
    The optional *source_address* parameter may be a tuple of a (host, port)
    to use as the source address the HTTP connection is made from.
    The optional *blocksize* parameter sets the buffer size in bytes for
-   sending a file-like message body.
+   sending a file-like message body. The optional *max_response_headers*
+   parameter sets the maximum number of allowed response headers to help
+   prevent denial-of-service attacks, otherwise the default value (100) is used.
 
    For example, the following calls all create instances that connect to the server
    at the same host and port::
@@ -66,10 +68,13 @@ The module provides the following classes:
    .. versionchanged:: 3.7
       *blocksize* parameter was added.
 
+   .. versionchanged:: next
+      *max_response_headers* parameter was added.
+
 
 .. class:: HTTPSConnection(host, port=None, *[, timeout], \
                            source_address=None, context=None, \
-                           blocksize=8192)
+                           blocksize=8192, max_response_headers=None)
 
    A subclass of :class:`HTTPConnection` that uses SSL for communication with
    secure servers.  Default port is ``443``.  If *context* is specified, it
@@ -109,6 +114,9 @@ The module provides the following classes:
       The deprecated *key_file*, *cert_file* and *check_hostname* parameters
       have been removed.
 
+   .. versionchanged:: next
+      *max_response_headers* parameter was added.
+
 
 .. class:: HTTPResponse(sock, debuglevel=0, method=None, url=None)
 
@@ -416,6 +424,14 @@ HTTPConnection Objects
    .. versionadded:: 3.7
 
 
+.. attribute:: HTTPConnection.max_response_headers
+
+   The maximum number of allowed response headers to help prevent denial-of-service
+   attacks. By default, the maximum number of allowed headers is set to 100.
+
+   .. versionadded:: next
+
+
 As an alternative to using the :meth:`~HTTPConnection.request` method described above, you can
 also send your request step by step, by using the four functions below.
 
index 0f65317633ba70ef551159c1adf9aaa7a9d3bd9b..7e47fa263d9a5eb1ff4cd2e3f54038f1b1a12d0b 100644 (file)
@@ -230,6 +230,16 @@ difflib
   (Contributed by Jiahao Li in :gh:`134580`.)
 
 
+http.client
+-----------
+
+* A new *max_response_headers* keyword-only parameter has been added to
+  :class:`~http.client.HTTPConnection` and :class:`~http.client.HTTPSConnection`
+  constructors. This parameter overrides the default maximum number of allowed
+  response headers.
+  (Contributed by Alexander Enrique Urieles Nieto in :gh:`131724`.)
+
+
 math
 ----
 
index e7a1c7bc3b2ae1a320ed0b4a8dc08747cf426c72..0cce49cadc09fae27e77a019cc04f4fb5ce61028 100644 (file)
@@ -209,22 +209,24 @@ class HTTPMessage(email.message.Message):
                 lst.append(line)
         return lst
 
-def _read_headers(fp):
+def _read_headers(fp, max_headers):
     """Reads potential header lines into a list from a file pointer.
 
     Length of line is limited by _MAXLINE, and number of
-    headers is limited by _MAXHEADERS.
+    headers is limited by max_headers.
     """
     headers = []
+    if max_headers is None:
+        max_headers = _MAXHEADERS
     while True:
         line = fp.readline(_MAXLINE + 1)
         if len(line) > _MAXLINE:
             raise LineTooLong("header line")
-        headers.append(line)
-        if len(headers) > _MAXHEADERS:
-            raise HTTPException("got more than %d headers" % _MAXHEADERS)
         if line in (b'\r\n', b'\n', b''):
             break
+        headers.append(line)
+        if len(headers) > max_headers:
+            raise HTTPException(f"got more than {max_headers} headers")
     return headers
 
 def _parse_header_lines(header_lines, _class=HTTPMessage):
@@ -241,10 +243,10 @@ def _parse_header_lines(header_lines, _class=HTTPMessage):
     hstring = b''.join(header_lines).decode('iso-8859-1')
     return email.parser.Parser(_class=_class).parsestr(hstring)
 
-def parse_headers(fp, _class=HTTPMessage):
+def parse_headers(fp, _class=HTTPMessage, *, _max_headers=None):
     """Parses only RFC2822 headers from a file pointer."""
 
-    headers = _read_headers(fp)
+    headers = _read_headers(fp, _max_headers)
     return _parse_header_lines(headers, _class)
 
 
@@ -320,7 +322,7 @@ class HTTPResponse(io.BufferedIOBase):
             raise BadStatusLine(line)
         return version, status, reason
 
-    def begin(self):
+    def begin(self, *, _max_headers=None):
         if self.headers is not None:
             # we've already started reading the response
             return
@@ -331,7 +333,7 @@ class HTTPResponse(io.BufferedIOBase):
             if status != CONTINUE:
                 break
             # skip the header from the 100 response
-            skipped_headers = _read_headers(self.fp)
+            skipped_headers = _read_headers(self.fp, _max_headers)
             if self.debuglevel > 0:
                 print("headers:", skipped_headers)
             del skipped_headers
@@ -346,7 +348,9 @@ class HTTPResponse(io.BufferedIOBase):
         else:
             raise UnknownProtocol(version)
 
-        self.headers = self.msg = parse_headers(self.fp)
+        self.headers = self.msg = parse_headers(
+            self.fp, _max_headers=_max_headers
+        )
 
         if self.debuglevel > 0:
             for hdr, val in self.headers.items():
@@ -864,7 +868,7 @@ class HTTPConnection:
         return None
 
     def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
-                 source_address=None, blocksize=8192):
+                 source_address=None, blocksize=8192, *, max_response_headers=None):
         self.timeout = timeout
         self.source_address = source_address
         self.blocksize = blocksize
@@ -877,6 +881,7 @@ class HTTPConnection:
         self._tunnel_port = None
         self._tunnel_headers = {}
         self._raw_proxy_headers = None
+        self.max_response_headers = max_response_headers
 
         (self.host, self.port) = self._get_hostport(host, port)
 
@@ -969,7 +974,7 @@ class HTTPConnection:
         try:
             (version, code, message) = response._read_status()
 
-            self._raw_proxy_headers = _read_headers(response.fp)
+            self._raw_proxy_headers = _read_headers(response.fp, self.max_response_headers)
 
             if self.debuglevel > 0:
                 for header in self._raw_proxy_headers:
@@ -1426,7 +1431,10 @@ class HTTPConnection:
 
         try:
             try:
-                response.begin()
+                if self.max_response_headers is None:
+                    response.begin()
+                else:
+                    response.begin(_max_headers=self.max_response_headers)
             except ConnectionError:
                 self.close()
                 raise
@@ -1457,10 +1465,12 @@ else:
 
         def __init__(self, host, port=None,
                      *, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
-                     source_address=None, context=None, blocksize=8192):
+                     source_address=None, context=None, blocksize=8192,
+                     max_response_headers=None):
             super(HTTPSConnection, self).__init__(host, port, timeout,
                                                   source_address,
-                                                  blocksize=blocksize)
+                                                  blocksize=blocksize,
+                                                  max_response_headers=max_response_headers)
             if context is None:
                 context = _create_https_context(self._http_vsn)
             self._context = context
index 38429ad480ff1c2a85e3540a55c4a4ae80d59271..47e3914d1dd62e58dd20f2e7a43c780c179f4de6 100644 (file)
@@ -386,6 +386,52 @@ class HeaderTests(TestCase):
         self.assertEqual(lines[2], "header: Second: val1")
         self.assertEqual(lines[3], "header: Second: val2")
 
+    def test_max_response_headers(self):
+        max_headers = client._MAXHEADERS + 20
+        headers = [f"Name{i}: Value{i}".encode() for i in range(max_headers)]
+        body = b"HTTP/1.1 200 OK\r\n" + b"\r\n".join(headers)
+
+        with self.subTest(max_headers=None):
+            sock = FakeSocket(body)
+            resp = client.HTTPResponse(sock)
+            with self.assertRaisesRegex(
+                client.HTTPException, f"got more than 100 headers"
+            ):
+                resp.begin()
+
+        with self.subTest(max_headers=max_headers):
+            sock = FakeSocket(body)
+            resp = client.HTTPResponse(sock)
+            resp.begin(_max_headers=max_headers)
+
+    def test_max_connection_headers(self):
+        max_headers = client._MAXHEADERS + 20
+        headers = (
+            f"Name{i}: Value{i}".encode() for i in range(max_headers - 1)
+        )
+        body = (
+            b"HTTP/1.1 200 OK\r\n"
+            + b"\r\n".join(headers)
+            + b"\r\nContent-Length: 12\r\n\r\nDummy body\r\n"
+        )
+
+        with self.subTest(max_headers=None):
+            conn = client.HTTPConnection("example.com")
+            conn.sock = FakeSocket(body)
+            conn.request("GET", "/")
+            with self.assertRaisesRegex(
+                client.HTTPException, f"got more than {client._MAXHEADERS} headers"
+            ):
+                response = conn.getresponse()
+
+        with self.subTest(max_headers=None):
+            conn = client.HTTPConnection(
+                "example.com", max_response_headers=max_headers
+            )
+            conn.sock = FakeSocket(body)
+            conn.request("GET", "/")
+            response = conn.getresponse()
+            response.read()
 
 class HttpMethodTests(TestCase):
     def test_invalid_method_names(self):
index fabd79b9f742107d98d85e3b1241e837bf37d3c4..35826bd713c0f6c8a632ce370f7cfad89d16a748 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1954,6 +1954,7 @@ Adnan Umer
 Utkarsh Upadhyay
 Roger Upole
 Daniel Urban
+Alexander Enrique Urieles Nieto
 Matthias Urlichs
 Michael Urman
 Hector Urtubia
diff --git a/Misc/NEWS.d/next/Library/2025-07-19-15-40-47.gh-issue-131724.LS59nA.rst b/Misc/NEWS.d/next/Library/2025-07-19-15-40-47.gh-issue-131724.LS59nA.rst
new file mode 100644 (file)
index 0000000..71a991a
--- /dev/null
@@ -0,0 +1,4 @@
+In :mod:`http.client`, a new *max_response_headers* keyword-only parameter has been
+added to :class:`~http.client.HTTPConnection` and :class:`~http.client.HTTPSConnection`
+constructors. This parameter sets the maximum number of allowed response headers,
+helping to prevent denial-of-service attacks.