From 14066dfb5ac20b1b2e375a6c47e43fdb68426d10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Jun 2019 23:08:55 -0700 Subject: [PATCH] Check cloud trusted proxies (#24395) --- homeassistant/components/cloud/const.py | 4 ++ homeassistant/components/cloud/http_api.py | 8 +++- homeassistant/components/cloud/prefs.py | 22 ++++++++- homeassistant/components/http/__init__.py | 1 + tests/components/cloud/test_http_api.py | 53 +++++++++++++++++++++- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index e2f4b9c0785..65062213a63 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -38,3 +38,7 @@ DISPATCHER_REMOTE_UPDATE = 'cloud_remote_update' class InvalidTrustedNetworks(Exception): """Raised when invalid trusted networks config.""" + + +class InvalidTrustedProxies(Exception): + """Raised when invalid trusted proxies config.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index e6151a917af..9908268b252 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -18,7 +18,8 @@ from homeassistant.components.google_assistant import helpers as google_helpers from .const import ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, - PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks) + PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks, + InvalidTrustedProxies) _LOGGER = logging.getLogger(__name__) @@ -52,7 +53,10 @@ SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ _CLOUD_ERRORS = { InvalidTrustedNetworks: (500, 'Remote UI not compatible with 127.0.0.1/::1' - ' as a trusted network.') + ' as a trusted network.'), + InvalidTrustedProxies: + (500, 'Remote UI not compatible with 127.0.0.1/::1' + ' as trusted proxies.'), } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0f45f25c49b..9f2579134e5 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -6,7 +6,7 @@ from .const import ( PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA, PREF_ALIASES, PREF_SHOULD_EXPOSE, - InvalidTrustedNetworks) + InvalidTrustedNetworks, InvalidTrustedProxies) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -59,6 +59,9 @@ class CloudPreferences: if remote_enabled is True and self._has_local_trusted_network: raise InvalidTrustedNetworks + if remote_enabled is True and self._has_local_trusted_proxies: + raise InvalidTrustedProxies + await self._store.async_save(self._prefs) async def async_update_google_entity_config( @@ -112,7 +115,7 @@ class CloudPreferences: if not enabled: return False - if self._has_local_trusted_network: + if self._has_local_trusted_network or self._has_local_trusted_proxies: return False return True @@ -162,3 +165,18 @@ class CloudPreferences: return True return False + + @property + def _has_local_trusted_proxies(self) -> bool: + """Return if we allow localhost to be a proxy and use its data.""" + if not hasattr(self._hass, 'http'): + return False + + local4 = ip_address('127.0.0.1') + local6 = ip_address('::1') + + if any(local4 in nwk or local6 in nwk + for nwk in self._hass.http.trusted_proxies): + return True + + return False diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index ad64b38200a..a21fb2ab632 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -228,6 +228,7 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile self._handler = None diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 5ccaba14be6..24bd647405a 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,6 +1,7 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from unittest.mock import patch, MagicMock +from ipaddress import ip_network import pytest from jose import jwt @@ -672,7 +673,7 @@ async def test_enabling_remote_trusted_networks_local6( async def test_enabling_remote_trusted_networks_other( hass, hass_ws_client, setup_api, mock_cloud_login): - """Test we cannot enable remote UI when trusted networks active.""" + """Test we can enable remote UI when trusted networks active.""" hass.auth._providers[('trusted_networks', None)] = \ tn_auth.TrustedNetworksAuthProvider( hass, None, tn_auth.CONFIG_SCHEMA({ @@ -749,3 +750,53 @@ async def test_update_google_entity( 'aliases': ['lefty', 'righty'], 'disable_2fa': False, } + + +async def test_enabling_remote_trusted_proxies_local4( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test we cannot enable remote UI when trusted networks active.""" + hass.http.trusted_proxies.append(ip_network('127.0.0.1')) + + client = await hass_ws_client(hass) + + with patch( + 'hass_nabucasa.remote.RemoteUI.connect', + side_effect=AssertionError + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/remote/connect', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 500 + assert response['error']['message'] == \ + 'Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.' + + assert len(mock_connect.mock_calls) == 0 + + +async def test_enabling_remote_trusted_proxies_local6( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test we cannot enable remote UI when trusted networks active.""" + hass.http.trusted_proxies.append(ip_network('::1')) + + client = await hass_ws_client(hass) + + with patch( + 'hass_nabucasa.remote.RemoteUI.connect', + side_effect=AssertionError + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/remote/connect', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 500 + assert response['error']['message'] == \ + 'Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.' + + assert len(mock_connect.mock_calls) == 0