summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2026-03-24 15:53:27 -0400
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2026-05-05 14:33:27 +0200
commit5a89e341bfc77dd67b7fd57b7091b6430558e1f4 (patch)
treeeda11f2a0d23e74eb8ff6111a44ee2c0d19990fa
parentd75d57c2c06934a65feabd206129b9d1a230a1da (diff)
Fixed CVE-2026-5766 -- Enforced DATA_UPLOAD_MAX_MEMORY_SIZE in MemoryFileUploadHandler on ASGI.
In ASGI deployments, Content-Length is not guaranteed to reflect the actual request body size, so relying on it to gate memory allocation allowed the limit to be bypassed. The handler now enforces DATA_UPLOAD_MAX_MEMORY_SIZE regardless of the declared header value. Thanks to Kyle Agronick for the report. Refs #35289. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
-rw-r--r--django/core/files/uploadhandler.py21
-rw-r--r--docs/releases/5.2.14.txt14
-rw-r--r--docs/releases/6.0.5.txt14
-rw-r--r--tests/asgi/tests.py32
-rw-r--r--tests/requests_tests/tests.py38
5 files changed, 114 insertions, 5 deletions
diff --git a/django/core/files/uploadhandler.py b/django/core/files/uploadhandler.py
index 133c0a597f..e8347b4637 100644
--- a/django/core/files/uploadhandler.py
+++ b/django/core/files/uploadhandler.py
@@ -3,7 +3,7 @@ Base file upload handler classes, and the built-in concrete subclasses
"""
import os
-from io import BytesIO
+from io import BytesIO, UnsupportedOperation
from django.conf import settings
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
@@ -203,9 +203,24 @@ class MemoryFileUploadHandler(FileUploadHandler):
Use the content_length to signal whether or not this handler should be
used.
"""
- # Check the content-length header to see if we should
# If the post is too large, we cannot use the Memory handler.
- self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
+ # Content-Length can be absent or understated (for example
+ # `Transfer-Encoding: chunked` on ASGI), so for seekable streams (such
+ # as SpooledTemporaryFile on ASGI), check the actual size.
+
+ stream = getattr(input_data, "_stream", input_data)
+ try:
+ content_length = stream.seek(0, os.SEEK_END)
+ except (UnsupportedOperation, AttributeError):
+ # Cannot seek; fall back to the Content-Length parameter.
+ # On WSGI the stream enforces this value so it is trustworthy.
+ pass
+ else:
+ stream.seek(0)
+ self.activated = (
+ content_length is not None
+ and content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
+ )
def new_file(self, *args, **kwargs):
super().new_file(*args, **kwargs)
diff --git a/docs/releases/5.2.14.txt b/docs/releases/5.2.14.txt
index 681687efb1..0722e80a95 100644
--- a/docs/releases/5.2.14.txt
+++ b/docs/releases/5.2.14.txt
@@ -5,3 +5,17 @@ Django 5.2.14 release notes
*May 5, 2026*
Django 5.2.14 fixes three security issues with severity "low" in 5.2.13.
+
+CVE-2026-5766: Potential denial-of-service vulnerability in ASGI requests via file upload limit bypass
+======================================================================================================
+
+ASGI requests with a missing or understated ``Content-Length`` header could
+bypass the :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE` limit, potentially loading
+large files into memory and causing service degradation.
+
+As a reminder, Django :ref:`expects a limit to be configured
+<user-uploaded-content-security>` at the web server level rather than solely
+relying on :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`.
+
+This issue has severity "low" according to the :ref:`Django security policy
+<security-disclosure>`.
diff --git a/docs/releases/6.0.5.txt b/docs/releases/6.0.5.txt
index 1e7e1b2c3b..8613b604fb 100644
--- a/docs/releases/6.0.5.txt
+++ b/docs/releases/6.0.5.txt
@@ -7,6 +7,20 @@ Django 6.0.5 release notes
Django 6.0.5 fixes three security issues with severity "low" and several bugs
in 6.0.4.
+CVE-2026-5766: Potential denial-of-service vulnerability in ASGI requests via file upload limit bypass
+======================================================================================================
+
+ASGI requests with a missing or understated ``Content-Length`` header could
+bypass the :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE` limit, potentially loading
+large files into memory and causing service degradation.
+
+As a reminder, Django :ref:`expects a limit to be configured
+<user-uploaded-content-security>` at the web server level rather than solely
+relying on :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`.
+
+This issue has severity "low" according to the :ref:`Django security policy
+<security-disclosure>`.
+
Bugfixes
========
diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py
index f77bd997a4..1ff6892078 100644
--- a/tests/asgi/tests.py
+++ b/tests/asgi/tests.py
@@ -13,6 +13,7 @@ from asgiref.testing import ApplicationCommunicator
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
from django.core.asgi import get_asgi_application
from django.core.exceptions import RequestDataTooBig
+from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.handlers.asgi import ASGIHandler, ASGIRequest
from django.core.signals import request_finished, request_started
from django.db import close_old_connections
@@ -804,8 +805,7 @@ class ASGITest(SimpleTestCase):
self.assertEqual(request.COOKIES, {"a": "abc", "b": "def", "c": "ghi"})
-class DataUploadMaxMemorySizeASGITests(SimpleTestCase):
-
+class MaxMemorySizeASGITests(SimpleTestCase):
def make_request(
self,
body,
@@ -923,6 +923,34 @@ class DataUploadMaxMemorySizeASGITests(SimpleTestCase):
self.addCleanup(uploaded.close)
self.assertEqual(uploaded.read(), file_content)
+ def test_multipart_file_upload_limited_by_file_upload_max(self):
+ boundary = "testboundary"
+ file_content = b"x" * 100
+ body = (
+ (
+ f"--{boundary}\r\n"
+ f'Content-Disposition: form-data; name="file"; filename="test.txt"\r\n'
+ f"Content-Type: application/octet-stream\r\n"
+ f"\r\n"
+ ).encode()
+ + file_content
+ + f"\r\n--{boundary}--\r\n".encode()
+ )
+ # Provide an understated content-length.
+ request = self.make_request(
+ body,
+ content_type=f"multipart/form-data; boundary={boundary}".encode(),
+ content_length=9,
+ )
+ with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
+ files = request.FILES
+ self.assertEqual(len(files), 1)
+ uploaded = files["file"]
+ # The file is not loaded into memory.
+ self.assertNotIsInstance(uploaded, InMemoryUploadedFile)
+ self.addCleanup(uploaded.close)
+ self.assertEqual(uploaded.read(), file_content)
+
async def test_read_body_buffers_all_chunks(self):
# read_body() consumes all chunks regardless of
# DATA_UPLOAD_MAX_MEMORY_SIZE; the limit is enforced later when
diff --git a/tests/requests_tests/tests.py b/tests/requests_tests/tests.py
index c25dd913cf..cf24ddd326 100644
--- a/tests/requests_tests/tests.py
+++ b/tests/requests_tests/tests.py
@@ -1265,6 +1265,44 @@ class RequestsTests(SimpleTestCase):
request.multipart_parser_class = MultiPartParser
+class MemoryFileUploadHandlerTests(SimpleTestCase):
+ def test_handle_raw_input_wsgi_request_within_limit_activated(self):
+
+ class WSGIRequest:
+ def __init__(self, body):
+ self._stream = LimitedStream(BytesIO(body), len(body))
+
+ handler = MemoryFileUploadHandler()
+ with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
+ handler.handle_raw_input(WSGIRequest(b"x" * 5), {}, 5, None)
+ self.assertIs(handler.activated, True)
+
+ def test_handle_raw_input_wsgi_request_exceeds_limit_deactivated(self):
+
+ class WSGIRequest:
+ def __init__(self, body):
+ self._stream = LimitedStream(BytesIO(body), len(body))
+
+ handler = MemoryFileUploadHandler()
+ with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
+ handler.handle_raw_input(WSGIRequest(b"x" * 15), {}, 15, None)
+ self.assertIs(handler.activated, False)
+
+ def test_handle_raw_input_seekable_within_limit_activated(self):
+ handler = MemoryFileUploadHandler()
+ with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
+ # content_length param is understated (0) but actual size is 10.
+ handler.handle_raw_input(BytesIO(b"x" * 10), {}, 0, None)
+ self.assertIs(handler.activated, True)
+
+ def test_handle_raw_input_seekable_exceeds_limit_deactivated(self):
+ handler = MemoryFileUploadHandler()
+ with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
+ # content_length param is understated (0) but actual size is 15.
+ handler.handle_raw_input(BytesIO(b"x" * 15), {}, 0, None)
+ self.assertIs(handler.activated, False)
+
+
class HostValidationTests(SimpleTestCase):
poisoned_hosts = [
"example.com@evil.tld",