Skip to content

Commit 0411337

Browse files
serhiy-storchakamiss-islington
authored andcommitted
[3.14] gh-119452: Fix a potential virtual memory allocation denial of service in http.server (GH-142216)
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. (cherry picked from commit 0e4f4f1) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 1173f80 commit 0411337

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

Lib/http/server.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@
128128

129129
DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8"
130130

131+
# Data larger than this will be read in chunks, to prevent extreme
132+
# overallocation.
133+
_MIN_READ_BUF_SIZE = 1 << 20
134+
131135
class HTTPServer(socketserver.TCPServer):
132136

133137
allow_reuse_address = 1 # Seems to make sense in testing environment
@@ -1215,7 +1219,18 @@ def run_cgi(self):
12151219
env = env
12161220
)
12171221
if self.command.lower() == "post" and nbytes > 0:
1218-
data = self.rfile.read(nbytes)
1222+
cursize = 0
1223+
data = self.rfile.read(min(nbytes, _MIN_READ_BUF_SIZE))
1224+
while len(data) < nbytes and len(data) != cursize:
1225+
cursize = len(data)
1226+
# This is a geometric increase in read size (never more
1227+
# than doubling out the current length of data per loop
1228+
# iteration).
1229+
delta = min(cursize, nbytes - cursize)
1230+
try:
1231+
data += self.rfile.read(delta)
1232+
except TimeoutError:
1233+
break
12191234
else:
12201235
data = None
12211236
# 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
@@ -667,6 +667,20 @@ def test_html_escape_filename(self):
667667
print("</pre>")
668668
"""
669669

670+
cgi_file7 = """\
671+
#!%s
672+
import os
673+
import sys
674+
675+
print("Content-type: text/plain")
676+
print()
677+
678+
content_length = int(os.environ["CONTENT_LENGTH"])
679+
body = sys.stdin.buffer.read(content_length)
680+
681+
print(f"{content_length} {len(body)}")
682+
"""
683+
670684

671685
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
672686
"This test can't be run reliably as root (issue #13308).")
@@ -696,6 +710,8 @@ def setUp(self):
696710
self.file3_path = None
697711
self.file4_path = None
698712
self.file5_path = None
713+
self.file6_path = None
714+
self.file7_path = None
699715

700716
# The shebang line should be pure ASCII: use symlink if possible.
701717
# See issue #7668.
@@ -750,6 +766,11 @@ def setUp(self):
750766
file6.write(cgi_file6 % self.pythonexe)
751767
os.chmod(self.file6_path, 0o777)
752768

769+
self.file7_path = os.path.join(self.cgi_dir, 'file7.py')
770+
with open(self.file7_path, 'w', encoding='utf-8') as file7:
771+
file7.write(cgi_file7 % self.pythonexe)
772+
os.chmod(self.file7_path, 0o777)
773+
753774
os.chdir(self.parent_dir)
754775

755776
def tearDown(self):
@@ -771,6 +792,8 @@ def tearDown(self):
771792
os.remove(self.file5_path)
772793
if self.file6_path:
773794
os.remove(self.file6_path)
795+
if self.file7_path:
796+
os.remove(self.file7_path)
774797
os.rmdir(self.cgi_child_dir)
775798
os.rmdir(self.cgi_dir)
776799
os.rmdir(self.cgi_dir_in_sub_dir)
@@ -840,6 +863,22 @@ def test_post(self):
840863

841864
self.assertEqual(res.read(), b'1, python, 123456' + self.linesep)
842865

866+
def test_large_content_length(self):
867+
for w in range(15, 25):
868+
size = 1 << w
869+
body = b'X' * size
870+
headers = {'Content-Length' : str(size)}
871+
res = self.request('/cgi-bin/file7.py', 'POST', body, headers)
872+
self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep)
873+
874+
def test_large_content_length_truncated(self):
875+
with support.swap_attr(self.request_handler, 'timeout', 0.001):
876+
for w in range(18, 65):
877+
size = 1 << w
878+
headers = {'Content-Length' : str(size)}
879+
res = self.request('/cgi-bin/file1.py', 'POST', b'x', headers)
880+
self.assertEqual(res.read(), b'Hello World' + self.linesep)
881+
843882
def test_invaliduri(self):
844883
res = self.request('/cgi-bin/invalid')
845884
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)