From ba70e1bcdd8dc879c7d5fc7a3d12b5831eb08540 Mon Sep 17 00:00:00 2001 From: Lier0102 Date: Sun, 17 May 2026 20:15:39 +0900 Subject: [PATCH 1/2] Fixed #37103 -- Made HttpRequest.body handle malformed CONTENT_LENGTH. --- django/http/request.py | 8 +++++++- tests/asgi/tests.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/django/http/request.py b/django/http/request.py index 44bf09450bb4..28208d57a995 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -404,10 +404,16 @@ def body(self): "You cannot access body after reading from request's data stream" ) + # Make Content-Length fall back to 0 if malformed (e.g. ASGIRequest + # comma-joins duplicate Content-Length headers). + try: + content_length = int(self.META.get("CONTENT_LENGTH") or 0) + except (ValueError, TypeError): + content_length = 0 # Limit the maximum request data size that will be handled # in-memory. Reject early when Content-Length is present and # already exceeds the limit, avoiding reading the body at all. - self._check_data_too_big(int(self.META.get("CONTENT_LENGTH") or 0)) + self._check_data_too_big(content_length) # Content-Length can be absent or understated (e.g. # `Transfer-Encoding: chunked` on ASGI), so for seekable diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py index 1ff68920782e..727ce2fa6c91 100644 --- a/tests/asgi/tests.py +++ b/tests/asgi/tests.py @@ -804,6 +804,24 @@ def test_multiple_cookie_headers_http2(self): self.assertEqual(request.META["HTTP_COOKIE"], "a=abc; b=def; c=ghi") self.assertEqual(request.COOKIES, {"a": "abc", "b": "def", "c": "ghi"}) + def test_malformed_content_length(self): + body = b"hello world body" + cases = [ + {"content_length_headers": [b"10", b"20"], "CONTENT_LENGTH": "10,20"}, + {"content_length_headers": [b"abc"], "CONTENT_LENGTH": "abc"}, + ] + for case in cases: + with self.subTest(CONTENT_LENGTH=case["CONTENT_LENGTH"]): + scope = self.async_request_factory._base_scope(method="POST", path="/") + headers = [(b"content-type", b"text/plain")] + for content_length in case["content_length_headers"]: + headers.append((b"content-length", content_length)) + scope["headers"] = headers + request = ASGIRequest(scope, BytesIO(body)) + self.assertEqual(dict(request.POST), {}) + self.assertEqual(request.body, body) + self.assertEqual(request.META["CONTENT_LENGTH"], case["CONTENT_LENGTH"]) + class MaxMemorySizeASGITests(SimpleTestCase): def make_request( @@ -832,6 +850,22 @@ def test_body_size_exceeded_without_content_length(self): ): request.body + def test_body_size_exceeded_with_malformed_content_length(self): + body = b"x" * 10 + scope = AsyncRequestFactory()._base_scope(method="POST", path="/") + scope["headers"] = [ + (b"content-type", b"application/octet-stream"), + (b"content-length", b"10"), + (b"content-length", b"20"), + ] + request = ASGIRequest(scope, BytesIO(body)) + self.assertEqual(request.META["CONTENT_LENGTH"], "10,20") + with ( + self.settings(DATA_UPLOAD_MAX_MEMORY_SIZE=5), + self.assertRaisesMessage(RequestDataTooBig, TOO_MUCH_DATA_MSG), + ): + request.body + def test_body_size_check_fires_before_read(self): # The seekable size check rejects oversized bodies before reading # them into memory (i.e. before calling self.read()). From 48e0a64e6491b464249086d6052a00c8d2be418e Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 1 Jun 2026 11:46:08 -0400 Subject: [PATCH 2/2] Refs #36439 -- Added missing thread_sensitive=False to dummy password hasher path. The existing user path also uses thread_sensitive=False in acheck_password(). Follow-up to 7f66c3b41f0fb0fb938d7b96e20a28dccdaa2ecd. --- django/contrib/auth/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 723f348824fc..1d7199ce471c 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -402,6 +402,7 @@ def check_password_with_timing_attack_mitigation(user, password): async def acheck_password_with_timing_attack_mitigation(user, password): """See check_user_with_timing_attack_mitigation.""" if user is None: - await sync_to_async(get_user_model()().set_password)(password) + set_password = get_user_model()().set_password + await sync_to_async(set_password, thread_sensitive=False)(password) else: return await user.acheck_password(password)