CVE-2023-47641: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling') (CWE-444)
value: aiohttp is an asynchronous HTTP client/server framework for asyncio and Python. Affected versions of aiohttp have a security vulnerability regarding the inconsistent interpretation of the http protocol. HTTP/1.1 is a persistent protocol, if both Content-Length(CL) and Transfer-Encoding(TE) header values are present it can lead to incorrect interpretation of two entities that parse the HTTP and we can poison other sockets with this incorrect interpretation. A possible Proof-of-Concept (POC) would be a configuration with a reverse proxy(frontend) that accepts both CL and TE headers and aiohttp as backend. As aiohttp parses anything with chunked, we can pass a chunked123 as TE, the frontend entity will ignore this header and will parse Content-Length. The impact of this vulnerability is that it is possible to bypass any proxy rule, poisoning sockets to other users like passing Authentication Headers, also if it is present an Open Redirect an attacker could combine it to redirect random users to another website and log the request. This vulnerability has been addressed in release 3.8.0 of aiohttp. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product acts as an intermediary HTTP agent
         (such as a proxy or firewall) in the data flow between two
         entities such as a client and server, but it does not
         interpret malformed HTTP requests or responses in ways that
         are consistent with how the messages will be processed by
         those entities that are at the ultimate destination.
CVSS v3 base score: 6.5
Published: 2023-11-14T21:15Z
Repository: https://github.com/aio-libs/aiohttp
Language: Python
Files changed: CHANGES/6182.bugfix, aiohttp/http_parser.py, tests/test_http_parser.py
Commit date: 2021-10-31 19:04:36 +0200
Commit message: Raise '400: Content-Length can't be present with Transfer-Encoding' if both Content-Length and Transfer-Encoding are sent by peer (#6182)

Diff: diff --git a/CHANGES/6182.bugfix b/CHANGES/6182.bugfix
new file mode 100644
index 000000000..28daaa328
--- /dev/null
+++ b/CHANGES/6182.bugfix
@@ -0,0 +1 @@
+Raise ``400: Content-Length can't be present with Transfer-Encoding`` if both ``Content-Length`` and ``Transfer-Encoding`` are sent by peer by both C and Python implementations
diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
index 4a4ae31ae..e1b86e8e4 100644
--- a/aiohttp/http_parser.py
+++ b/aiohttp/http_parser.py
@@ -26,10 +26,11 @@ from yarl import URL
 
 from . import hdrs
 from .base_protocol import BaseProtocol
 from .helpers import NO_EXTENSIONS, BaseTimerContext
 from .http_exceptions import (
+    BadHttpMessage,
     BadStatusLine,
     ContentEncodingError,
     ContentLengthError,
     InvalidHeader,
     LineTooLong,
@@ -487,12 +488,19 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
             if enc in ("gzip", "deflate", "br"):
                 encoding = enc
 
         # chunking
         te = headers.get(hdrs.TRANSFER_ENCODING)
-        if te and "chunked" in te.lower():
-            chunked = True
+        if te is not None:
+            te_lower = te.lower()
+            if "chunked" in te_lower:
+                chunked = True
+
+            if hdrs.CONTENT_LENGTH in headers:
+                raise BadHttpMessage(
+                    "Content-Length can't be present with Transfer-Encoding",
+                )
 
         return (headers, raw_headers, close_conn, encoding, upgrade, chunked)
 
     def set_upgraded(self, val: bool) -> None:
         """Set connection upgraded (to websocket) mode.
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 78e9ea640..d86d238f5 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -289,11 +289,24 @@ def test_request_chunked(parser) -> None:
     assert msg.chunked
     assert not upgrade
     assert isinstance(payload, streams.StreamReader)
 
 
-def test_conn_upgrade(parser) -> None:
+def test_request_te_chunked_with_content_length(parser: Any) -> None:
+    text = (
+        b"GET /test HTTP/1.1\r\n"
+        b"content-length: 1234\r\n"
+        b"transfer-encoding: chunked\r\n\r\n"
+    )
+    with pytest.raises(
+        http_exceptions.BadHttpMessage,
+        match="Content-Length can't be present with Transfer-Encoding",
+    ):
+        parser.feed_data(text)
+
+
+def test_conn_upgrade(parser: Any) -> None:
     text = (
         b"GET /test HTTP/1.1\r\n"
         b"connection: upgrade\r\n"
         b"upgrade: websocket\r\n\r\n"
     )

Vulnerable code:         if te and "chunked" in te.lower():
            chunked = True
def test_conn_upgrade(parser) -> None:
Fixed code: Raise ``400: Content-Length can't be present with Transfer-Encoding`` if both ``Content-Length`` and ``Transfer-Encoding`` are sent by peer by both C and Python implementations
    BadHttpMessage,
        if te is not None:
            te_lower = te.lower()
            if "chunked" in te_lower:
                chunked = True

            if hdrs.CONTENT_LENGTH in headers:
                raise BadHttpMessage(
                    "Content-Length can't be present with Transfer-Encoding",
                )
def test_request_te_chunked_with_content_length(parser: Any) -> None:
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"content-length: 1234\r\n"
        b"transfer-encoding: chunked\r\n\r\n"
    )
    with pytest.raises(
        http_exceptions.BadHttpMessage,
        match="Content-Length can't be present with Transfer-Encoding",
    ):
        parser.feed_data(text)


def test_conn_upgrade(parser: Any) -> None: