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) 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()).