Skip to content

Commit 25166b1

Browse files
gh-119451: Fix OOM vulnerability in http.client
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 858b9e8 commit 25166b1

File tree

3 files changed

+49
-3
lines changed

3 files changed

+49
-3
lines changed

Lib/http/client.py

Lines changed: 13 additions & 3 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+
_SAFE_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
@@ -637,9 +642,14 @@ def _safe_read(self, amt):
637642
reading. If the bytes are truly not available (due to EOF), then the
638643
IncompleteRead exception can be used to detect the problem.
639644
"""
640-
data = self.fp.read(amt)
641-
if len(data) < amt:
642-
raise IncompleteRead(data, amt-len(data))
645+
cursize = min(amt, _SAFE_BUF_SIZE)
646+
data = self.fp.read(cursize)
647+
while len(data) < amt:
648+
if len(data) < cursize:
649+
raise IncompleteRead(data, amt-len(data))
650+
delta = min(cursize, amt - cursize)
651+
data += self.fp.read(cursize)
652+
cursize += delta
643653
return data
644654

645655
def _safe_readinto(self, b):

Lib/test/test_httplib.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,39 @@ def run_server():
14361436
thread.join()
14371437
self.assertEqual(result, b"proxied data\n")
14381438

1439+
def test_large_content_length(self):
1440+
serv = socket.create_server((HOST, 0))
1441+
self.addCleanup(serv.close)
1442+
1443+
def run_server():
1444+
while True:
1445+
[conn, address] = serv.accept()
1446+
with conn:
1447+
conn.recv(1024)
1448+
if not size:
1449+
break
1450+
body = b"HTTP/1.1 200 Ok\r\nContent-Length: %d\r\n\r\nText" % size
1451+
conn.sendall(body)
1452+
1453+
thread = threading.Thread(target=run_server)
1454+
thread.start()
1455+
self.addCleanup(thread.join, 1.0)
1456+
1457+
conn = client.HTTPConnection(*serv.getsockname())
1458+
try:
1459+
for w in range(18, 65):
1460+
size = 1 << w
1461+
conn.request("GET", "/")
1462+
with conn.getresponse() as response:
1463+
self.assertRaises(client.IncompleteRead, response.read)
1464+
conn.close()
1465+
finally:
1466+
conn.close()
1467+
size = 0
1468+
conn.request("GET", "/")
1469+
conn.close()
1470+
thread.join()
1471+
14391472
def test_putrequest_override_domain_validation(self):
14401473
"""
14411474
It should be possible to override the default validation
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix OOM vulnerability in :mod:`http.client`, when reading the whole body of
2+
a specially prepared small HTTP response could cause consuming an arbitrary
3+
amount of memory.

0 commit comments

Comments
 (0)