Skip to content

Commit f396864

Browse files
bpo-44022: Fix http client infinite line reading (DoS) after a HTTP 100 Continue (pythonGH-25916) (python#25933)
Fixes http.client potential denial of service where it could get stuck reading lines from a malicious server after a 100 Continue response. Co-authored-by: Gregory P. Smith <greg@krypto.org> (cherry picked from commit 47895e3) Co-authored-by: Gen Xu <xgbarry@gmail.com>
1 parent 515a7bc commit f396864

File tree

3 files changed

+32
-18
lines changed

3 files changed

+32
-18
lines changed

Lib/http/client.py

+21-17
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,11 @@ def getallmatchingheaders(self, name):
201201
lst.append(line)
202202
return lst
203203

204-
def parse_headers(fp, _class=HTTPMessage):
205-
"""Parses only RFC2822 headers from a file pointer.
206-
207-
email Parser wants to see strings rather than bytes.
208-
But a TextIOWrapper around self.rfile would buffer too many bytes
209-
from the stream, bytes which we later need to read as bytes.
210-
So we read the correct bytes here, as bytes, for email Parser
211-
to parse.
204+
def _read_headers(fp):
205+
"""Reads potential header lines into a list from a file pointer.
212206
207+
Length of line is limited by _MAXLINE, and number of
208+
headers is limited by _MAXHEADERS.
213209
"""
214210
headers = []
215211
while True:
@@ -221,6 +217,19 @@ def parse_headers(fp, _class=HTTPMessage):
221217
raise HTTPException("got more than %d headers" % _MAXHEADERS)
222218
if line in (b'\r\n', b'\n', b''):
223219
break
220+
return headers
221+
222+
def parse_headers(fp, _class=HTTPMessage):
223+
"""Parses only RFC2822 headers from a file pointer.
224+
225+
email Parser wants to see strings rather than bytes.
226+
But a TextIOWrapper around self.rfile would buffer too many bytes
227+
from the stream, bytes which we later need to read as bytes.
228+
So we read the correct bytes here, as bytes, for email Parser
229+
to parse.
230+
231+
"""
232+
headers = _read_headers(fp)
224233
hstring = b''.join(headers).decode('iso-8859-1')
225234
return email.parser.Parser(_class=_class).parsestr(hstring)
226235

@@ -308,15 +317,10 @@ def begin(self):
308317
if status != CONTINUE:
309318
break
310319
# skip the header from the 100 response
311-
while True:
312-
skip = self.fp.readline(_MAXLINE + 1)
313-
if len(skip) > _MAXLINE:
314-
raise LineTooLong("header line")
315-
skip = skip.strip()
316-
if not skip:
317-
break
318-
if self.debuglevel > 0:
319-
print("header:", skip)
320+
skipped_headers = _read_headers(self.fp)
321+
if self.debuglevel > 0:
322+
print("headers:", skipped_headers)
323+
del skipped_headers
320324

321325
self.code = self.status = status
322326
self.reason = reason.strip()

Lib/test/test_httplib.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,14 @@ def test_overflowing_header_line(self):
10031003
resp = client.HTTPResponse(FakeSocket(body))
10041004
self.assertRaises(client.LineTooLong, resp.begin)
10051005

1006+
def test_overflowing_header_limit_after_100(self):
1007+
body = (
1008+
'HTTP/1.1 100 OK\r\n'
1009+
'r\n' * 32768
1010+
)
1011+
resp = client.HTTPResponse(FakeSocket(body))
1012+
self.assertRaises(client.HTTPException, resp.begin)
1013+
10061014
def test_overflowing_chunked_line(self):
10071015
body = (
10081016
'HTTP/1.1 200 OK\r\n'
@@ -1404,7 +1412,7 @@ def readline(self, limit):
14041412
class OfflineTest(TestCase):
14051413
def test_all(self):
14061414
# Documented objects defined in the module should be in __all__
1407-
expected = {"responses"} # White-list documented dict() object
1415+
expected = {"responses"} # Allowlist documented dict() object
14081416
# HTTPMessage, parse_headers(), and the HTTP status code constants are
14091417
# intentionally omitted for simplicity
14101418
blacklist = {"HTTPMessage", "parse_headers"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mod:`http.client` now avoids infinitely reading potential HTTP headers after a
2+
``100 Continue`` status response from the server.

0 commit comments

Comments
 (0)