Skip to content

Commit

Permalink
bpo-44022: Fix http client infinite line reading (DoS) after a HTTP 1…
Browse files Browse the repository at this point in the history
…00 Continue (pythonGH-25916)

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>
  • Loading branch information
gen-xu authored and miss-islington committed May 5, 2021
1 parent 5b1e502 commit f7fb35b
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 18 deletions.
38 changes: 21 additions & 17 deletions Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,11 @@ def getallmatchingheaders(self, name):
lst.append(line)
return lst

def parse_headers(fp, _class=HTTPMessage):
"""Parses only RFC2822 headers from a file pointer.
email Parser wants to see strings rather than bytes.
But a TextIOWrapper around self.rfile would buffer too many bytes
from the stream, bytes which we later need to read as bytes.
So we read the correct bytes here, as bytes, for email Parser
to parse.
def _read_headers(fp):
"""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 = []
while True:
Expand All @@ -225,6 +221,19 @@ def parse_headers(fp, _class=HTTPMessage):
raise HTTPException("got more than %d headers" % _MAXHEADERS)
if line in (b'\r\n', b'\n', b''):
break
return headers

def parse_headers(fp, _class=HTTPMessage):
"""Parses only RFC2822 headers from a file pointer.
email Parser wants to see strings rather than bytes.
But a TextIOWrapper around self.rfile would buffer too many bytes
from the stream, bytes which we later need to read as bytes.
So we read the correct bytes here, as bytes, for email Parser
to parse.
"""
headers = _read_headers(fp)
hstring = b''.join(headers).decode('iso-8859-1')
return email.parser.Parser(_class=_class).parsestr(hstring)

Expand Down Expand Up @@ -312,15 +321,10 @@ def begin(self):
if status != CONTINUE:
break
# skip the header from the 100 response
while True:
skip = self.fp.readline(_MAXLINE + 1)
if len(skip) > _MAXLINE:
raise LineTooLong("header line")
skip = skip.strip()
if not skip:
break
if self.debuglevel > 0:
print("header:", skip)
skipped_headers = _read_headers(self.fp)
if self.debuglevel > 0:
print("headers:", skipped_headers)
del skipped_headers

self.code = self.status = status
self.reason = reason.strip()
Expand Down
10 changes: 9 additions & 1 deletion Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,14 @@ def test_overflowing_header_line(self):
resp = client.HTTPResponse(FakeSocket(body))
self.assertRaises(client.LineTooLong, resp.begin)

def test_overflowing_header_limit_after_100(self):
body = (
'HTTP/1.1 100 OK\r\n'
'r\n' * 32768
)
resp = client.HTTPResponse(FakeSocket(body))
self.assertRaises(client.HTTPException, resp.begin)

def test_overflowing_chunked_line(self):
body = (
'HTTP/1.1 200 OK\r\n'
Expand Down Expand Up @@ -1377,7 +1385,7 @@ def readline(self, limit):
class OfflineTest(TestCase):
def test_all(self):
# Documented objects defined in the module should be in __all__
expected = {"responses"} # White-list documented dict() object
expected = {"responses"} # Allowlist documented dict() object
# HTTPMessage, parse_headers(), and the HTTP status code constants are
# intentionally omitted for simplicity
blacklist = {"HTTPMessage", "parse_headers"}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod:`http.client` now avoids infinitely reading potential HTTP headers after a
``100 Continue`` status response from the server.

0 comments on commit f7fb35b

Please sign in to comment.