summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorNatalia <124304+nessita@users.noreply.github.com>2026-03-05 14:41:44 -0300
committerJacob Walls <jacobtylerwalls@gmail.com>2026-04-07 07:22:16 -0400
commit0910af60468216c856dfbcac1177372c225deb76 (patch)
treea536b8c35c18724366514bce5153d834e3f5a95b /tests
parent428c48f358c5a0ed5ca2834fb721d615eb2b0e11 (diff)
[6.0.x] Fixed CVE-2026-33033 -- Mitigated potential DoS in MultiPartParser.
When a multipart file part used `Content-Transfer-Encoding: base64` and the non-whitespace base64 bytes did not align to a multiple of 4 within a chunk, the parser entered a loop calling `field_stream.read(1-3)` once per whitespace byte. Each such call fetched the entire internal buffer, sliced off 1-3 bytes, and pushed the remainder back via unget(), doing an O(n) memory copy per call. A 2.5 MB payload of mostly whitespace produced CPU amplification relative to a normal upload of the same size. The alignment loop now reads `self._chunk_size` bytes at a time, and accumulates stripped parts in a list joined once at the end. Thanks to Seokchan Yoon for the report and the fixing patch. Backport of 7e9885f99cee771b51692fadc5592bdbf19641aa from main.
Diffstat (limited to 'tests')
-rw-r--r--tests/requests_tests/tests.py66
1 files changed, 65 insertions, 1 deletions
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