Skip to content

Commit 289f29b

Browse files
[3.13] gh-119451: Fix a potential denial of service in http.client (GH-119454) (#142139)
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 e7d2f62 commit 289f29b

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

647667
def _safe_readinto(self, b):
648668
"""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
@@ -1455,6 +1455,72 @@ def run_server():
14551455
thread.join()
14561456
self.assertEqual(result, b"proxied data\n")
14571457

1458+
def test_large_content_length(self):
1459+
serv = socket.create_server((HOST, 0))
1460+
self.addCleanup(serv.close)
1461+
1462+
def run_server():
1463+
[conn, address] = serv.accept()
1464+
with conn:
1465+
while conn.recv(1024):
1466+
conn.sendall(
1467+
b"HTTP/1.1 200 Ok\r\n"
1468+
b"Content-Length: %d\r\n"
1469+
b"\r\n" % size)
1470+
conn.sendall(b'A' * (size//3))
1471+
conn.sendall(b'B' * (size - size//3))
1472+
1473+
thread = threading.Thread(target=run_server)
1474+
thread.start()
1475+
self.addCleanup(thread.join, 1.0)
1476+
1477+
conn = client.HTTPConnection(*serv.getsockname())
1478+
try:
1479+
for w in range(15, 27):
1480+
size = 1 << w
1481+
conn.request("GET", "/")
1482+
with conn.getresponse() as response:
1483+
self.assertEqual(len(response.read()), size)
1484+
finally:
1485+
conn.close()
1486+
thread.join(1.0)
1487+
1488+
def test_large_content_length_truncated(self):
1489+
serv = socket.create_server((HOST, 0))
1490+
self.addCleanup(serv.close)
1491+
1492+
def run_server():
1493+
while True:
1494+
[conn, address] = serv.accept()
1495+
with conn:
1496+
conn.recv(1024)
1497+
if not size:
1498+
break
1499+
conn.sendall(
1500+
b"HTTP/1.1 200 Ok\r\n"
1501+
b"Content-Length: %d\r\n"
1502+
b"\r\n"
1503+
b"Text" % size)
1504+
1505+
thread = threading.Thread(target=run_server)
1506+
thread.start()
1507+
self.addCleanup(thread.join, 1.0)
1508+
1509+
conn = client.HTTPConnection(*serv.getsockname())
1510+
try:
1511+
for w in range(18, 65):
1512+
size = 1 << w
1513+
conn.request("GET", "/")
1514+
with conn.getresponse() as response:
1515+
self.assertRaises(client.IncompleteRead, response.read)
1516+
conn.close()
1517+
finally:
1518+
conn.close()
1519+
size = 0
1520+
conn.request("GET", "/")
1521+
conn.close()
1522+
thread.join(1.0)
1523+
14581524
def test_putrequest_override_domain_validation(self):
14591525
"""
14601526
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)