From a2200dc43d9fe0ee19b9185b30749c204a4dfd45 Mon Sep 17 00:00:00 2001
From: Sam Bull <git@sambull.org>
Date: Wed, 8 Nov 2023 19:25:05 +0000
Subject: [PATCH] Add HTTP method validation (#6533) (#7806)

(cherry picked from commit 75fca0b00b4297d0a30c51ae97a65428336eb2c1)

Upstream-Status: Backport
[https://github.com/aio-libs/aiohttp/pull/7806/commits/a43bc1779892e7014b7723c59d08fb37a000955e]

CVE: CVE-2023-49082

Co-authored-by: Andrew Svetlov <andrew.svetlov@gmail.com>
Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com>
---
 CHANGES/6533.feature         | 1 +
 aiohttp/client_reqrep.py     | 9 ++++++++-
 tests/test_client_request.py | 5 +++++
 tests/test_web_request.py    | 9 +++++++--
 4 files changed, 21 insertions(+), 3 deletions(-)
 create mode 100644 CHANGES/6533.feature

diff --git a/CHANGES/6533.feature b/CHANGES/6533.feature
new file mode 100644
index 0000000..36bcbeb
--- /dev/null
+++ b/CHANGES/6533.feature
@@ -0,0 +1 @@
+Add HTTP method validation.
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index d3cd77e..a8135b2 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -78,6 +78,7 @@ if TYPE_CHECKING:  # pragma: no cover
     from .tracing import Trace
 
 
+_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
 json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json")
 
 
@@ -266,10 +267,16 @@ class ClientRequest:
         proxy_headers: Optional[LooseHeaders] = None,
         traces: Optional[List["Trace"]] = None,
     ):
-
         if loop is None:
             loop = asyncio.get_event_loop()
 
+        match = _CONTAINS_CONTROL_CHAR_RE.search(method)
+        if match:
+            raise ValueError(
+                f"Method cannot contain non-token characters {method!r} "
+                "(found at least {match.group()!r})"
+            )
+
         assert isinstance(url, URL), url
         assert isinstance(proxy, (URL, type(None))), proxy
         # FIXME: session is None in tests only, need to fix tests
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
index 009f1a0..d0f208b 100644
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -89,6 +89,11 @@ def test_method3(make_request) -> None:
     assert req.method == "HEAD"
 
 
+def test_method_invalid(make_request) -> None:
+    with pytest.raises(ValueError, match="Method cannot contain non-token characters"):
+        make_request("METHOD WITH\nWHITESPACES", "http://python.org/")
+
+
 def test_version_1_0(make_request) -> None:
     req = make_request("get", "http://python.org/", version="1.0")
     assert req.version == (1, 0)
diff --git a/tests/test_web_request.py b/tests/test_web_request.py
index c6aeaf8..2bb0cd5 100644
--- a/tests/test_web_request.py
+++ b/tests/test_web_request.py
@@ -43,7 +43,10 @@ def test_base_ctor() -> None:
 
     assert "GET" == req.method
     assert HttpVersion(1, 1) == req.version
-    assert req.host == socket.getfqdn()
+    # MacOS may return CamelCased host name, need .lower()
+    # FQDN can be wider than host, e.g.
+    # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net'
+    assert req.host.lower() in socket.getfqdn().lower()
     assert "/path/to?a=1&b=2" == req.path_qs
     assert "/path/to" == req.path
     assert "a=1&b=2" == req.query_string
@@ -66,7 +69,9 @@ def test_ctor() -> None:
     assert "GET" == req.method
     assert HttpVersion(1, 1) == req.version
     # MacOS may return CamelCased host name, need .lower()
-    assert req.host.lower() == socket.getfqdn().lower()
+    # FQDN can be wider than host, e.g.
+    # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net'
+    assert req.host.lower() in socket.getfqdn().lower()
     assert "/path/to?a=1&b=2" == req.path_qs
     assert "/path/to" == req.path
     assert "a=1&b=2" == req.query_string
-- 
2.25.1

