Skip to content

Commit 958657b

Browse files
aeurielesnblurb-it[bot]encukou
authored
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>
1 parent 18a7f5d commit 958657b

File tree

6 files changed

+105
-18
lines changed

6 files changed

+105
-18
lines changed

Doc/library/http.client.rst

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The module provides the following classes:
3434

3535

3636
.. class:: HTTPConnection(host, port=None[, timeout], source_address=None, \
37-
blocksize=8192)
37+
blocksize=8192, max_response_headers=None)
3838

3939
An :class:`HTTPConnection` instance represents one transaction with an HTTP
4040
server. It should be instantiated by passing it a host and optional port
@@ -46,7 +46,9 @@ The module provides the following classes:
4646
The optional *source_address* parameter may be a tuple of a (host, port)
4747
to use as the source address the HTTP connection is made from.
4848
The optional *blocksize* parameter sets the buffer size in bytes for
49-
sending a file-like message body.
49+
sending a file-like message body. The optional *max_response_headers*
50+
parameter sets the maximum number of allowed response headers to help
51+
prevent denial-of-service attacks, otherwise the default value (100) is used.
5052

5153
For example, the following calls all create instances that connect to the server
5254
at the same host and port::
@@ -66,10 +68,13 @@ The module provides the following classes:
6668
.. versionchanged:: 3.7
6769
*blocksize* parameter was added.
6870

71+
.. versionchanged:: next
72+
*max_response_headers* parameter was added.
73+
6974

7075
.. class:: HTTPSConnection(host, port=None, *[, timeout], \
7176
source_address=None, context=None, \
72-
blocksize=8192)
77+
blocksize=8192, max_response_headers=None)
7378

7479
A subclass of :class:`HTTPConnection` that uses SSL for communication with
7580
secure servers. Default port is ``443``. If *context* is specified, it
@@ -109,6 +114,9 @@ The module provides the following classes:
109114
The deprecated *key_file*, *cert_file* and *check_hostname* parameters
110115
have been removed.
111116

117+
.. versionchanged:: next
118+
*max_response_headers* parameter was added.
119+
112120

113121
.. class:: HTTPResponse(sock, debuglevel=0, method=None, url=None)
114122

@@ -416,6 +424,14 @@ HTTPConnection Objects
416424
.. versionadded:: 3.7
417425

418426

427+
.. attribute:: HTTPConnection.max_response_headers
428+
429+
The maximum number of allowed response headers to help prevent denial-of-service
430+
attacks. By default, the maximum number of allowed headers is set to 100.
431+
432+
.. versionadded:: next
433+
434+
419435
As an alternative to using the :meth:`~HTTPConnection.request` method described above, you can
420436
also send your request step by step, by using the four functions below.
421437

Doc/whatsnew/3.15.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,16 @@ difflib
230230
(Contributed by Jiahao Li in :gh:`134580`.)
231231

232232

233+
http.client
234+
-----------
235+
236+
* A new *max_response_headers* keyword-only parameter has been added to
237+
:class:`~http.client.HTTPConnection` and :class:`~http.client.HTTPSConnection`
238+
constructors. This parameter overrides the default maximum number of allowed
239+
response headers.
240+
(Contributed by Alexander Enrique Urieles Nieto in :gh:`131724`.)
241+
242+
233243
math
234244
----
235245

Lib/http/client.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -209,22 +209,24 @@ def getallmatchingheaders(self, name):
209209
lst.append(line)
210210
return lst
211211

212-
def _read_headers(fp):
212+
def _read_headers(fp, max_headers):
213213
"""Reads potential header lines into a list from a file pointer.
214214
215215
Length of line is limited by _MAXLINE, and number of
216-
headers is limited by _MAXHEADERS.
216+
headers is limited by max_headers.
217217
"""
218218
headers = []
219+
if max_headers is None:
220+
max_headers = _MAXHEADERS
219221
while True:
220222
line = fp.readline(_MAXLINE + 1)
221223
if len(line) > _MAXLINE:
222224
raise LineTooLong("header line")
223-
headers.append(line)
224-
if len(headers) > _MAXHEADERS:
225-
raise HTTPException("got more than %d headers" % _MAXHEADERS)
226225
if line in (b'\r\n', b'\n', b''):
227226
break
227+
headers.append(line)
228+
if len(headers) > max_headers:
229+
raise HTTPException(f"got more than {max_headers} headers")
228230
return headers
229231

230232
def _parse_header_lines(header_lines, _class=HTTPMessage):
@@ -241,10 +243,10 @@ def _parse_header_lines(header_lines, _class=HTTPMessage):
241243
hstring = b''.join(header_lines).decode('iso-8859-1')
242244
return email.parser.Parser(_class=_class).parsestr(hstring)
243245

244-
def parse_headers(fp, _class=HTTPMessage):
246+
def parse_headers(fp, _class=HTTPMessage, *, _max_headers=None):
245247
"""Parses only RFC2822 headers from a file pointer."""
246248

247-
headers = _read_headers(fp)
249+
headers = _read_headers(fp, _max_headers)
248250
return _parse_header_lines(headers, _class)
249251

250252

@@ -320,7 +322,7 @@ def _read_status(self):
320322
raise BadStatusLine(line)
321323
return version, status, reason
322324

