Skip to content

Commit 2ec27ed

Browse files
jacobtylerwallssarahboyce
authored andcommitted
[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 5a89e34 from main.
1 parent ed18840 commit 2ec27ed

4 files changed

Lines changed: 101 additions & 5 deletions

File tree

django/core/files/uploadhandler.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
import os
6-
from io import BytesIO
6+
from io import BytesIO, UnsupportedOperation
77

88
from django.conf import settings
99
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
@@ -202,9 +202,24 @@ def handle_raw_input(
202202
Use the content_length to signal whether or not this handler should be
203203
used.
204204
"""
205-
# Check the content-length header to see if we should
206205
# If the post is too large, we cannot use the Memory handler.
207-
self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
206+
# Content-Length can be absent or understated (for example
207+
# `Transfer-Encoding: chunked` on ASGI), so for seekable streams (such
208+
# as SpooledTemporaryFile on ASGI), check the actual size.
209+
210+
stream = getattr(input_data, "_stream", input_data)
211+
try:
212+
content_length = stream.seek(0, os.SEEK_END)
213+
except (UnsupportedOperation, AttributeError):
214+
# Cannot seek; fall back to the Content-Length parameter.
215+
# On WSGI the stream enforces this value so it is trustworthy.
216+
pass
217+
else:
218+
stream.seek(0)
219+
self.activated = (
220+
content_length is not None
221+
and content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
222+
)
208223

209224
def new_file(self, *args, **kwargs):
210225
super().new_file(*args, **kwargs)

docs/releases/5.2.14.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,18 @@ Django 5.2.14 release notes
55
*May 5, 2026*
66

77
Django 5.2.14 fixes three security issues with severity "low" in 5.2.13.
8+
Django 5.2.14 fixes three security issue with severity "low" in 5.2.13.
9+
10+
CVE-2026-5766: Potential denial-of-service vulnerability in ASGI requests via file upload limit bypass
11+
======================================================================================================
12+
13+
ASGI requests with a missing or understated ``Content-Length`` header could
14+
bypass the :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE` limit, potentially loading
15+
large files into memory and causing service degradation.
16+
17+
As a reminder, Django :ref:`expects a limit to be configured
18+
<user-uploaded-content-security>` at the web server level rather than solely
19+
relying on :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`.
20+
21+
This issue has severity "low" according to the :ref:`Django security policy
22+
<security-disclosure>`.

tests/asgi/tests.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
1212
from django.core.asgi import get_asgi_application
1313
from django.core.exceptions import RequestDataTooBig
14+
from django.core.files.uploadedfile import InMemoryUploadedFile
1415
from django.core.handlers.asgi import ASGIHandler, ASGIRequest
1516
from django.core.signals import request_finished, request_started
1617
from django.db import close_old_connections
@@ -700,8 +701,7 @@ async def test_streaming_disconnect(self):
700701
await communicator.receive_output(timeout=0.2)
701702

702703

703-
class DataUploadMaxMemorySizeASGITests(SimpleTestCase):
704-
704+
class MaxMemorySizeASGITests(SimpleTestCase):
705705
def make_request(
706706
self,
707707
body,
@@ -819,6 +819,34 @@ def test_multipart_file_upload_not_limited_by_data_upload_max(self):
819819
self.addCleanup(uploaded.close)
820820
self.assertEqual(uploaded.read(), file_content)
821821

822+
def test_multipart_file_upload_limited_by_file_upload_max(self):
823+
boundary = "testboundary"
824+
file_content = b"x" * 100
825+
body = (
826+
(
827+
f"--{boundary}\r\n"
828+
f'Content-Disposition: form-data; name="file"; filename="test.txt"\r\n'
829+
f"Content-Type: application/octet-stream\r\n"
830+
f"\r\n"
831+
).encode()
832+
+ file_content
833+
+ f"\r\n--{boundary}--\r\n".encode()
834+
)
835+
# Provide an understated content-length.
836+
request = self.make_request(
837+
body,
838+
content_type=f"multipart/form-data; boundary={boundary}".encode(),
839+
content_length=9,
840+
)
841+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
842+
files = request.FILES
843+
self.assertEqual(len(files), 1)
844+
uploaded = files["file"]
845+
# The file is not loaded into memory.
846+
self.assertNotIsInstance(uploaded, InMemoryUploadedFile)
847+
self.addCleanup(uploaded.close)
848+
self.assertEqual(uploaded.read(), file_content)
849+
822850
async def test_read_body_buffers_all_chunks(self):
823851
# read_body() consumes all chunks regardless of
824852
# DATA_UPLOAD_MAX_MEMORY_SIZE; the limit is enforced later when

tests/requests_tests/tests.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,44 @@ def test_deepcopy(self):
11481148
self.assertEqual(request_copy.session, {})
11491149

11501150

1151+
class MemoryFileUploadHandlerTests(SimpleTestCase):
1152+
def test_handle_raw_input_wsgi_request_within_limit_activated(self):
1153+
1154+
class WSGIRequest:
1155+
def __init__(self, body):
1156+
self._stream = LimitedStream(BytesIO(body), len(body))
1157+
1158+
handler = MemoryFileUploadHandler()
1159+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1160+
handler.handle_raw_input(WSGIRequest(b"x" * 5), {}, 5, None)
1161+
self.assertIs(handler.activated, True)
1162+
1163+
def test_handle_raw_input_wsgi_request_exceeds_limit_deactivated(self):
1164+
1165+
class WSGIRequest:
1166+
def __init__(self, body):
1167+
self._stream = LimitedStream(BytesIO(body), len(body))
1168+
1169+
handler = MemoryFileUploadHandler()
1170+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1171+
handler.handle_raw_input(WSGIRequest(b"x" * 15), {}, 15, None)
1172+
self.assertIs(handler.activated, False)
1173+
1174+
def test_handle_raw_input_seekable_within_limit_activated(self):
1175+
handler = MemoryFileUploadHandler()
1176+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1177+
# content_length param is understated (0) but actual size is 10.
1178+
handler.handle_raw_input(BytesIO(b"x" * 10), {}, 0, None)
1179+
self.assertIs(handler.activated, True)
1180+
1181+
def test_handle_raw_input_seekable_exceeds_limit_deactivated(self):
1182+
handler = MemoryFileUploadHandler()
1183+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1184+
# content_length param is understated (0) but actual size is 15.
1185+
handler.handle_raw_input(BytesIO(b"x" * 15), {}, 0, None)
1186+
self.assertIs(handler.activated, False)
1187+
1188+
11511189
class HostValidationTests(SimpleTestCase):
11521190
poisoned_hosts = [
11531191
"example.com@evil.tld",

0 commit comments

Comments
 (0)