mirror of
https://github.com/python/cpython.git
synced 2025-12-22 16:39:14 +00:00
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:
parent
18a7f5dad8
commit
958657bbc3
6 changed files with 105 additions and 18 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1954,6 +1954,7 @@ Adnan Umer
|
|||
Utkarsh Upadhyay
|
||||
Roger Upole
|
||||
Daniel Urban
|
||||
Alexander Enrique Urieles Nieto
|
||||
Matthias Urlichs
|
||||
Michael Urman
|
||||
Hector Urtubia
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue