bpo-44022: Fix http client infinite line reading (DoS) after a HTTP 100 Continue (GH-25916) (GH-25931)

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 47895e31b6)

Co-authored-by: Gen Xu <xgbarry@gmail.com>
This commit is contained in:
Miss Islington (bot) 2021-05-05 16:14:28 -07:00 committed by GitHub
parent 24f1d1a8a2
commit 60ba0b6847
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 32 additions and 18 deletions

View file

@ -202,15 +202,11 @@ class HTTPMessage(email.message.Message):
lst.append(line) lst.append(line)
return lst return lst
def parse_headers(fp, _class=HTTPMessage): def _read_headers(fp):
"""Parses only RFC2822 headers from a file pointer. """Reads potential header lines into a list 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.
Length of line is limited by _MAXLINE, and number of
headers is limited by _MAXHEADERS.
""" """
headers = [] headers = []
while True: while True:
@ -222,6 +218,19 @@ def parse_headers(fp, _class=HTTPMessage):
raise HTTPException("got more than %d headers" % _MAXHEADERS) raise HTTPException("got more than %d headers" % _MAXHEADERS)
if line in (b'\r\n', b'\n', b''): if line in (b'\r\n', b'\n', b''):
break 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') hstring = b''.join(headers).decode('iso-8859-1')
return email.parser.Parser(_class=_class).parsestr(hstring) return email.parser.Parser(_class=_class).parsestr(hstring)
@ -309,15 +318,10 @@ class HTTPResponse(io.BufferedIOBase):
if status != CONTINUE: if status != CONTINUE:
break break
# skip the header from the 100 response # skip the header from the 100 response
while True: skipped_headers = _read_headers(self.fp)
skip = self.fp.readline(_MAXLINE + 1) if self.debuglevel > 0:
if len(skip) > _MAXLINE: print("headers:", skipped_headers)
raise LineTooLong("header line") del skipped_headers
skip = skip.strip()
if not skip:
break
if self.debuglevel > 0:
print("header:", skip)
self.code = self.status = status self.code = self.status = status
self.reason = reason.strip() self.reason = reason.strip()

View file

@ -1180,6 +1180,14 @@ class BasicTest(TestCase):
resp = client.HTTPResponse(FakeSocket(body)) resp = client.HTTPResponse(FakeSocket(body))
self.assertRaises(client.LineTooLong, resp.begin) 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): def test_overflowing_chunked_line(self):
body = ( body = (
'HTTP/1.1 200 OK\r\n' 'HTTP/1.1 200 OK\r\n'
@ -1581,7 +1589,7 @@ class Readliner:
class OfflineTest(TestCase): class OfflineTest(TestCase):
def test_all(self): def test_all(self):
# Documented objects defined in the module should be in __all__ # 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 # HTTPMessage, parse_headers(), and the HTTP status code constants are
# intentionally omitted for simplicity # intentionally omitted for simplicity
denylist = {"HTTPMessage", "parse_headers"} denylist = {"HTTPMessage", "parse_headers"}

View file

@ -0,0 +1,2 @@
mod:`http.client` now avoids infinitely reading potential HTTP headers after a
``100 Continue`` status response from the server.