CVE-2024-30251: Insufficient Information (NVD-CWE-noinfo)
value: aiohttp is an asynchronous HTTP client/server framework for asyncio and Python. In affected versions an attacker can send a specially crafted POST (multipart/form-data) request. When the aiohttp server processes it, the server will enter an infinite loop and be unable to process any further requests. An attacker can stop the application from serving requests after sending a single request. This issue has been addressed in version 3.9.4. Users are advised to upgrade. Users unable to upgrade may manually apply a patch to their systems. Please see the linked GHSA for instructions.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-02T14:15Z
Repository: https://github.com/aio-libs/aiohttp
Language: Python
Files changed: CHANGES/8335.bugfix.rst, aiohttp/multipart.py, tests/test_multipart.py
Commit date: 2024-04-15 21:54:12 +0100
Commit message: [PR #8335/5a6949da backport][3.9] Add Content-Disposition automatically (#8336)

**This is a backport of PR #8335 as merged into master
(5a6949da642d1db6cf414fd0d1f70e54c7b7be14).**

Co-authored-by: Sam Bull <git@sambull.org>
Diff: diff --git a/CHANGES/8335.bugfix.rst b/CHANGES/8335.bugfix.rst
new file mode 100644
index 0000000000..cd93b864a5
--- /dev/null
+++ b/CHANGES/8335.bugfix.rst
@@ -0,0 +1 @@
+Added default Content-Disposition in multipart/form-data responses -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
index fcdf16183c..71fc2654a1 100644
--- a/aiohttp/multipart.py
+++ b/aiohttp/multipart.py
@@ -850,10 +850,14 @@ class MultipartWriter(Payload):
             # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
             assert (
                 not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING}
                 & payload.headers.keys()
             )
+            # Set default Content-Disposition in case user doesn't create one
+            if CONTENT_DISPOSITION not in payload.headers:
+                name = f"section-{len(self._parts)}"
+                payload.set_content_disposition("form-data", name=name)
         else:
             # compression
             encoding = payload.headers.get(CONTENT_ENCODING, "").lower()
             if encoding and encoding not in ("deflate", "gzip", "identity"):
                 raise RuntimeError(f"unknown content encoding: {encoding}")
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
index 37ac54797f..436b70957f 100644
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -1280,16 +1280,28 @@ class TestMultipartWriter:
         writer.append(subwriter, {CONTENT_TYPE: "test/passed"})
         assert 1 == len(writer)
         part = writer._parts[0][0]
         assert part.headers[CONTENT_TYPE] == "test/passed"
 
-    async def test_set_content_disposition_after_append(self):
+    def test_set_content_disposition_after_append(self):
         writer = aiohttp.MultipartWriter("form-data")
-        payload = writer.append("some-data")
-        payload.set_content_disposition("form-data", name="method")
-        assert CONTENT_DISPOSITION in payload.headers
-        assert "name=" in payload.headers[CONTENT_DISPOSITION]
+        part = writer.append("some-data")
+        part.set_content_disposition("form-data", name="method")
+        assert 'name="method"' in part.headers[CONTENT_DISPOSITION]
+
+    def test_automatic_content_disposition(self):
+        writer = aiohttp.MultipartWriter("form-data")
+        writer.append_json(())
+        part = payload.StringPayload("foo")
+        part.set_content_disposition("form-data", name="second")
+        writer.append_payload(part)
+        writer.append("foo")
+
+        disps = tuple(p[0].headers[CONTENT_DISPOSITION] for p in writer._parts)
+        assert 'name="section-0"' in disps[0]
+        assert 'name="second"' in disps[1]
+        assert 'name="section-2"' in disps[2]
 
     def test_with(self) -> None:
         with aiohttp.MultipartWriter(boundary=":") as writer:
             writer.append("foo")
             writer.append(b"bar")

Vulnerable code:     async def test_set_content_disposition_after_append(self):
        payload = writer.append("some-data")
        payload.set_content_disposition("form-data", name="method")
        assert CONTENT_DISPOSITION in payload.headers
        assert "name=" in payload.headers[CONTENT_DISPOSITION]
Fixed code: Added default Content-Disposition in multipart/form-data responses -- by :user:`Dreamsorcerer`.
            # Set default Content-Disposition in case user doesn't create one
            if CONTENT_DISPOSITION not in payload.headers:
                name = f"section-{len(self._parts)}"
                payload.set_content_disposition("form-data", name=name)
    def test_set_content_disposition_after_append(self):
        part = writer.append("some-data")
        part.set_content_disposition("form-data", name="method")
        assert 'name="method"' in part.headers[CONTENT_DISPOSITION]

    def test_automatic_content_disposition(self):
        writer = aiohttp.MultipartWriter("form-data")
        writer.append_json(())
        part = payload.StringPayload("foo")
        part.set_content_disposition("form-data", name="second")
        writer.append_payload(part)
        writer.append("foo")

        disps = tuple(p[0].headers[CONTENT_DISPOSITION] for p in writer._parts)
        assert 'name="section-0"' in disps[0]
        assert 'name="second"' in disps[1]
        assert 'name="section-2"' in disps[2]