Skip to content

Commit f68d2d6

Browse files
bpo-44022: Fix http client infinite line reading (DoS) after a HTTP 100 Continue (GH-25916) (GH-25935)
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 3fbe961 commit f68d2d6

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
@@ -205,15 +205,11 @@ def getallmatchingheaders(self, name):
205205
lst.append(line)
206206
return lst
207207

208-
def parse_headers(fp, _class=HTTPMessage):
209-
"""Parses only RFC2822 headers from a file pointer.
210-
211-
email Parser wants to see strings rather than bytes.
212-
But a TextIOWrapper around self.rfile would buffer too many bytes
213-
from the stream, bytes which we later need to read as bytes.
214-
So we read the correct bytes here, as bytes, for email Parser
215-
to parse.
208+
def _read_headers(fp):
209+
"""Reads potential header lines into a list from a file pointer.
216210
211+
Length of line is limited by _MAXLINE, and number of
212+
headers is limited by _MAXHEADERS.
217213
"""
218214
headers = []
219215
while True:
@@ -225,6 +221,19 @@ def parse_headers(fp, _class=HTTPMessage):
225221
raise HTTPException("got more than %d headers" % _MAXHEADERS)
226222
if line in (b'\r\n', b'\n', b''):
227223
break
224+
return headers
225+
226+
def parse_headers(fp, _class=HTTPMessage):
227+
"""Parses only RFC2822 headers from a file pointer.
228+
229+
email Parser wants to see strings rather than bytes.
230+
But a TextIOWrapper around self.rfile would buffer too many bytes
231+
from the stream, bytes which we later need to read as bytes.
232+
So we read the correct bytes here, as bytes, for email Parser
233+
to parse.
234+
235+
"""
236+
headers = _read_headers(fp)
228237
hstring = b''.join(headers).decode('iso-8859-1')
229238
return email.parser.Parser(_class=_class).parsestr(hstring)
230239

@@ -312,15 +321,10 @@ def begin(self):
312321
if status != CONTINUE:
313322
break
314323
# skip the header from the 100 response
315-
while True:
316-
skip = self.fp.readline(_MAXLINE + 1)
317-
if len(skip) > _MAXLINE:
318-
raise LineTooLong("header line")
319-
skip = skip.strip()
320-
if not skip:
321-
break
322-
if self.debuglevel > 0:
323-
print("header:", skip)
324+
skipped_headers = _read_headers(self.fp)
325+
if self.debuglevel > 0:
326+
print("headers:", skipped_headers)
327+
del skipped_headers
324328

325329
self.code = self.status = status
326330
self.reason = reason.strip()

Lib/test/test_httplib.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,14 @@ def test_overflowing_header_line(self):
971971
resp = client.HTTPResponse(FakeSocket(body))
972972
self.assertRaises(client.LineTooLong, resp.begin)
973973

974+
def test_overflowing_header_limit_after_100(self):
975+
body = (
976+
'HTTP/1.1 100 OK\r\n'
977+
'r\n' * 32768
978+
)
979+
resp = client.HTTPResponse(FakeSocket(body))
980+
self.assertRaises(client.HTTPException, resp.begin)
981+
974982
def test_overflowing_chunked_line(self):
975983
body = (
976984
'HTTP/1.1 200 OK\r\n'
@@ -1377,7 +1385,7 @@ def readline(self, limit):
13771385
class OfflineTest(TestCase):
13781386
def test_all(self):
13791387
# Documented objects defined in the module should be in __all__
1380-
expected = {"responses"} # White-list documented dict() object
1388+
expected = {"responses"} # Allowlist documented dict() object
13811389
# HTTPMessage, parse_headers(), and the HTTP status code constants are
13821390
# intentionally omitted for simplicity
13831391
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)