From 2ec27eda3ba6c14f0856e6e3eb1df07c41fd95e6 Mon Sep 17 00:00:00 2001
From: Jacob Walls <jacobtylerwalls@gmail.com>
Date: Tue, 24 Mar 2026 20:53:27 +0100
Subject: [PATCH] [5.2.x] 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>

Backport of 5a89e341bfc77dd67b7fd57b7091b6430558e1f4 from main.
---
 django/core/files/uploadhandler.py | 21 ++++++++++++++---
 docs/releases/5.2.14.txt           | 15 ++++++++++++
 tests/asgi/tests.py                | 32 +++++++++++++++++++++++--
 tests/requests_tests/tests.py      | 38 ++++++++++++++++++++++++++++++
 4 files changed, 101 insertions(+), 5 deletions(-)

Index: Django-4.2.11/django/core/files/uploadhandler.py
===================================================================
--- Django-4.2.11.orig/django/core/files/uploadhandler.py
+++ Django-4.2.11/django/core/files/uploadhandler.py
@@ -2,7 +2,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
@@ -201,9 +201,24 @@ class MemoryFileUploadHandler(FileUpload
         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)
Index: Django-4.2.11/tests/asgi/tests.py
===================================================================
--- Django-4.2.11.orig/tests/asgi/tests.py
+++ Django-4.2.11/tests/asgi/tests.py
@@ -9,6 +9,7 @@ from asgiref.testing import ApplicationC
 from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
 from django.core.asgi import ASGIHandler, get_asgi_application
 from django.core.exceptions import RequestDataTooBig
+from django.core.files.uploadedfile import InMemoryUploadedFile
 from django.core.handlers.asgi import ASGIRequest
 from django.core.signals import request_finished, request_started
 from django.db import close_old_connections
@@ -361,7 +362,7 @@ class ASGITest(SimpleTestCase):
         sync_waiter.active_threads.clear()
 
 
-class DataUploadMaxMemorySizeASGITests(SimpleTestCase):
+class MaxMemorySizeASGITests(SimpleTestCase):
     def make_request(
         self,
         body,
@@ -471,6 +472,34 @@ class DataUploadMaxMemorySizeASGITests(S
         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
Index: Django-4.2.11/tests/requests_tests/tests.py
===================================================================
--- Django-4.2.11.orig/tests/requests_tests/tests.py
+++ Django-4.2.11/tests/requests_tests/tests.py
@@ -5,6 +5,7 @@ from unittest import mock
 from urllib.parse import urlencode
 
 from django.core.exceptions import DisallowedHost
+from django.core.files.uploadhandler import MemoryFileUploadHandler
 from django.core.handlers.wsgi import LimitedStream, WSGIRequest
 from django.http import (
     HttpHeaders,
@@ -815,6 +816,44 @@ class RequestsTests(SimpleTestCase):
         self.assertEqual(request_copy.session, {})
 
 
+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",
