From 22a12cc2e2ef289d9e96fd87dfc17272177e52ac Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 1 Oct 2024 01:13:35 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20issue=206652:=20Raise=20`aiohttp.ServerFi?= =?UTF-8?q?ngerprintMismatch`=20exception=20o=E2=80=A6=20(#9363)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …… (#6653) (cherry picked from commit e3b1011f2146ad0faa4c3d3c29f26b73e1400564) Co-authored-by: Gang Ji <62988402+gangj@users.noreply.github.com> --- CHANGES/6652.bugfix | 1 + CONTRIBUTORS.txt | 1 + aiohttp/connector.py | 10 ++++ tests/test_proxy.py | 106 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 CHANGES/6652.bugfix diff --git a/CHANGES/6652.bugfix b/CHANGES/6652.bugfix new file mode 100644 index 00000000000..4ce1f678792 --- /dev/null +++ b/CHANGES/6652.bugfix @@ -0,0 +1 @@ +Raise `aiohttp.ServerFingerprintMismatch` exception on client-side if request through http proxy with mismatching server fingerprint digest: `aiohttp.ClientSession(headers=headers, connector=TCPConnector(ssl=aiohttp.Fingerprint(mismatch_digest), trust_env=True).request(...)`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 92e1666fbc6..96403c2aec4 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -127,6 +127,7 @@ Franek Magiera Frederik Gladhorn Frederik Peter Aalund Gabriel Tremblay +Gang Ji Gary Wilson Jr. Gennady Andreyev Georges Dubus diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 6bc3ee54cdf..13c1a0cdc48 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -1217,6 +1217,16 @@ async def _start_tls_connection( # chance to do this: underlying_transport.close() raise + if isinstance(tls_transport, asyncio.Transport): + fingerprint = self._get_fingerprint(req) + if fingerprint: + try: + fingerprint.check(tls_transport) + except ServerFingerprintMismatch: + tls_transport.close() + if not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(tls_transport) + raise except cert_errors as exc: raise ClientConnectorCertificateError(req.connection_key, exc) from exc except ssl_errors as exc: diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 4fa5e932098..c98ae7c2653 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -11,7 +11,7 @@ from yarl import URL import aiohttp -from aiohttp.client_reqrep import ClientRequest, ClientResponse +from aiohttp.client_reqrep import ClientRequest, ClientResponse, Fingerprint from aiohttp.connector import _SSL_CONTEXT_VERIFIED from aiohttp.helpers import TimerNoop from aiohttp.test_utils import make_mocked_coro @@ -384,7 +384,109 @@ async def make_conn(): autospec=True, spec_set=True, ) - def test_https_connect(self, start_connection: Any, ClientRequestMock: Any) -> None: + def test_https_connect_fingerprint_mismatch( + self, start_connection: mock.Mock, ClientRequestMock: mock.Mock + ) -> None: + async def make_conn() -> aiohttp.TCPConnector: + return aiohttp.TCPConnector(enable_cleanup_closed=cleanup) + + for cleanup in (True, False): + with self.subTest(cleanup=cleanup): + proxy_req = ClientRequest( + "GET", URL("http://proxy.example.com"), loop=self.loop + ) + ClientRequestMock.return_value = proxy_req + + class TransportMock(asyncio.Transport): + def close(self) -> None: + pass + + proxy_resp = ClientResponse( + "get", + URL("http://proxy.example.com"), + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + traces=[], + loop=self.loop, + session=mock.Mock(), + ) + fingerprint_mock = mock.Mock(spec=Fingerprint, auto_spec=True) + fingerprint_mock.check.side_effect = aiohttp.ServerFingerprintMismatch( + b"exp", b"got", "example.com", 8080 + ) + with mock.patch.object( + proxy_req, + "send", + autospec=True, + spec_set=True, + return_value=proxy_resp, + ), mock.patch.object( + proxy_resp, + "start", + autospec=True, + spec_set=True, + return_value=mock.Mock(status=200), + ): + connector = self.loop.run_until_complete(make_conn()) + host = [ + { + "hostname": "hostname", + "host": "127.0.0.1", + "port": 80, + "family": socket.AF_INET, + "proto": 0, + "flags": 0, + } + ] + with mock.patch.object( + connector, + "_resolve_host", + autospec=True, + spec_set=True, + return_value=host, + ), mock.patch.object( + connector, + "_get_fingerprint", + autospec=True, + spec_set=True, + return_value=fingerprint_mock, + ), mock.patch.object( # Called on connection to http://proxy.example.com + self.loop, + "create_connection", + autospec=True, + spec_set=True, + return_value=(mock.Mock(), mock.Mock()), + ), mock.patch.object( # Called on connection to https://www.python.org + self.loop, + "start_tls", + autospec=True, + spec_set=True, + return_value=TransportMock(), + ): + req = ClientRequest( + "GET", + URL("https://www.python.org"), + proxy=URL("http://proxy.example.com"), + loop=self.loop, + ) + with self.assertRaises(aiohttp.ServerFingerprintMismatch): + self.loop.run_until_complete( + connector._create_connection( + req, [], aiohttp.ClientTimeout() + ) + ) + + @mock.patch("aiohttp.connector.ClientRequest") + @mock.patch( + "aiohttp.connector.aiohappyeyeballs.start_connection", + autospec=True, + spec_set=True, + ) + def test_https_connect( + self, start_connection: mock.Mock, ClientRequestMock: mock.Mock + ) -> None: proxy_req = ClientRequest( "GET", URL("http://proxy.example.com"), loop=self.loop )