Skip to content

Commit 5a4c4a0

Browse files
gh-119451: Fix a potential denial of service in http.client (GH-119454)
Reading the whole body of the HTTP response could cause OOM if the Content-Length value is too large even if the server does not send a large amount of data. Now the HTTP client reads large data by chunks, therefore the amount of consumed memory is proportional to the amount of sent data.
1 parent d4fa707 commit 5a4c4a0

File tree

3 files changed

+95
-4
lines changed

3 files changed

+95
-4
lines changed

Lib/http/client.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@
111111
_MAXLINE = 65536
112112
_MAXHEADERS = 100
113113

114+
# Data larger than this will be read in chunks, to prevent extreme
115+
# overallocation.
116+
_MIN_READ_BUF_SIZE = 1 << 20
117+
118+
114119
# Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2)
115120
#
116121
# VCHAR = %x21-7E
@@ -642,10 +647,25 @@ def _safe_read(self, amt):
642647
reading. If the bytes are truly not available (due to EOF), then the
643648
IncompleteRead exception can be used to detect the problem.
644649
"""
645-
data = self.fp.read(amt)
646-
if len(data) < amt:
647-
raise IncompleteRead(data, amt-len(data))
648-
return data
650+
cursize = min(amt, _MIN_READ_BUF_SIZE)
651+
data = self.fp.read(cursize)
652+
if len(data) >= amt:
653+
return data
654+
if len(data) < cursize:
655+
raise IncompleteRead(data, amt - len(data))
656+
657+
data = io.BytesIO(data)
658+
data.seek(0, 2)
659+
while True:
660+
# This is a geometric increase in read size (never more than
661+
# doubling out the current length of data per loop iteration).
662+
delta = min(cursize, amt - cursize)
663+
data.write(self.fp.read(delta))
664+
if data.tell() >= amt:
665+
return data.getvalue()
666+
cursize += delta
667+
if data.tell() < cursize:
668+
raise IncompleteRead(data.getvalue(), amt - data.tell())
649669

650670
def _safe_readinto(self, b):
651671
"""Same as _safe_read, but for reading into a buffer."""

Lib/test/test_httplib.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,72 @@ def run_server():
15111511
thread.join()
15121512
self.assertEqual(result, b"proxied data\n")
15131513

1514+
def test_large_content_length(self):
1515+
serv = socket.create_server((HOST, 0))
1516+
self.addCleanup(serv.close)
1517+
1518+
def run_server():
1519+
[conn, address] = serv.accept()
1520+
with conn:
1521+
while conn.recv(1024):
1522+
conn.sendall(
1523+
b"HTTP/1.1 200 Ok\r\n"
1524+
b"Content-Length: %d\r\n"
1525+
b"\r\n" % size)
1526+
conn.sendall(b'A' * (size//3))
1527+
conn.sendall(b'B' * (size - size//3))
1528+
1529+
thread = threading.Thread(target=run_server)
1530+
thread.start()
1531+
self.addCleanup(thread.join, 1.0)
1532+
1533+
conn = client.HTTPConnection(*serv.getsockname())
1534+
try:
1535+
for w in range(15, 27):
1536+
size = 1 << w
1537+
conn.request("GET", "/")
1538+
with conn.getresponse() as response:
1539+
self.assertEqual(len(response.read()), size)
1540+
finally:
1541+
conn.close()
1542+
thread.join(1.0)
1543+
1544+
def test_large_content_length_truncated(self):
1545+
serv = socket.create_server((HOST, 0))
1546+
self.addCleanup(serv.close)
1547+
1548+
def run_server():
1549+
while True:
1550+
[conn, address] = serv.accept()
1551+
with conn:
1552+
conn.recv(1024)
1553+
if not size:
1554+
break
1555+
conn.sendall(
1556+
b"HTTP/1.1 200 Ok\r\n"
1557+
b"Content-Length: %d\r\n"
1558+
b"\r\n"
1559+
b"Text" % size)
1560+
1561+
thread = threading.Thread(target=run_server)
1562+
thread.start()
1563+
self.addCleanup(thread.join, 1.0)
1564+
1565+
conn = client.HTTPConnection(*serv.getsockname())
1566+
try:
1567+
for w in range(18, 65):
1568+
size = 1 << w
1569+
conn.request("GET", "/")
1570+
with conn.getresponse() as response:
1571+
self.assertRaises(client.IncompleteRead, response.read)
1572+
conn.close()
1573+
finally:
1574+
conn.close()
1575+
size = 0
1576+
conn.request("GET", "/")
1577+
conn.close()
1578+
thread.join(1.0)
1579+
15141580
def test_putrequest_override_domain_validation(self):
15151581
"""
15161582
It should be possible to override the default validation
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a potential memory denial of service in the :mod:`http.client` module.
2+
When connecting to a malicious server, it could cause
3+
an arbitrary amount of memory to be allocated.
4+
This could have led to symptoms including a :exc:`MemoryError`, swapping, out
5+
of memory (OOM) killed processes or containers, or even system crashes.

0 commit comments

Comments
 (0)