-
-
Notifications
You must be signed in to change notification settings - Fork 33.6k
[3.14] gh-119452: Fix a potential virtual memory allocation denial of service in http.server #119455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
I've marked this Draft for now as discussion on this on the security response team list is not complete. (we'll summarize that in a public issue once it has settled) |
|
See #119514 (comment) for results of the PSRT discussion. |
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 momory consumption is proportional to the amont of sent data.
8d16f15 to
64620ea
Compare
| while (len(data) < nbytes and len(data) != cursize and | ||
| select.select([self.rfile._sock], [], [], 0)[0]): | ||
| cursize = len(data) | ||
| delta = min(cursize, nbytes - cursize) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a comment that this is a geometric increase in read size (never more than doubling our current the length of data per loop iteration) could be good. otherwise it takes some staring at the code to understand what it is doing with the delta.
not a big deal though given CGIHTTPRequestHandler is gone in 3.15 onwards. :)
|
Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.10, 3.11, 3.12, 3.13. |
…ial 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>
…ial 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>
|
GH-142130 is a backport of this pull request to the 3.13 branch. |
…ial 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>
|
GH-142131 is a backport of this pull request to the 3.12 branch. |
…ial 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>
|
GH-142132 is a backport of this pull request to the 3.11 branch. |
|
GH-142133 is a backport of this pull request to the 3.10 branch. |
… service in http.server (GH-119455) (GH-142130) 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>
|
|
The 3.14 buildbot failure looks related to this change, but I don't see it reported on this PR: https://buildbot.python.org/#/builders/1684/builds/326 ======================================================================
FAIL: test_large_content_length (test.test_httpservers.CGIHTTPServerTestCase.test_large_content_length)
----------------------------------------------------------------------
Traceback (most recent call last):
File "b:\uildarea\3.14.ware-win11.nondebug\build\Lib\test\test_httpservers.py", line 1132, in test_large_content_length
self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: b'4194304 2226830\r\n' != b'4194304 4194304\r\n'
----------------------------------------------------------------------
Ran 1 test in 0.336s |
| cursize = 0 | ||
| data = self.rfile.read(min(nbytes, _MIN_READ_BUF_SIZE)) | ||
| while (len(data) < nbytes and len(data) != cursize and | ||
| select.select([self.rfile._sock], [], [], 0)[0]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the timeout=0 fourth arg here makes this non-blocking. the loop won't wait for further data to arrive. so we wind up not processing all of the data.
(Claude Code Opus 4.5 looking at the buildbot failure, issue, pr & branch triaged this FWIW)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why select at all here given we just want to block on reading another chunk? if the socket closes or errors, that'd return anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick PR to make it blocking: #142176
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should this do if the data doesn't arrive?
Wait forever, as in did before the fix?
Or doses this PR also meant fix the DoS? (I don't see how, is there a timeout setting or an implicit one?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was so long ago that I don't remember my train of thought, but playing around with the code I think I've recreated it.
I tested this code on Linux with have_fork = False. The tests failed because the underlying file object is unbuffered and read(n) can return less than n bytes. So the loop differs from loops in other fixes -- it continues until it reads 0 bytes. select() is needed to avoid blocking. Until now all worked on Linux and Windows on my computer and in CI tests. Perhaps this depends on the system load or on non-debug build (I only tested with the debug build).
Perhaps we need to use non-zero timeout (self.timeout?) and set it to non-None for tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
xmlrpc/server.py has a similar code path
Lines 483 to 493 in d3c888b
| max_chunk_size = 10*1024*1024 | |
| size_remaining = int(self.headers["content-length"]) | |
| L = [] | |
| while size_remaining: | |
| chunk_size = min(size_remaining, max_chunk_size) | |
| chunk = self.rfile.read(chunk_size) | |
| if not chunk: | |
| break | |
| L.append(chunk) | |
| size_remaining -= len(L[-1]) | |
| data = b''.join(L) |
but without a select. The last change there was ec1712a in 2012 adding the break if no data was read. So without a timeout it will suffer the same problem if content-length lies and less data is sent?
OTOH, does anybody out there operate a server without a timeout?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait forever, as in did before the fix?
yes. this is intentionally a blocking read until sufficient data arrives or the connection dies. :)
fwiw, i missed this myself in the first code review. cursed unnamed parameters and presumptions about what 0 means. 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we do not need to add timeout logic here. just remove the select. this path was always intended to block until it got enough data or the socket is closed. the issue being protected against has nothing to do with blocking by holding a connection open, it's just about memory allocation.
This 1990s era server code was never meant to provide a robust server. There's a reason we got rid of it in 3.15.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need timeout logic here to be able to test this change.
…tion denial of service in http.server (pythonGH-119455)" This reverts commit 29c657a.
|
GH-142184 is a backport of this pull request to the 3.14 branch. |
…tion denial of service in http.server (pythonGH-119455) (pythonGH-142130)" This reverts commit 6c922bb.
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.