Skip to content

Commit b3a7998

Browse files
serhiy-storchakamiss-islington
authored andcommitted
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 1173f80 commit b3a7998

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

636656
def _safe_readinto(self, b):
637657
"""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
@@ -1226,6 +1226,72 @@ def run_server():
12261226
thread.join()
12271227
self.assertEqual(result, b"proxied data\n")
12281228

1229+
def test_large_content_length(self):
1230+
serv = socket.create_server((HOST, 0))
1231+
self.addCleanup(serv.close)
1232+
1233+
def run_server():
1234+
[conn, address] = serv.accept()
1235+
with conn:
1236+
while conn.recv(1024):
1237+
conn.sendall(
1238+
b"HTTP/1.1 200 Ok\r\n"
1239+
b"Content-Length: %d\r\n"
1240+
b"\r\n" % size)
1241+
conn.sendall(b'A' * (size//3))
1242+
conn.sendall(b'B' * (size - size//3))
1243+
1244+
thread = threading.Thread(target=run_server)
1245+
thread.start()
1246+
self.addCleanup(thread.join, 1.0)
1247+
1248+
conn = client.HTTPConnection(*serv.getsockname())
1249+
try:
1250+
for w in range(15, 27):
1251+
size = 1 << w
1252+
conn.request("GET", "/")
1253+
with conn.getresponse() as response:
1254+
self.assertEqual(len(response.read()), size)
1255+
finally:
1256+
conn.close()
1257+
thread.join(1.0)
1258+
1259+
def test_large_content_length_truncated(self):
1260+
serv = socket.create_server((HOST, 0))
1261+
self.addCleanup(serv.close)
1262+
1263+
def run_server():
1264+
while True:
1265+
[conn, address] = serv.accept()
1266+
with conn:
1267+
conn.recv(1024)
1268+
if not size:
1269+
break
1270+
conn.sendall(
1271+
b"HTTP/1.1 200 Ok\r\n"
1272+
b"Content-Length: %d\r\n"
1273+
b"\r\n"
1274+
b"Text" % size)
1275+
1276+
thread = threading.Thread(target=run_server)
1277+
thread.start()
1278+
self.addCleanup(thread.join, 1.0)
1279+
1280+
conn = client.HTTPConnection(*serv.getsockname())
1281+
try:
1282+
for w in range(18, 65):
1283+
size = 1 << w
1284+
conn.request("GET", "/")
1285+
with conn.getresponse() as response:
1286+
self.assertRaises(client.IncompleteRead, response.read)
1287+
conn.close()
1288+
finally:
1289+
conn.close()
1290+
size = 0
1291+
conn.request("GET", "/")
1292+
conn.close()
1293+
thread.join(1.0)
1294+
12291295
def test_putrequest_override_domain_validation(self):
12301296
"""
12311297
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)