mirror of https://github.com/python/cpython
bpo-44022: Fix http client infinite line reading (DoS) after a HTTP 100 Continue (GH-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>
This commit is contained in:
parent
da5c808fb5
commit
47895e31b6
|
@ -202,15 +202,11 @@ class HTTPMessage(email.message.Message):
|
|||
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:
|
||||
|
@ -222,6 +218,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)
|
||||
|
||||
|
@ -309,15 +318,10 @@ class HTTPResponse(io.BufferedIOBase):
|
|||
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()
|
||||
|
|
|
@ -1180,6 +1180,14 @@ class BasicTest(TestCase):
|
|||
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'
|
||||
|
@ -1581,7 +1589,7 @@ class Readliner:
|
|||
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
|
||||
denylist = {"HTTPMessage", "parse_headers"}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
mod:`http.client` now avoids infinitely reading potential HTTP headers after a
|
||||
``100 Continue`` status response from the server.
|
Loading…
Reference in New Issue