Skip to content

Commit 861bded

Browse files
serhiy-storchakamiss-islington
authored andcommitted
[3.14] pythongh-119452: Fix a potential virtual memory allocation denial of service in http.server (pythonGH-119455)
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, so that the memory consumption is proportional to the amount of sent data. (cherry picked from commit 29c657a) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent a183a11 commit 861bded

File tree

3 files changed

+57
-1
lines changed

3 files changed

+57
-1
lines changed

Lib/http/server.py

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

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

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

132136
allow_reuse_address = 1 # Seems to make sense in testing environment
@@ -1218,7 +1222,16 @@ def run_cgi(self):
12181222
env = env
12191223
)
12201224
if self.command.lower() == "post" and nbytes > 0:
1221-
data = self.rfile.read(nbytes)
1225+
cursize = 0
1226+
data = self.rfile.read(min(nbytes, _MIN_READ_BUF_SIZE))
1227+
while (len(data) < nbytes and len(data) != cursize and
1228+
select.select([self.rfile._sock], [], [], 0)[0]):
1229+
cursize = len(data)
1230+
# This is a geometric increase in read size (never more
1231+
# than doubling our the current length of data per loop
1232+
# iteration).
1233+
delta = min(cursize, nbytes - cursize)
1234+
data += self.rfile.read(delta)
12221235
else:
12231236
data = None
12241237
# throw away additional data [see bug #427345]

Lib/test/test_httpservers.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,20 @@ def test_html_escape_filename(self):
695695
print("</pre>")
696696
"""
697697

698+
cgi_file7 = """\
699+
#!%s
700+
import os
701+
import sys
702+
703+
print("Content-type: text/plain")
704+
print()
705+
706+
content_length = int(os.environ["CONTENT_LENGTH"])
707+
body = sys.stdin.buffer.read(content_length)
708+
709+
print(f"{content_length} {len(body)}")
710+
"""
711+
698712

699713
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
700714
"This test can't be run reliably as root (issue #13308).")
@@ -728,6 +742,8 @@ def setUp(self):
728742
self.file3_path = None
729743
self.file4_path = None
730744
self.file5_path = None
745+
self.file6_path = None
746+
self.file7_path = None
731747

732748
# The shebang line should be pure ASCII: use symlink if possible.
733749
# See issue #7668.
@@ -782,6 +798,11 @@ def setUp(self):
782798
file6.write(cgi_file6 % self.pythonexe)
783799
os.chmod(self.file6_path, 0o777)
784800

801+
self.file7_path = os.path.join(self.cgi_dir, 'file7.py')
802+
with open(self.file7_path, 'w', encoding='utf-8') as file7:
803+
file7.write(cgi_file7 % self.pythonexe)
804+
os.chmod(self.file7_path, 0o777)
805+
785806
os.chdir(self.parent_dir)
786807

787808
def tearDown(self):
@@ -803,6 +824,8 @@ def tearDown(self):
803824
os.remove(self.file5_path)
804825
if self.file6_path:
805826
os.remove(self.file6_path)
827+
if self.file7_path:
828+
os.remove(self.file7_path)
806829
os.rmdir(self.cgi_child_dir)
807830
os.rmdir(self.cgi_dir)
808831
os.rmdir(self.cgi_dir_in_sub_dir)
@@ -875,6 +898,21 @@ def test_post(self):
875898

876899
self.assertEqual(res.read(), b'1, python, 123456' + self.linesep)
877900

901+
def test_large_content_length(self):
902+
for w in range(15, 25):
903+
size = 1 << w
904+
body = b'X' * size
905+
headers = {'Content-Length' : str(size)}
906+
res = self.request('/cgi-bin/file7.py', 'POST', body, headers)
907+
self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep)
908+
909+
def test_large_content_length_truncated(self):
910+
for w in range(18, 65):
911+
size = 1 << w
912+
headers = {'Content-Length' : str(size)}
913+
res = self.request('/cgi-bin/file1.py', 'POST', b'x', headers)
914+
self.assertEqual(res.read(), b'Hello World' + self.linesep)
915+
878916
def test_invaliduri(self):
879917
res = self.request('/cgi-bin/invalid')
880918
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)