Skip to content

Commit 47895e3

Browse files
gen-xugpshead
andauthored
bpo-44022: Fix http client infinite line reading (DoS) after a HTTP 100 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>
1 parent da5c808 commit 47895e3

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
@@ -202,15 +202,11 @@ def getallmatchingheaders(self, name):
202202
lst.append(line)
203203
return lst
204204

205-
def parse_headers(fp, _class=HTTPMessage):
206-
"""Parses only RFC2822 headers from a file pointer.
207-
208-
email Parser wants to see strings rather than bytes.
209-
But a TextIOWrapper around self.rfile would buffer too many bytes
210-
from the stream, bytes which we later need to read as bytes.
211-
So we read the correct bytes here, as bytes, for email Parser
212-
to parse.
205+
def _read_headers(fp):
206+
"""Reads potential header lines into a list from a file pointer.
213207
208+
Length of line is limited by _MAXLINE, and number of
209+
headers is limited by _MAXHEADERS.
214210
"""
215211
headers = []
216212
while True:
@@ -222,6 +218,19 @@ def parse_headers(fp, _class=HTTPMessage):
222218
raise HTTPException("got more than %d headers" % _MAXHEADERS)
223219
if line in (b'\r\n', b'\n', b''):
224220
break
221+
return headers
222+
223+
def parse_headers(fp, _class=HTTPMessage):
224+
"""Parses only RFC2822 headers from a file pointer.
225+
226+
email Parser wants to see strings rather than bytes.
227+
But a TextIOWrapper around self.rfile would buffer too many bytes
228+
from the stream, bytes which we later need to read as bytes.
229+
So we read the correct bytes here, as bytes, for email Parser
230+
to parse.
231+
232+
"""
233+
headers = _read_headers(fp)
225234
hstring = b''.join(headers).decode('iso-8859-1')
226235
return email.parser.Parser(_class=_class).parsestr(hstring)
227236

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

322326
self.code = self.status = status
323327
self.reason = reason.strip()

Lib/test/test_httplib.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,14 @@ def test_overflowing_header_line(self):
11801180
resp = client.HTTPResponse(FakeSocket(body))
11811181
self.assertRaises(client.LineTooLong, resp.begin)
11821182

1183+
def test_overflowing_header_limit_after_100(self):
1184+
body = (
1185+
'HTTP/1.1 100 OK\r\n'
1186+
'r\n' * 32768
1187+
)
1188+
resp = client.HTTPResponse(FakeSocket(body))
1189+
self.assertRaises(client.HTTPException, resp.begin)
1190+
11831191
def test_overflowing_chunked_line(self):
11841192
body = (
11851193
'HTTP/1.1 200 OK\r\n'
@@ -1581,7 +1589,7 @@ def readline(self, limit):
15811589
class OfflineTest(TestCase):
15821590
def test_all(self):
15831591
# Documented objects defined in the module should be in __all__
1584-
expected = {"responses"} # White-list documented dict() object
1592+
expected = {"responses"} # Allowlist documented dict() object
15851593
# HTTPMessage, parse_headers(), and the HTTP status code constants are
15861594
# intentionally omitted for simplicity
15871595
denylist = {"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)