From d339e3bd8c0d1030fe1f686af26e9f890f8ff687 Mon Sep 17 00:00:00 2001 From: Joakim Plate <elupus@ecce.se> Date: Fri, 2 Jul 2021 11:49:42 +0200 Subject: [PATCH] Reject trusted network access from proxies (#52388) --- .../auth/providers/trusted_networks.py | 14 ++++++++ homeassistant/components/http/forwarded.py | 8 ++--- tests/auth/providers/test_trusted_networks.py | 25 ++++++++++++++ tests/components/http/test_forwarded.py | 33 ++++--------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index fd2014667f8..7b609f371ef 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -81,6 +81,17 @@ class TrustedNetworksAuthProvider(AuthProvider): """Return trusted users per network.""" return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS]) + @property + def trusted_proxies(self) -> list[IPNetwork]: + """Return trusted proxies in the system.""" + if not self.hass.http: + return [] + + return [ + ip_network(trusted_proxy) + for trusted_proxy in self.hass.http.trusted_proxies + ] + @property def support_mfa(self) -> bool: """Trusted Networks auth provider does not support MFA.""" @@ -178,6 +189,9 @@ class TrustedNetworksAuthProvider(AuthProvider): ): raise InvalidAuthError("Not in trusted_networks") + if any(ip_addr in trusted_proxy for trusted_proxy in self.trusted_proxies): + raise InvalidAuthError("Can't allow access from a proxy server") + @callback def async_validate_refresh_token( self, refresh_token: RefreshToken, remote_ip: str | None = None diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 684dbbb9e2b..18bc51af1d1 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -129,11 +129,9 @@ def async_setup_forwarded( overrides["remote"] = str(forwarded_ip) break else: - _LOGGER.warning( - "Request originated directly from a trusted proxy included in X-Forwarded-For: %s, this is likely a miss configuration and will be rejected", - forwarded_for_headers, - ) - raise HTTPBadRequest() + # If all the IP addresses are from trusted networks, take the left-most. + forwarded_for_index = -1 + overrides["remote"] = str(forwarded_for[-1]) # Handle X-Forwarded-Proto forwarded_proto_headers: list[str] = request.headers.getall( diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 412f660adc3..d7574bf0da1 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -8,7 +8,9 @@ import voluptuous as vol from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.components.http import CONF_TRUSTED_PROXIES, CONF_USE_X_FORWARDED_FOR from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.setup import async_setup_component @pytest.fixture @@ -144,6 +146,29 @@ async def test_validate_access(provider): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) +async def test_validate_access_proxy(hass, provider): + """Test validate access from trusted networks are blocked from proxy.""" + + await async_setup_component( + hass, + "http", + { + "http": { + CONF_TRUSTED_PROXIES: ["192.168.128.0/31", "fd00::1"], + CONF_USE_X_FORWARDED_FOR: True, + } + }, + ) + provider.async_validate_access(ip_address("192.168.128.2")) + provider.async_validate_access(ip_address("fd00::2")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.128.0")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.128.1")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("fd00::1")) + + async def test_validate_refresh_token(provider): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 8d8467b699f..400a1f32729 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -43,9 +43,15 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): @pytest.mark.parametrize( "trusted_proxies,x_forwarded_for,remote", [ + ( + ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], + "10.10.10.10, 1.1.1.1", + "10.10.10.10", + ), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123,2.2.2.2,1.1.1.1", "2.2.2.2"), (["127.0.0.0/24"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "1.1.1.1"), + (["127.0.0.0/24"], "127.0.0.1", "127.0.0.1"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 1.1.1.1", "123.123.123.123"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.1"], "255.255.255.255", "255.255.255.255"), @@ -77,33 +83,6 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 -@pytest.mark.parametrize( - "trusted_proxies,x_forwarded_for", - [ - ( - ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], - "10.10.10.10, 1.1.1.1", - ), - (["127.0.0.0/24"], "127.0.0.1"), - ], -) -async def test_x_forwarded_for_from_trusted_proxy_rejected( - trusted_proxies, x_forwarded_for, aiohttp_client -): - """Test that we reject forwarded requests from proxy server itself.""" - - app = web.Application() - app.router.add_get("/", mock_handler) - async_setup_forwarded( - app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] - ) - - mock_api_client = await aiohttp_client(app) - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) - - assert resp.status == 400 - - async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): """Test that we warn when processing is disabled, but proxy has been detected."""