CVE-2023-49081: Other (NVD-CWE-Other)
value: aiohttp is an asynchronous HTTP client/server framework for asyncio and Python. Improper validation made it possible for an attacker to modify the HTTP request (e.g. to insert a new header) or create a new HTTP request if the attacker controls the HTTP version. The vulnerability only occurs if the attacker can control the HTTP version of the request. This issue has been patched in version 3.9.0.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 5.3
Published: 2023-11-30T07:15Z
Repository: https://github.com/aio-libs/aiohttp
Language: Python
Files changed: CHANGES/7835.bugfix, aiohttp/client_reqrep.py, tests/test_client_request.py
Commit date: 2023-11-13 22:13:06 +0000
Commit message: Disallow arbitrary sequence types in version (#7835)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Diff: diff --git a/CHANGES/7835.bugfix b/CHANGES/7835.bugfix
new file mode 100644
index 000000000..4ce3af4f6
--- /dev/null
+++ b/CHANGES/7835.bugfix
@@ -0,0 +1 @@
+Fixed arbitrary sequence types being allowed to inject headers via version parameter -- by :user:`Dreamsorcerer`
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index ad480a86c..f05a08637 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -642,12 +642,12 @@ class ClientRequest:
 
         if connection is not None:
             self.headers[hdrs.CONNECTION] = connection
 
         # status + headers
-        status_line = "{0} {1} HTTP/{2[0]}.{2[1]}".format(
-            self.method, path, self.version
+        status_line = "{0} {1} HTTP/{v.major}.{v.minor}".format(
+            self.method, path, v=self.version
         )
         await writer.write_headers(status_line, self.headers)
 
         self._writer = self.loop.create_task(self.write_bytes(writer, conn))
 
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
index c2853861a..d7053efbd 100644
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -18,10 +18,11 @@ from aiohttp.client_reqrep import (
     ClientRequest,
     ClientResponse,
     Fingerprint,
     _gen_default_accept_encoding,
 )
+from aiohttp.http import HttpVersion
 from aiohttp.test_utils import make_mocked_coro
 
 
 class WriterMock(mock.AsyncMock):
     def __await__(self) -> None:
@@ -588,22 +589,22 @@ async def test_connection_header(loop: Any, conn: Any) -> None:
     req = ClientRequest("get", URL("http://python.org"), loop=loop)
     req.keep_alive = mock.Mock()
     req.headers.clear()
 
     req.keep_alive.return_value = True
-    req.version = (1, 1)
+    req.version = HttpVersion(1, 1)
     req.headers.clear()
     await req.send(conn)
     assert req.headers.get("CONNECTION") is None
 
-    req.version = (1, 0)
+    req.version = HttpVersion(1, 0)
     req.headers.clear()
     await req.send(conn)
     assert req.headers.get("CONNECTION") == "keep-alive"
 
     req.keep_alive.return_value = False
-    req.version = (1, 1)
+    req.version = HttpVersion(1, 1)
     req.headers.clear()
     await req.send(conn)
     assert req.headers.get("CONNECTION") == "close"
 
 
@@ -1110,10 +1111,23 @@ async def test_close(loop: Any, buf: Any, conn: Any) -> None:
     assert buf.split(b"\r\n\r\n", 1)[1] == b"6\r\nresult\r\n0\r\n\r\n"
     await req.close()
     resp.close()
 
 
+async def test_bad_version(loop: Any, conn: Any) -> None:
+    req = ClientRequest(
+        "GET",
+        URL("http://python.org"),
+        loop=loop,
+        headers={"Connection": "Close"},
+        version=("1", "1\r\nInjected-Header: not allowed"),
+    )
+
+    with pytest.raises(AttributeError):
+        await req.send(conn)
+
+
 async def test_custom_response_class(loop: Any, conn: Any) -> None:
     class CustomResponse(ClientResponse):
         def read(self, decode=False):
             return "customized!"
 

Vulnerable code:         status_line = "{0} {1} HTTP/{2[0]}.{2[1]}".format(
            self.method, path, self.version
    req.version = (1, 1)
    req.version = (1, 0)
    req.version = (1, 1)
Fixed code: Fixed arbitrary sequence types being allowed to inject headers via version parameter -- by :user:`Dreamsorcerer`
        status_line = "{0} {1} HTTP/{v.major}.{v.minor}".format(
            self.method, path, v=self.version
from aiohttp.http import HttpVersion
    req.version = HttpVersion(1, 1)
    req.version = HttpVersion(1, 0)
    req.version = HttpVersion(1, 1)
async def test_bad_version(loop: Any, conn: Any) -> None:
    req = ClientRequest(
        "GET",
        URL("http://python.org"),
        loop=loop,
        headers={"Connection": "Close"},
        version=("1", "1\r\nInjected-Header: not allowed"),
    )

    with pytest.raises(AttributeError):
        await req.send(conn)