mirror of
https://github.com/python/cpython.git
synced 2025-09-26 10:19:53 +00:00
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:
parent
24f1d1a8a2
commit
60ba0b6847
3 changed files with 32 additions and 18 deletions
|
@ -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()
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod:`http.client` now avoids infinitely reading potential HTTP headers after a
|
||||||
|
``100 Continue`` status response from the server.
|
Loading…
Add table
Add a link
Reference in a new issue