Add concept of allowed external URLs to config (#36988)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Aaron Bach 2020-06-24 18:37:01 -06:00 committed by GitHub
parent cbb76be9d0
commit 7968cd650a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 66 additions and 7 deletions

View file

@ -20,6 +20,7 @@ from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME,
ATTR_HIDDEN,
CONF_ALLOWLIST_EXTERNAL_URLS,
CONF_AUTH_MFA_MODULES,
CONF_AUTH_PROVIDERS,
CONF_CUSTOMIZE,
@ -185,6 +186,7 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend(
vol.Optional(CONF_WHITELIST_EXTERNAL_DIRS): vol.All(
cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter
),
vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(cv.ensure_list, [cv.url]),
vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA,
vol.Optional(CONF_AUTH_PROVIDERS): vol.All(
cv.ensure_list,
@ -502,6 +504,14 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non
if CONF_WHITELIST_EXTERNAL_DIRS in config:
hac.whitelist_external_dirs.update(set(config[CONF_WHITELIST_EXTERNAL_DIRS]))
# Init whitelist external URL list make sure to add / to every URL that doesn't
# already have it so that we can properly test "path ownership"
if CONF_ALLOWLIST_EXTERNAL_URLS in config:
hac.allowlist_external_urls.update(
url if url.endswith("/") else f"{url}/"
for url in config[CONF_ALLOWLIST_EXTERNAL_URLS]
)
# Customize
cust_exact = dict(config[CONF_CUSTOMIZE])
cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])

View file

@ -32,13 +32,14 @@ CONF_ACCESS_TOKEN = "access_token"
CONF_ADDRESS = "address"
CONF_AFTER = "after"
CONF_ALIAS = "alias"
CONF_ALLOWLIST_EXTERNAL_URLS = "allowlist_external_urls"
CONF_API_KEY = "api_key"
CONF_API_VERSION = "api_version"
CONF_ARMING_TIME = "arming_time"
CONF_AT = "at"
CONF_AUTHENTICATION = "authentication"
CONF_AUTH_MFA_MODULES = "auth_mfa_modules"
CONF_AUTH_PROVIDERS = "auth_providers"
CONF_AUTHENTICATION = "authentication"
CONF_BASE = "base"
CONF_BEFORE = "before"
CONF_BELOW = "below"
@ -68,9 +69,9 @@ CONF_CUSTOMIZE_GLOB = "customize_glob"
CONF_DELAY = "delay"
CONF_DELAY_TIME = "delay_time"
CONF_DEVICE = "device"
CONF_DEVICES = "devices"
CONF_DEVICE_CLASS = "device_class"
CONF_DEVICE_ID = "device_id"
CONF_DEVICES = "devices"
CONF_DISARM_AFTER_TRIGGER = "disarm_after_trigger"
CONF_DISCOVERY = "discovery"
CONF_DISKS = "disks"
@ -90,8 +91,8 @@ CONF_EVENT_DATA = "event_data"
CONF_EVENT_DATA_TEMPLATE = "event_data_template"
CONF_EXCLUDE = "exclude"
CONF_EXTERNAL_URL = "external_url"
CONF_FILE_PATH = "file_path"
CONF_FILENAME = "filename"
CONF_FILE_PATH = "file_path"
CONF_FOR = "for"
CONF_FORCE_UPDATE = "force_update"
CONF_FRIENDLY_NAME = "friendly_name"
@ -138,15 +139,15 @@ CONF_RADIUS = "radius"
CONF_RECIPIENT = "recipient"
CONF_REGION = "region"
CONF_RESOURCE = "resource"
CONF_RESOURCE_TEMPLATE = "resource_template"
CONF_RESOURCES = "resources"
CONF_RESOURCE_TEMPLATE = "resource_template"
CONF_RGB = "rgb"
CONF_ROOM = "room"
CONF_SCAN_INTERVAL = "scan_interval"
CONF_SCENE = "scene"
CONF_SENDER = "sender"
CONF_SENSOR_TYPE = "sensor_type"
CONF_SENSORS = "sensors"
CONF_SENSOR_TYPE = "sensor_type"
CONF_SERVICE = "service"
CONF_SERVICE_DATA = "data"
CONF_SERVICE_TEMPLATE = "service_template"
@ -159,8 +160,8 @@ CONF_STATE_TEMPLATE = "state_template"
CONF_STRUCTURE = "structure"
CONF_SWITCHES = "switches"
CONF_TEMPERATURE_UNIT = "temperature_unit"
CONF_TIME_ZONE = "time_zone"
CONF_TIMEOUT = "timeout"
CONF_TIME_ZONE = "time_zone"
CONF_TOKEN = "token"
CONF_TRIGGER_TIME = "trigger_time"
CONF_TTL = "ttl"
@ -174,9 +175,9 @@ CONF_VERIFY_SSL = "verify_ssl"
CONF_WAIT_TEMPLATE = "wait_template"
CONF_WEBHOOK_ID = "webhook_id"
CONF_WEEKDAY = "weekday"
CONF_WHITE_VALUE = "white_value"
CONF_WHITELIST = "whitelist"
CONF_WHITELIST_EXTERNAL_DIRS = "whitelist_external_dirs"
CONF_WHITE_VALUE = "white_value"
CONF_XY = "xy"
CONF_ZONE = "zone"

View file

@ -1332,6 +1332,9 @@ class Config:
# List of allowed external dirs to access
self.whitelist_external_dirs: Set[str] = set()
# List of allowed external URLs that integrations may use
self.allowlist_external_urls: Set[str] = set()
# If Home Assistant is running in safe mode
self.safe_mode: bool = False
@ -1353,6 +1356,16 @@ class Config:
raise HomeAssistantError("config_dir is not set")
return os.path.join(self.config_dir, *path)
def is_allowed_external_url(self, url: str) -> bool:
"""Check if an external URL is allowed."""
parsed_url = f"{str(yarl.URL(url))}/"
return any(
allowed
for allowed in self.allowlist_external_urls
if parsed_url.startswith(allowed)
)
def is_allowed_path(self, path: str) -> bool:
"""Check if the path is valid for access from outside."""
assert path is not None
@ -1395,6 +1408,7 @@ class Config:
"components": self.components,
"config_dir": self.config_dir,
"whitelist_external_dirs": self.whitelist_external_dirs,
"allowlist_external_urls": self.allowlist_external_urls,
"version": __version__,
"config_source": self.config_source,
"safe_mode": self.safe_mode,

View file

@ -212,6 +212,8 @@ async def test_api_get_config(hass, mock_api_client):
result["components"] = set(result["components"])
if "whitelist_external_dirs" in result:
result["whitelist_external_dirs"] = set(result["whitelist_external_dirs"])
if "allowlist_external_urls" in result:
result["allowlist_external_urls"] = set(result["allowlist_external_urls"])
assert hass.config.as_dict() == result

View file

@ -234,6 +234,10 @@ async def test_get_config(hass, websocket_client):
msg["result"]["whitelist_external_dirs"] = set(
msg["result"]["whitelist_external_dirs"]
)
if "allowlist_external_urls" in msg["result"]:
msg["result"]["allowlist_external_urls"] = set(
msg["result"]["allowlist_external_urls"]
)
assert msg["result"] == hass.config.as_dict()

View file

@ -915,6 +915,7 @@ class TestConfig(unittest.TestCase):
"components": set(),
"config_dir": "/test/ha-config",
"whitelist_external_dirs": set(),
"allowlist_external_urls": set(),
"version": __version__,
"config_source": "default",
"safe_mode": False,
@ -955,6 +956,33 @@ class TestConfig(unittest.TestCase):
with pytest.raises(AssertionError):
self.config.is_allowed_path(None)
def test_is_allowed_external_url(self):
"""Test is_allowed_external_url method."""
self.config.allowlist_external_urls = [
"http://x.com/",
"https://y.com/bla/",
"https://z.com/images/1.jpg/",
]
valid = [
"http://x.com/1.jpg",
"http://x.com",
"https://y.com/bla/",
"https://y.com/bla/2.png",
"https://z.com/images/1.jpg",
]
for url in valid:
assert self.config.is_allowed_external_url(url)
invalid = [
"https://a.co",
"https://y.com/bla_wrong",
"https://y.com/bla/../image.jpg",
"https://z.com/images",
]
for url in invalid:
assert not self.config.is_allowed_external_url(url)
async def test_event_on_update(hass):
"""Test that event is fired on update."""