티스토리 수익 글 보기

티스토리 수익 글 보기

[6.0.x] Fixed CVE-2026-5766 — Enforced DATA_UPLOAD_MAX_MEMORY_SIZE i… · django/django@ad8f9e1 · GitHub
Skip to content

Commit ad8f9e1

Browse files
jacobtylerwallssarahboyce
authored andcommitted
[6.0.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 990ab01 commit ad8f9e1

5 files changed

Lines changed: 115 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
@@ -203,9 +203,24 @@ def handle_raw_input(
203203
Use the content_length to signal whether or not this handler should be
204204
used.
205205
"""
206-
# Check the content-length header to see if we should
207206
# If the post is too large, we cannot use the Memory handler.
208-
self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
207+
# Content-Length can be absent or understated (for example
208+
# `Transfer-Encoding: chunked` on ASGI), so for seekable streams (such
209+
# as SpooledTemporaryFile on ASGI), check the actual size.
210+
211+
stream = getattr(input_data, "_stream", input_data)
212+
try:
213+
content_length = stream.seek(0, os.SEEK_END)
214+
except (UnsupportedOperation, AttributeError):
215+
# Cannot seek; fall back to the Content-Length parameter.
216+
# On WSGI the stream enforces this value so it is trustworthy.
217+
pass
218+
else:
219+
stream.seek(0)
220+
self.activated = (
221+
content_length is not None
222+
and content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
223+
)
209224

210225
def new_file(self, *args, **kwargs):
211226
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>`.

docs/releases/6.0.5.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ Django 6.0.5 release notes
77
Django 6.0.5 fixes three security issues with severity "low" and several bugs
88
in 6.0.4.
99

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>`.
23+
1024
Bugfixes
1125
========
1226

tests/asgi/tests.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
1414
from django.core.asgi import get_asgi_application
1515
from django.core.exceptions import RequestDataTooBig
16+
from django.core.files.uploadedfile import InMemoryUploadedFile
1617
from django.core.handlers.asgi import ASGIHandler, ASGIRequest
1718
from django.core.signals import request_finished, request_started
1819
from django.db import close_old_connections
@@ -804,8 +805,7 @@ def test_multiple_cookie_headers_http2(self):
804805
self.assertEqual(request.COOKIES, {"a": "abc", "b": "def", "c": "ghi"})
805806

806807

807-
class DataUploadMaxMemorySizeASGITests(SimpleTestCase):
808-
808+
class MaxMemorySizeASGITests(SimpleTestCase):
809809
def make_request(
810810
self,
811811
body,
@@ -923,6 +923,34 @@ def test_multipart_file_upload_not_limited_by_data_upload_max(self):
923923
self.addCleanup(uploaded.close)
924924
self.assertEqual(uploaded.read(), file_content)
925925

926+
def test_multipart_file_upload_limited_by_file_upload_max(self):
927+
boundary = "testboundary"
928+
file_content = b"x" * 100
929+
body = (
930+
(
931+
f"--{boundary}\r\n"
932+
f'Content-Disposition: form-data; name="file"; filename="test.txt"\r\n'
933+
f"Content-Type: application/octet-stream\r\n"
934+
f"\r\n"
935+
).encode()
936+
+ file_content
937+
+ f"\r\n--{boundary}--\r\n".encode()
938+
)
939+
# Provide an understated content-length.
940+
request = self.make_request(
941+
body,
942+
content_type=f"multipart/form-data; boundary={boundary}".encode(),
943+
content_length=9,
944+
)
945+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
946+
files = request.FILES
947+
self.assertEqual(len(files), 1)
948+
uploaded = files["file"]
949+
# The file is not loaded into memory.
950+
self.assertNotIsInstance(uploaded, InMemoryUploadedFile)
951+
self.addCleanup(uploaded.close)
952+
self.assertEqual(uploaded.read(), file_content)
953+
926954
async def test_read_body_buffers_all_chunks(self):
927955
# read_body() consumes all chunks regardless of
928956
# 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
@@ -1177,6 +1177,44 @@ def test_deepcopy(self):
11771177
self.assertEqual(request_copy.session, {})
11781178

11791179

1180+
class MemoryFileUploadHandlerTests(SimpleTestCase):
1181+
def test_handle_raw_input_wsgi_request_within_limit_activated(self):
1182+
1183+
class WSGIRequest:
1184+
def __init__(self, body):
1185+
self._stream = LimitedStream(BytesIO(body), len(body))
1186+
1187+
handler = MemoryFileUploadHandler()
1188+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1189+
handler.handle_raw_input(WSGIRequest(b"x" * 5), {}, 5, None)
1190+
self.assertIs(handler.activated, True)
1191+
1192+
def test_handle_raw_input_wsgi_request_exceeds_limit_deactivated(self):
1193+
1194+
class WSGIRequest:
1195+
def __init__(self, body):
1196+
self._stream = LimitedStream(BytesIO(body), len(body))
1197+
1198+
handler = MemoryFileUploadHandler()
1199+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1200+
handler.handle_raw_input(WSGIRequest(b"x" * 15), {}, 15, None)
1201+
self.assertIs(handler.activated, False)
1202+
1203+
def test_handle_raw_input_seekable_within_limit_activated(self):
1204+
handler = MemoryFileUploadHandler()
1205+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1206+
# content_length param is understated (0) but actual size is 10.
1207+
handler.handle_raw_input(BytesIO(b"x" * 10), {}, 0, None)
1208+
self.assertIs(handler.activated, True)
1209+
1210+
def test_handle_raw_input_seekable_exceeds_limit_deactivated(self):
1211+
handler = MemoryFileUploadHandler()
1212+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1213+
# content_length param is understated (0) but actual size is 15.
1214+
handler.handle_raw_input(BytesIO(b"x" * 15), {}, 0, None)
1215+
self.assertIs(handler.activated, False)
1216+
1217+
11801218
class HostValidationTests(SimpleTestCase):
11811219
poisoned_hosts = [
11821220
"example.com@evil.tld",

0 commit comments

Comments
 (0)