티스토리 수익 글 보기

티스토리 수익 글 보기

Fixed CVE-2026-5766 — Enforced DATA_UPLOAD_MAX_MEMORY_SIZE in Memory… · django/django@5a89e34 · GitHub
Skip to content

Commit 5a89e34

Browse files
jacobtylerwallsnessita
authored andcommitted
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>
1 parent d75d57c commit 5a89e34

5 files changed

Lines changed: 114 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,17 @@ 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+
9+
CVE-2026-5766: Potential denial-of-service vulnerability in ASGI requests via file upload limit bypass
10+
======================================================================================================
11+
12+
ASGI requests with a missing or understated ``Content-Length`` header could
13+
bypass the :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE` limit, potentially loading
14+
large files into memory and causing service degradation.
15+
16+
As a reminder, Django :ref:`expects a limit to be configured
17+
<user-uploaded-content-security>` at the web server level rather than solely
18+
relying on :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`.
19+
20+
This issue has severity "low" according to the :ref:`Django security policy
21+
<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
@@ -1265,6 +1265,44 @@ def test_multipart_parser_class_immutable_after_parse(self):
12651265
request.multipart_parser_class = MultiPartParser
12661266

12671267

1268+
class MemoryFileUploadHandlerTests(SimpleTestCase):
1269+
def test_handle_raw_input_wsgi_request_within_limit_activated(self):
1270+
1271+
class WSGIRequest:
1272+
def __init__(self, body):
1273+
self._stream = LimitedStream(BytesIO(body), len(body))
1274+
1275+
handler = MemoryFileUploadHandler()
1276+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1277+
handler.handle_raw_input(WSGIRequest(b"x" * 5), {}, 5, None)
1278+
self.assertIs(handler.activated, True)
1279+
1280+
def test_handle_raw_input_wsgi_request_exceeds_limit_deactivated(self):
1281+
1282+
class WSGIRequest:
1283+
def __init__(self, body):
1284+
self._stream = LimitedStream(BytesIO(body), len(body))
1285+
1286+
handler = MemoryFileUploadHandler()
1287+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1288+
handler.handle_raw_input(WSGIRequest(b"x" * 15), {}, 15, None)
1289+
self.assertIs(handler.activated, False)
1290+
1291+
def test_handle_raw_input_seekable_within_limit_activated(self):
1292+
handler = MemoryFileUploadHandler()
1293+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1294+
# content_length param is understated (0) but actual size is 10.
1295+
handler.handle_raw_input(BytesIO(b"x" * 10), {}, 0, None)
1296+
self.assertIs(handler.activated, True)
1297+
1298+
def test_handle_raw_input_seekable_exceeds_limit_deactivated(self):
1299+
handler = MemoryFileUploadHandler()
1300+
with self.settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10):
1301+
# content_length param is understated (0) but actual size is 15.
1302+
handler.handle_raw_input(BytesIO(b"x" * 15), {}, 0, None)
1303+
self.assertIs(handler.activated, False)
1304+
1305+
12681306
class HostValidationTests(SimpleTestCase):
12691307
poisoned_hosts = [
12701308
"example.com@evil.tld",

0 commit comments

Comments
 (0)