323-
def begin(self):
325+
def begin(self, *, _max_headers=None):
324326
if self.headers is not None:
325327
# we've already started reading the response
326328
return
@@ -331,7 +333,7 @@ def begin(self):
331333
if status != CONTINUE:
332334
break
333335
# skip the header from the 100 response
334-
skipped_headers = _read_headers(self.fp)
336+
skipped_headers = _read_headers(self.fp, _max_headers)
335337
if self.debuglevel > 0:
336338
print("headers:", skipped_headers)
337339
del skipped_headers
@@ -346,7 +348,9 @@ def begin(self):
346348
else:
347349
raise UnknownProtocol(version)
348350

349-
self.headers = self.msg = parse_headers(self.fp)
351+
self.headers = self.msg = parse_headers(
352+
self.fp, _max_headers=_max_headers
353+
)
350354

351355
if self.debuglevel > 0:
352356
for hdr, val in self.headers.items():
@@ -864,7 +868,7 @@ def _get_content_length(body, method):
864868
return None
865869

866870
def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
867-
source_address=None, blocksize=8192):
871+
source_address=None, blocksize=8192, *, max_response_headers=None):
868872
self.timeout = timeout
869873
self.source_address = source_address
870874
self.blocksize = blocksize
@@ -877,6 +881,7 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
877881
self._tunnel_port = None
878882
self._tunnel_headers = {}
879883
self._raw_proxy_headers = None
884+
self.max_response_headers = max_response_headers
880885

881886
(self.host, self.port) = self._get_hostport(host, port)
882887

@@ -969,7 +974,7 @@ def _tunnel(self):
969974
try:
970975
(version, code, message) = response._read_status()
971976

972-
self._raw_proxy_headers = _read_headers(response.fp)
977+
self._raw_proxy_headers = _read_headers(response.fp, self.max_response_headers)
973978

974979
if self.debuglevel > 0:
975980
for header in self._raw_proxy_headers:
@@ -1426,7 +1431,10 @@ def getresponse(self):
14261431

14271432
try:
14281433
try:
1429-
response.begin()
1434+
if self.max_response_headers is None:
1435+
response.begin()
1436+
else:
1437+
response.begin(_max_headers=self.max_response_headers)
14301438
except ConnectionError:
14311439
self.close()
14321440
raise
@@ -1457,10 +1465,12 @@ class HTTPSConnection(HTTPConnection):
14571465

14581466
def __init__(self, host, port=None,
14591467
*, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
1460-
source_address=None, context=None, blocksize=8192):
1468+
source_address=None, context=None, blocksize=8192,
1469+
max_response_headers=None):
14611470
super(HTTPSConnection, self).__init__(host, port, timeout,
14621471
source_address,
1463-
blocksize=blocksize)
1472+
blocksize=blocksize,
1473+
max_response_headers=max_response_headers)
14641474
if context is None:
14651475
context = _create_https_context(self._http_vsn)
14661476
self._context = context

Lib/test/test_httplib.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,52 @@ def test_headers_debuglevel(self):
386386
self.assertEqual(lines[2], "header: Second: val1")
387387
self.assertEqual(lines[3], "header: Second: val2")
388388

389+
def test_max_response_headers(self):
390+
max_headers = client._MAXHEADERS + 20
391+
headers = [f"Name{i}: Value{i}".encode() for i in range(max_headers)]
392+
body = b"HTTP/1.1 200 OK\r\n" + b"\r\n".join(headers)
393+
394+
with self.subTest(max_headers=None):
395+
sock = FakeSocket(body)
396+
resp = client.HTTPResponse(sock)
397+
with self.assertRaisesRegex(
398+
client.HTTPException, f"got more than 100 headers"
399+
):
400+
resp.begin()
401+
402+
with self.subTest(max_headers=max_headers):
403+
sock = FakeSocket(body)
404+
resp = client.HTTPResponse(sock)
405+
resp.begin(_max_headers=max_headers)
406+
407+
def test_max_connection_headers(self):
408+
max_headers = client._MAXHEADERS + 20
409+
headers = (
410+
f"Name{i}: Value{i}".encode() for i in range(max_headers - 1)
411+
)
412+
body = (
413+
b"HTTP/1.1 200 OK\r\n"
414+
+ b"\r\n".join(headers)
415+
+ b"\r\nContent-Length: 12\r\n\r\nDummy body\r\n"
416+
)
417+
418+
with self.subTest(max_headers=None):
419+
conn = client.HTTPConnection("example.com")
420+
conn.sock = FakeSocket(body)
421+
conn.request("GET", "/")
422+
with self.assertRaisesRegex(
423+
client.HTTPException, f"got more than {client._MAXHEADERS} headers"
424+
):
425+
response = conn.getresponse()
426+
427+
with self.subTest(max_headers=None):
428+
conn = client.HTTPConnection(
429+
"example.com", max_response_headers=max_headers
430+
)
431+
conn.sock = FakeSocket(body)
432+
conn.request("GET", "/")
433+
response = conn.getresponse()
434+
response.read()
389435

390436
class HttpMethodTests(TestCase):
391437
def test_invalid_method_names(self):

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,7 @@ Adnan Umer
19541954
Utkarsh Upadhyay
19551955
Roger Upole
19561956
Daniel Urban
1957+
Alexander Enrique Urieles Nieto
19571958
Matthias Urlichs
19581959
Michael Urman
19591960
Hector Urtubia
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
In :mod:`http.client`, a new *max_response_headers* keyword-only parameter has been
2+
added to :class:`~http.client.HTTPConnection` and :class:`~http.client.HTTPSConnection`
3+
constructors. This parameter sets the maximum number of allowed response headers,
4+
helping to prevent denial-of-service attacks.

0 commit comments

Comments
 (0)