summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/http/multipartparser.py15
-rw-r--r--docs/releases/4.2.30.txt10
-rw-r--r--docs/releases/5.2.13.txt10
-rw-r--r--docs/releases/6.0.4.txt10
-rw-r--r--tests/requests_tests/tests.py66
5 files changed, 104 insertions, 7 deletions
diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py
index d420c255eb..e3cf3ecc3b 100644
--- a/django/http/multipartparser.py
+++ b/django/http/multipartparser.py
@@ -305,15 +305,18 @@ class MultiPartParser:
# We should always decode base64 chunks by
# multiple of 4, ignoring whitespace.
- stripped_chunk = b"".join(chunk.split())
+ stripped_parts = [b"".join(chunk.split())]
+ stripped_length = len(stripped_parts[0])
- remaining = len(stripped_chunk) % 4
- while remaining != 0:
- over_chunk = field_stream.read(4 - remaining)
+ while stripped_length % 4 != 0:
+ over_chunk = field_stream.read(self._chunk_size)
if not over_chunk:
break
- stripped_chunk += b"".join(over_chunk.split())
- remaining = len(stripped_chunk) % 4
+ over_stripped = b"".join(over_chunk.split())
+ stripped_parts.append(over_stripped)
+ stripped_length += len(over_stripped)
+
+ stripped_chunk = b"".join(stripped_parts)
try:
chunk = base64.b64decode(stripped_chunk)
diff --git a/docs/releases/4.2.30.txt b/docs/releases/4.2.30.txt
index de19a6f08f..c5058d9b84 100644
--- a/docs/releases/4.2.30.txt
+++ b/docs/releases/4.2.30.txt
@@ -46,3 +46,13 @@ instances to be created via forged ``POST`` data.
This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
+
+CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload
+===============================================================================================================
+
+When using ``django.http.multipartparser.MultiPartParser``, multipart uploads
+with ``Content-Transfer-Encoding: base64`` that include excessive whitespace
+may trigger repeated memory copying, potentially degrading performance.
+
+This issue has severity "moderate" according to the :ref:`Django security
+policy <security-disclosure>`.
diff --git a/docs/releases/5.2.13.txt b/docs/releases/5.2.13.txt
index 8b303f2700..46303da3c7 100644
--- a/docs/releases/5.2.13.txt
+++ b/docs/releases/5.2.13.txt
@@ -46,3 +46,13 @@ instances to be created via forged ``POST`` data.
This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
+
+CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload
+===============================================================================================================
+
+When using ``django.http.multipartparser.MultiPartParser``, multipart uploads
+with ``Content-Transfer-Encoding: base64`` that include excessive whitespace
+may trigger repeated memory copying, potentially degrading performance.
+
+This issue has severity "moderate" according to the :ref:`Django security
+policy <security-disclosure>`.
diff --git a/docs/releases/6.0.4.txt b/docs/releases/6.0.4.txt
index 4287a3086a..6452768f2a 100644
--- a/docs/releases/6.0.4.txt
+++ b/docs/releases/6.0.4.txt
@@ -47,6 +47,16 @@ instances to be created via forged ``POST`` data.
This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
+CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload
+===============================================================================================================
+
+When using ``django.http.multipartparser.MultiPartParser``, multipart uploads
+with ``Content-Transfer-Encoding: base64`` that include excessive whitespace
+may trigger repeated memory copying, potentially degrading performance.
+
+This issue has severity "moderate" according to the :ref:`Django security
+policy <security-disclosure>`.
+
Bugfixes
========
diff --git a/tests/requests_tests/tests.py b/tests/requests_tests/tests.py
index 36843df9b6..25157814cf 100644
--- a/tests/requests_tests/tests.py
+++ b/tests/requests_tests/tests.py
@@ -1,6 +1,7 @@
import copy
from io import BytesIO
from itertools import chain
+from unittest import mock
from urllib.parse import urlencode
from django.core.exceptions import BadRequest, DisallowedHost
@@ -13,7 +14,11 @@ from django.http import (
RawPostDataException,
UnreadablePostError,
)
-from django.http.multipartparser import MAX_TOTAL_HEADER_SIZE, MultiPartParserError
+from django.http.multipartparser import (
+ MAX_TOTAL_HEADER_SIZE,
+ LazyStream,
+ MultiPartParserError,
+)
from django.http.request import split_domain_port
from django.test import RequestFactory, SimpleTestCase, override_settings
from django.test.client import BOUNDARY, MULTIPART_CONTENT, FakePayload
@@ -906,6 +911,65 @@ class RequestsTests(SimpleTestCase):
request.body # evaluate
self.assertEqual(request.POST, {"name": ["123"]})
+ def test_multipart_file_upload_base64_whitespace_heavy(self):
+ # Fake a file upload with base64-encoded content including mostly
+ # whitespaces across chunk boundaries.
+ payload = FakePayload(
+ "\r\n".join(
+ [
+ f"--{BOUNDARY}",
+ 'Content-Disposition: form-data; name="file"; filename="test.txt"',
+ "Content-Type: application/octet-stream",
+ "Content-Transfer-Encoding: base64",
+ "",
+ ]
+ )
+ )
+ # "AAAA" decodes to b"\x00\x00\x00". Whitespace (70000 bytes) spans the
+ # default 64KB chunk boundary, hence the alignment loop is exercised.
+ payload.write(b"\r\n" + b"AAA" + b" " * 70000 + b"A" + b"\r\n")
+ payload.write("--" + BOUNDARY + "--\r\n")
+ request = WSGIRequest(
+ {
+ "REQUEST_METHOD": "POST",
+ "CONTENT_TYPE": MULTIPART_CONTENT,
+ "CONTENT_LENGTH": len(payload),
+ "wsgi.input": payload,
+ }
+ )
+ reads = []
+ original_read = LazyStream.read
+
+ def counting_read(self_stream, size=None):
+ reads.append(size)
+ return original_read(self_stream, size)
+
+ with mock.patch.object(LazyStream, "read", counting_read):
+ files = request.FILES
+
+ self.assertEqual(len(files), 1)
+ self.assertEqual(files["file"].read(), b"\x00\x00\x00")
+
+ # The alignment loop must read in `chunk-sized` units rather than one
+ # byte at a time, otherwise each whitespace byte triggers a separate
+ # read() call with a costly internal unget() cycle.
+ # Parsing this payload should issue exactly 8 LazyStream.read() calls:
+ # 1. main_stream.read(1) -- BoundaryIter.__init__ probe, preamble
+ # 2. sub_stream.read(1024) -- parse_boundary_stream, preamble headers
+ # 3. main_stream.read(1) -- BoundaryIter.__init__ probe, file field
+ # 4. field_stream.read(1024) -- parse_boundary_stream, file headers
+ # 5. field_stream.read(65536)-- base64 alignment loop: one chunk-sized
+ # read to find the non-whitespace bytes
+ # needed to complete the 4-byte base64
+ # group that spans the chunk boundary
+ # 6. main_stream.read(1) -- BoundaryIter.__init__ probe, epilogue
+ # 7. sub_stream.read(1024) -- parse_boundary_stream, epilogue headers
+ # 8. main_stream.read(1) -- BoundaryIter.__init__ probe, exhausted
+ # stream; returns b"" and stops iteration
+ # A byte-at-a-time implementation of read() in step 5 would do instead
+ # one read(1) per whitespace byte past the chunk boundary (4488 calls).
+ self.assertEqual(reads, [1, 1024, 1, 1024, 65536, 1, 1024, 1])
+
def test_POST_after_body_read_and_stream_read_multipart(self):
"""
POST should be populated even if body is read first, and then