gh-131724: Add a new max_response_headers param to HTTP/HTTPSConnection (GH-136814)

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
This commit is contained in:
Alexander Urieles 2025-07-20 15:53:54 +02:00 committed by GitHub
parent 18a7f5dad8
commit 958657bbc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 105 additions and 18 deletions

View 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.

View 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
----

View 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:
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

View 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):

View file

@ -1954,6 +1954,7 @@ Adnan Umer
Utkarsh Upadhyay
Roger Upole
Daniel Urban
Alexander Enrique Urieles Nieto
Matthias Urlichs
Michael Urman
Hector Urtubia

View file

@ -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.