From 87daca6e11c2123c04d737bde7ddef35a154b272 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Jun 2026 00:30:30 -0500 Subject: [PATCH] [PR #12835/1e94b3e8 backport][3.14] Tls server hostname pool key (#12847) CVE: CVE-2026-54275 Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/0ca2b6c28a25726527a8b60f25960262a91ed0e0] Backport Changes: - Upstream uses a NamedTuple ConnectionKey with PEP 604 union annotations; aiohttp 3.9.5 uses an attr.s class, so server_hostname is added with the equivalent Optional[str] annotation and included in its constructor. - aiohttp 3.9.5 does not define the newer AiohttpServer or _RequestMaker test aliases, so their fixture annotations are omitted. The request-key test is synchronous because this branch's make_request fixture returns ClientRequest directly. (cherry picked from commit 0ca2b6c28a25726527a8b60f25960262a91ed0e0) Signed-off-by: Darsh Kelaiya --- CHANGES/12835.bugfix.rst | 1 + aiohttp/client_reqrep.py | 2 ++ tests/test_client_functional.py | 29 +++++++++++++++++++++++++++++ tests/test_client_request.py | 14 ++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 CHANGES/12835.bugfix.rst diff --git a/CHANGES/12835.bugfix.rst b/CHANGES/12835.bugfix.rst new file mode 100644 index 000000000..84a8ae006 --- /dev/null +++ b/CHANGES/12835.bugfix.rst @@ -0,0 +1 @@ +Included the per-request ``server_hostname`` override in the :class:`~aiohttp.TCPConnector` connection pool key, so a pooled TLS connection is no longer reused for a request that sets ``server_hostname`` to a different value -- by :user:`bdraco`. diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index afe719da1..9abfc79de 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -220,6 +220,7 @@ class ConnectionKey: proxy: Optional[URL] proxy_auth: Optional[BasicAuth] proxy_headers_hash: Optional[int] # hash(CIMultiDict) + server_hostname: Optional[str] = None def _is_expected_content_type( @@ -377,6 +378,7 @@ class ClientRequest: self.proxy, self.proxy_auth, h, + self.server_hostname, ) @property diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 2c531d7d3..40c551ffa 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -462,6 +462,35 @@ async def test_ssl_client( assert txt == "Test message" +async def test_server_hostname_override_not_reused(aiohttp_server) -> None: + """A pooled TLS connection must not be reused for a different server_hostname.""" + trustme = pytest.importorskip("trustme") + + ca = trustme.CA() + cert = ca.issue_cert("first.example") + server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + cert.configure_cert(server_ctx) + client_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) + ca.configure_trust(client_ctx) + + async def handler(request: web.Request) -> web.Response: + return web.Response(text="ok") + + app = web.Application() + app.router.add_route("GET", "/", handler) + server = await aiohttp_server(app, ssl=server_ctx) + url = server.make_url("/") + + connector = aiohttp.TCPConnector(ssl=client_ctx, limit=1, limit_per_host=1) + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get(url, server_hostname="first.example") as resp: + assert resp.status == 200 + await resp.read() + + with pytest.raises(aiohttp.ClientConnectorCertificateError): + await session.get(url, server_hostname="second.example") + + async def test_tcp_connector_fingerprint_ok( aiohttp_server, aiohttp_client, diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 6084f6854..0ef6e92d6 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -1326,6 +1326,20 @@ def test_insecure_fingerprint_sha1(loop) -> None: Fingerprint(hashlib.sha1(b"foo").digest()) +def test_connection_key_includes_server_hostname(make_request) -> None: + """A server_hostname override must be part of the connection reuse key.""" + url = URL("https://127.0.0.1:8443/") + none_req = make_request("GET", url) + first = make_request("GET", url, server_hostname="first.example") + first_again = make_request("GET", url, server_hostname="first.example") + second = make_request("GET", url, server_hostname="second.example") + + assert first.connection_key.server_hostname == "first.example" + assert first.connection_key != none_req.connection_key + assert first.connection_key != second.connection_key + assert first.connection_key == first_again.connection_key + + def test_loose_cookies_types(loop) -> None: req = ClientRequest("get", URL("http://python.org"), loop=loop) morsel = Morsel() -- 2.35.6