Skip to content

Commit 4bfd8b4

Browse files
[3.14] gh-119452: Fix a potential virtual memory allocation denial of service in http.server
The CGI server on Windows could consume the amount of memory specified in the Content-Length header of the request even if the client does not send such much data. Now it reads the POST request body by chunks, therefore the memory consumption is proportional to the amount of sent data.
1 parent 206f196 commit 4bfd8b4

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

Lib/http/server.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@
134134

135135
DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8"
136136

137+
# Data larger than this will be read in chunks, to prevent extreme
138+
# overallocation.
139+
_MIN_READ_BUF_SIZE = 1 << 20
140+
137141
class HTTPServer(socketserver.TCPServer):
138142

139143
allow_reuse_address = True # Seems to make sense in testing environment
@@ -1284,7 +1288,16 @@ def run_cgi(self):
12841288
env = env
12851289
)
12861290
if self.command.lower() == "post" and nbytes > 0:
1287-
data = self.rfile.read(nbytes)
1291+
cursize = 0
1292+
data = self.rfile.read(min(nbytes, _MIN_READ_BUF_SIZE))
1293+
while (len(data) < nbytes and len(data) != cursize and
1294+
select.select([self.rfile._sock], [], [], self.timeout)[0]):
1295+
cursize = len(data)
1296+
# This is a geometric increase in read size (never more
1297+
# than doubling our the current length of data per loop
1298+
# iteration).
1299+
delta = min(cursize, nbytes - cursize)
1300+
data += self.rfile.read(delta)
12881301
else:
12891302
data = None
12901303
# throw away additional data [see bug #427345]

Lib/test/test_httpservers.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,20 @@ def test_path_without_leading_slash(self):
913913
print("</pre>")
914914
"""
915915

916+
cgi_file7 = """\
917+
#!%s
918+
import os
919+
import sys
920+
921+
print("Content-type: text/plain")
922+
print()
923+
924+
content_length = int(os.environ["CONTENT_LENGTH"])
925+
body = sys.stdin.buffer.read(content_length)
926+
927+
print(f"{content_length} {len(body)}")
928+
"""
929+
916930

917931
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
918932
"This test can't be run reliably as root (issue #13308).")
@@ -952,6 +966,8 @@ def setUp(self):
952966
self.file3_path = None
953967
self.file4_path = None
954968
self.file5_path = None
969+
self.file6_path = None
970+
self.file7_path = None
955971

956972
# The shebang line should be pure ASCII: use symlink if possible.
957973
# See issue #7668.
@@ -1006,6 +1022,11 @@ def setUp(self):
10061022
file6.write(cgi_file6 % self.pythonexe)
10071023
os.chmod(self.file6_path, 0o777)
10081024

1025+
self.file7_path = os.path.join(self.cgi_dir, 'file7.py')
1026+
with open(self.file7_path, 'w', encoding='utf-8') as file7:
1027+
file7.write(cgi_file7 % self.pythonexe)
1028+
os.chmod(self.file7_path, 0o777)
1029+
10091030
os.chdir(self.parent_dir)
10101031

10111032
def tearDown(self):
@@ -1028,6 +1049,8 @@ def tearDown(self):
10281049
os.remove(self.file5_path)
10291050
if self.file6_path:
10301051
os.remove(self.file6_path)
1052+
if self.file7_path:
1053+
os.remove(self.file7_path)
10311054
os.rmdir(self.cgi_child_dir)
10321055
os.rmdir(self.cgi_dir)
10331056
os.rmdir(self.cgi_dir_in_sub_dir)
@@ -1100,6 +1123,22 @@ def test_post(self):
11001123

11011124
self.assertEqual(res.read(), b'1, python, 123456' + self.linesep)
11021125

1126+
def test_large_content_length(self):
1127+
for w in range(15, 25):
1128+
size = 1 << w
1129+
body = b'X' * size
1130+
headers = {'Content-Length' : str(size)}
1131+
res = self.request('/cgi-bin/file7.py', 'POST', body, headers)
1132+
self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep)
1133+
1134+
def test_large_content_length_truncated(self):
1135+
with support.swap_attr(self.request_handler, 'timeout', 0.001):
1136+
for w in range(18, 65):
1137+
size = 1 << w
1138+
headers = {'Content-Length' : str(size)}
1139+
res = self.request('/cgi-bin/file1.py', 'POST', b'x', headers)
1140+
self.assertEqual(res.read(), b'Hello World' + self.linesep)
1141+
11031142
def test_invaliduri(self):
11041143
res = self.request('/cgi-bin/invalid')
11051144
res.read()
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.server` module.
2+
When a malicious user is connected to the CGI server on Windows, 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)