From a24a88d782833c5f5732070542b75b66fc20632b Mon Sep 17 00:00:00 2001 From: aeurielesn Date: Sat, 19 Jul 2025 14:50:08 +0200 Subject: [PATCH 1/8] gh-131724: Add a new max_headers param to HTTP/HTTPSConnection --- Lib/http/client.py | 39 +++++++++++++++++++++-------------- Lib/test/test_httplib.py | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index e7a1c7bc3b2ae1..b5d001d09c0afb 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -209,22 +209,24 @@ def getallmatchingheaders(self, name): 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("got more than %d headers" % max_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 @@ def _read_status(self): 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 @@ def begin(self): 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 @@ def begin(self): 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 @@ def _get_content_length(body, method): return None def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, blocksize=8192): + source_address=None, blocksize=8192, max_headers=None): self.timeout = timeout self.source_address = source_address self.blocksize = blocksize @@ -877,6 +881,9 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, self._tunnel_port = None self._tunnel_headers = {} self._raw_proxy_headers = None + if max_headers is None: + max_headers = _MAXHEADERS + self.max_headers = max_headers (self.host, self.port) = self._get_hostport(host, port) @@ -969,7 +976,7 @@ def _tunnel(self): try: (version, code, message) = response._read_status() - self._raw_proxy_headers = _read_headers(response.fp) + self._raw_proxy_headers = _read_headers(response.fp, max_headers=self.max_headers) if self.debuglevel > 0: for header in self._raw_proxy_headers: @@ -1426,7 +1433,7 @@ def getresponse(self): try: try: - response.begin() + response.begin(_max_headers=self.max_headers) except ConnectionError: self.close() raise @@ -1457,10 +1464,12 @@ class HTTPSConnection(HTTPConnection): 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_headers=None): super(HTTPSConnection, self).__init__(host, port, timeout, source_address, - blocksize=blocksize) + blocksize=blocksize, + max_headers=max_headers) if context is None: context = _create_https_context(self._http_vsn) self._context = context diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 38429ad480ff1c..9911efc9971c44 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -386,6 +386,50 @@ def test_headers_debuglevel(self): 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_headers=max_headers) + conn.sock = FakeSocket(body) + conn.request("GET", "/") + response = conn.getresponse() + response.read() class HttpMethodTests(TestCase): def test_invalid_method_names(self): From 34813cae8cb24a34927645b18a5a2600fed2ef54 Mon Sep 17 00:00:00 2001 From: aeurielesn Date: Sat, 19 Jul 2025 16:54:18 +0200 Subject: [PATCH 2/8] Apply review comments --- Lib/http/client.py | 17 +++++++++-------- Lib/test/test_httplib.py | 4 +++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index b5d001d09c0afb..5a09d1e1dfaaba 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -868,7 +868,7 @@ def _get_content_length(body, method): return None def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, blocksize=8192, max_headers=None): + source_address=None, blocksize=8192, max_response_headers=None): self.timeout = timeout self.source_address = source_address self.blocksize = blocksize @@ -881,9 +881,7 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, self._tunnel_port = None self._tunnel_headers = {} self._raw_proxy_headers = None - if max_headers is None: - max_headers = _MAXHEADERS - self.max_headers = max_headers + self.max_response_headers = max_response_headers (self.host, self.port) = self._get_hostport(host, port) @@ -976,7 +974,7 @@ def _tunnel(self): try: (version, code, message) = response._read_status() - self._raw_proxy_headers = _read_headers(response.fp, max_headers=self.max_headers) + self._raw_proxy_headers = _read_headers(response.fp, self.max_response_headers) if self.debuglevel > 0: for header in self._raw_proxy_headers: @@ -1433,7 +1431,10 @@ def getresponse(self): try: try: - response.begin(_max_headers=self.max_headers) + if self.max_response_headers is None: + response.begin() + else: + response.begin(self.max_response_headers) except ConnectionError: self.close() raise @@ -1465,11 +1466,11 @@ class HTTPSConnection(HTTPConnection): def __init__(self, host, port=None, *, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None, context=None, blocksize=8192, - max_headers=None): + max_response_headers=None): super(HTTPSConnection, self).__init__(host, port, timeout, source_address, blocksize=blocksize, - max_headers=max_headers) + max_response_headers=max_response_headers) if context is None: context = _create_https_context(self._http_vsn) self._context = context diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 9911efc9971c44..47e3914d1dd62e 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -425,7 +425,9 @@ def test_max_connection_headers(self): response = conn.getresponse() with self.subTest(max_headers=None): - conn = client.HTTPConnection("example.com", max_headers=max_headers) + conn = client.HTTPConnection( + "example.com", max_response_headers=max_headers + ) conn.sock = FakeSocket(body) conn.request("GET", "/") response = conn.getresponse() From 2ea571ebaec5b3d6a93f343024f40d98925b3225 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:40:48 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-07-19-15-40-47.gh-issue-131724.LS59nA.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-19-15-40-47.gh-issue-131724.LS59nA.rst 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 index 00000000000000..010c74e055dbab --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-19-15-40-47.gh-issue-131724.LS59nA.rst @@ -0,0 +1 @@ +Adds a new max_response_headers parameter to HTTP/HTTPSConnection. From ad0523aba9ed073868ca7910f88108921806c5e7 Mon Sep 17 00:00:00 2001 From: aeurielesn Date: Sun, 20 Jul 2025 11:08:32 +0200 Subject: [PATCH 4/8] Adds docs --- Doc/library/http.client.rst | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Doc/library/http.client.rst b/Doc/library/http.client.rst index 2835c8d0eb711e..fe7486d107b72a 100644 --- a/Doc/library/http.client.rst +++ b/Doc/library/http.client.rst @@ -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:: 3.15 + *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:: 3.15 + *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. + + .. versionadded:: 3.15 + + 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. From b40c12678d63ea4fde14d9759a35d8652ea3a053 Mon Sep 17 00:00:00 2001 From: aeurielesn Date: Sun, 20 Jul 2025 12:35:58 +0200 Subject: [PATCH 5/8] Apply review comments --- Doc/library/http.client.rst | 12 ++++++------ Lib/http/client.py | 10 +++++----- .../2025-07-19-15-40-47.gh-issue-131724.LS59nA.rst | 5 ++++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Doc/library/http.client.rst b/Doc/library/http.client.rst index fe7486d107b72a..07f5ebf57c9b54 100644 --- a/Doc/library/http.client.rst +++ b/Doc/library/http.client.rst @@ -48,7 +48,7 @@ The module provides the following classes: The optional *blocksize* parameter sets the buffer size in bytes for 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. + 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:: @@ -68,7 +68,7 @@ The module provides the following classes: .. versionchanged:: 3.7 *blocksize* parameter was added. - .. versionchanged:: 3.15 + .. versionchanged:: next *max_response_headers* parameter was added. @@ -114,7 +114,7 @@ The module provides the following classes: The deprecated *key_file*, *cert_file* and *check_hostname* parameters have been removed. - .. versionchanged:: 3.15 + .. versionchanged:: next *max_response_headers* parameter was added. @@ -426,10 +426,10 @@ HTTPConnection Objects .. attribute:: HTTPConnection.max_response_headers - The maximum number of allowed response headers to help prevent denial of - service attacks. + 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:: 3.15 + .. versionadded:: next As an alternative to using the :meth:`~HTTPConnection.request` method described above, you can diff --git a/Lib/http/client.py b/Lib/http/client.py index 5a09d1e1dfaaba..0cce49cadc09fa 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -226,7 +226,7 @@ def _read_headers(fp, max_headers): break headers.append(line) if len(headers) > max_headers: - raise HTTPException("got more than %d headers" % max_headers) + raise HTTPException(f"got more than {max_headers} headers") return headers def _parse_header_lines(header_lines, _class=HTTPMessage): @@ -243,7 +243,7 @@ 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, _max_headers=None): +def parse_headers(fp, _class=HTTPMessage, *, _max_headers=None): """Parses only RFC2822 headers from a file pointer.""" headers = _read_headers(fp, _max_headers) @@ -322,7 +322,7 @@ def _read_status(self): raise BadStatusLine(line) return version, status, reason - def begin(self, _max_headers=None): + def begin(self, *, _max_headers=None): if self.headers is not None: # we've already started reading the response return @@ -868,7 +868,7 @@ def _get_content_length(body, method): return None def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, blocksize=8192, max_response_headers=None): + source_address=None, blocksize=8192, *, max_response_headers=None): self.timeout = timeout self.source_address = source_address self.blocksize = blocksize @@ -1434,7 +1434,7 @@ def getresponse(self): if self.max_response_headers is None: response.begin() else: - response.begin(self.max_response_headers) + response.begin(_max_headers=self.max_response_headers) except ConnectionError: self.close() raise 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 index 010c74e055dbab..71a991aa2c5ae6 100644 --- 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 @@ -1 +1,4 @@ -Adds a new max_response_headers parameter to HTTP/HTTPSConnection. +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. From f5450ddc212ccb215c3713af7e8ebc24aee0bb20 Mon Sep 17 00:00:00 2001 From: aeurielesn Date: Sun, 20 Jul 2025 13:15:12 +0200 Subject: [PATCH 6/8] Update docs --- Doc/whatsnew/3.15.rst | 10 ++++++++++ Misc/ACKS | 1 + 2 files changed, 11 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index ea369a36983497..54240cd64d95a7 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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 sets the maximum number of allowed response headers, + helping to prevent denial-of-service attacks. + (Contributed by Alexander Enrique Urieles Nieto in :gh:`136814`.) + + math ---- diff --git a/Misc/ACKS b/Misc/ACKS index fabd79b9f74210..35826bd713c0f6 100644 --- 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 From e93e33b0fde4c7215bd45320afcb33e3900c738a Mon Sep 17 00:00:00 2001 From: aeurielesn Date: Sun, 20 Jul 2025 14:15:31 +0200 Subject: [PATCH 7/8] Update docs --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 54240cd64d95a7..2a5bf085c864fd 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -237,7 +237,7 @@ http.client :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. - (Contributed by Alexander Enrique Urieles Nieto in :gh:`136814`.) + (Contributed by Alexander Enrique Urieles Nieto in :gh:`131724`.) math From 29b8348cb1cc7fc7620587b8954bb2fabffa4f70 Mon Sep 17 00:00:00 2001 From: Alexander Urieles Date: Sun, 20 Jul 2025 15:28:34 +0200 Subject: [PATCH 8/8] Update Doc/whatsnew/3.15.rst Co-authored-by: Petr Viktorin --- Doc/whatsnew/3.15.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 2a5bf085c864fd..a4993964d6f690 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -235,8 +235,8 @@ 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. + constructors. This parameter overrides the default maximum number of allowed + response headers. (Contributed by Alexander Enrique Urieles Nieto in :gh:`131724`.) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy