Skip to content

Commit afc40bd

Browse files
[3.11] gh-119451: Fix a potential denial of service in http.client (GH-119454) (#142141)
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. (cherry picked from commit 5a4c4a0) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent a46c10e commit afc40bd

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
@@ -635,10 +640,25 @@ def _safe_read(self, amt):
635640
reading. If the bytes are truly not available (due to EOF), then the
636641
IncompleteRead exception can be used to detect the problem.
637642
"""
638-
data = self.fp.read(amt)
639-
if len(data) < amt:
640-
raise IncompleteRead(data, amt-len(data))
641-
return data
643+
cursize = min(amt, _MIN_READ_BUF_SIZE)
644+
data = self.fp.read(cursize)
645+
if len(data) >= amt:
646+
return data
647+
if len(data) < cursize:
648+
raise IncompleteRead(data, amt - len(data))
649+
650+
data = io.BytesIO(data)
651+
data.seek(0, 2)
652+
while True:
653+
# This is a geometric increase in read size (never more than
654+
# doubling out the current length of data per loop iteration).
655+
delta = min(cursize, amt - cursize)
656+
data.write(self.fp.read(delta))
657+
if data.tell() >= amt:
658+
return data.getvalue()
659+
cursize += delta
660+
if data.tell() < cursize:
661+
raise IncompleteRead(data.getvalue(), amt - data.tell())
642662

643663
def _safe_readinto(self, b):
644664
"""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
@@ -1390,6 +1390,72 @@ def run_server():
13901390
thread.join()
13911391
self.assertEqual(result, b"proxied data\n")
13921392

1393+
def test_large_content_length(self):
1394+
serv = socket.create_server((HOST, 0))
1395+
self.addCleanup(serv.close)
1396+
1397+
def run_server():
1398+
[conn, address] = serv.accept()
1399+
with conn:
1400+
while conn.recv(1024):
1401+
conn.sendall(
1402+
b"HTTP/1.1 200 Ok\r\n"
1403+
b"Content-Length: %d\r\n"
1404+
b"\r\n" % size)
1405+
conn.sendall(b'A' * (size//3))
1406+
conn.sendall(b'B' * (size - size//3))
1407+
1408+
thread = threading.Thread(target=run_server)
1409+
thread.start()
1410+
self.addCleanup(thread.join, 1.0)
1411+
1412+
conn = client.HTTPConnection(*serv.getsockname())
1413+
try:
1414+
for w in range(15, 27):
1415+
size = 1 << w
1416+
conn.request("GET", "/")
1417+
with conn.getresponse() as response:
1418+
self.assertEqual(len(response.read()), size)
1419+
finally:
1420+
conn.close()
1421+
thread.join(1.0)
1422+
1423+
def test_large_content_length_truncated(self):
1424+
serv = socket.create_server((HOST, 0))
1425+
self.addCleanup(serv.close)
1426+
1427+
def run_server():
1428+
while True:
1429+
[conn, address] = serv.accept()
1430+
with conn:
1431+
conn.recv(1024)
1432+
if not size:
1433+
break
1434+
conn.sendall(
1435+
b"HTTP/1.1 200 Ok\r\n"
1436+
b"Content-Length: %d\r\n"
1437+
b"\r\n"
1438+
b"Text" % size)
1439+
1440+
thread = threading.Thread(target=run_server)
1441+
thread.start()
1442+
self.addCleanup(thread.join, 1.0)
1443+
1444+
conn = client.HTTPConnection(*serv.getsockname())
1445+
try:
1446+
for w in range(18, 65):
1447+
size = 1 << w
1448+
conn.request("GET", "/")
1449+
with conn.getresponse() as response:
1450+
self.assertRaises(client.IncompleteRead, response.read)
1451+
conn.close()
1452+
finally:
1453+
conn.close()
1454+
size = 0
1455+
conn.request("GET", "/")
1456+
conn.close()
1457+
thread.join(1.0)
1458+
13931459
def test_putrequest_override_domain_validation(self):
13941460
"""
13951461
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)