Ensure internal/external URL have no path (#54304)

* Ensure internal/external URL have no path

* Fix comment typo

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Paulus Schoutsen 2021-08-09 00:38:09 -07:00 committed by GitHub
parent a8354e729b
commit 952d11cb03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 161 additions and 68 deletions

View File

@ -44,8 +44,8 @@ class CheckConfigView(HomeAssistantView):
vol.Optional("unit_system"): cv.unit_system, vol.Optional("unit_system"): cv.unit_system,
vol.Optional("location_name"): str, vol.Optional("location_name"): str,
vol.Optional("time_zone"): cv.time_zone, vol.Optional("time_zone"): cv.time_zone,
vol.Optional("external_url"): vol.Any(cv.url, None), vol.Optional("external_url"): vol.Any(cv.url_no_path, None),
vol.Optional("internal_url"): vol.Any(cv.url, None), vol.Optional("internal_url"): vol.Any(cv.url_no_path, None),
vol.Optional("currency"): cv.currency, vol.Optional("currency"): cv.currency,
} }
) )

View File

@ -10,6 +10,7 @@ import re
import shutil import shutil
from types import ModuleType from types import ModuleType
from typing import Any, Callable from typing import Any, Callable
from urllib.parse import urlparse
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
import voluptuous as vol import voluptuous as vol
@ -161,6 +162,19 @@ def _no_duplicate_auth_mfa_module(
return configs return configs
def _filter_bad_internal_external_urls(conf: dict) -> dict:
"""Filter internal/external URL with a path."""
for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL:
if key in conf and urlparse(conf[key]).path not in ("", "/"):
# We warn but do not fix, because if this was incorrectly configured,
# adjusting this value might impact security.
_LOGGER.warning(
"Invalid %s set. It's not allowed to have a path (/bla)", key
)
return conf
PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs
vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config
) )
@ -188,7 +202,8 @@ CUSTOMIZE_CONFIG_SCHEMA = vol.Schema(
} }
) )
CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( CORE_CONFIG_SCHEMA = vol.All(
CUSTOMIZE_CONFIG_SCHEMA.extend(
{ {
CONF_NAME: vol.Coerce(str), CONF_NAME: vol.Coerce(str),
CONF_LATITUDE: cv.latitude, CONF_LATITUDE: cv.latitude,
@ -205,7 +220,9 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend(
vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All( vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All(
cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter 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_ALLOWLIST_EXTERNAL_URLS): vol.All(
cv.ensure_list, [cv.url]
),
vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA,
vol.Optional(CONF_AUTH_PROVIDERS): vol.All( vol.Optional(CONF_AUTH_PROVIDERS): vol.All(
cv.ensure_list, cv.ensure_list,
@ -241,6 +258,8 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend(
vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean,
vol.Optional(CONF_CURRENCY): cv.currency, vol.Optional(CONF_CURRENCY): cv.currency,
} }
),
_filter_bad_internal_external_urls,
) )

View File

@ -19,6 +19,7 @@ import threading
from time import monotonic from time import monotonic
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast
from urllib.parse import urlparse
import attr import attr
import voluptuous as vol import voluptuous as vol
@ -1717,7 +1718,23 @@ class Config:
) )
data = await store.async_load() data = await store.async_load()
if data: if not data:
return
# In 2021.9 we fixed validation to disallow a path (because that's never correct)
# but this data still lives in storage, so we print a warning.
if "external_url" in data and urlparse(data["external_url"]).path not in (
"",
"/",
):
_LOGGER.warning("Invalid external_url set. It's not allowed to have a path")
if "internal_url" in data and urlparse(data["internal_url"]).path not in (
"",
"/",
):
_LOGGER.warning("Invalid internal_url set. It's not allowed to have a path")
self._update( self._update(
source=SOURCE_STORAGE, source=SOURCE_STORAGE,
latitude=data.get("latitude"), latitude=data.get("latitude"),

View File

@ -649,6 +649,16 @@ def url(value: Any) -> str:
raise vol.Invalid("invalid url") raise vol.Invalid("invalid url")
def url_no_path(value: Any) -> str:
"""Validate a url without a path."""
url_in = url(value)
if urlparse(url_in).path not in ("", "/"):
raise vol.Invalid("url it not allowed to have a path component")
return url_in
def x10_address(value: str) -> str: def x10_address(value: str) -> str:
"""Validate an x10 address.""" """Validate an x10 address."""
regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$") regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$")

View File

@ -120,6 +120,25 @@ def test_url():
assert schema(value) assert schema(value)
def test_url_no_path():
"""Test URL."""
schema = vol.Schema(cv.url_no_path)
for value in (
"https://localhost/test/index.html",
"http://home-assistant.io/test/",
):
with pytest.raises(vol.MultipleInvalid):
schema(value)
for value in (
"http://localhost",
"http://home-assistant.io",
"https://community.home-assistant.io/",
):
assert schema(value)
def test_platform_config(): def test_platform_config():
"""Test platform config validation.""" """Test platform config validation."""
options = ({}, {"hello": "world"}) options = ({}, {"hello": "world"})

View File

@ -215,6 +215,19 @@ def test_core_config_schema():
) )
def test_core_config_schema_internal_external_warning(caplog):
"""Test that we warn for internal/external URL with path."""
config_util.CORE_CONFIG_SCHEMA(
{
"external_url": "https://www.example.com/bla",
"internal_url": "http://example.local/yo",
}
)
assert "Invalid external_url set" in caplog.text
assert "Invalid internal_url set" in caplog.text
def test_customize_dict_schema(): def test_customize_dict_schema():
"""Test basic customize config validation.""" """Test basic customize config validation."""
values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"}) values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"})

View File

@ -1374,6 +1374,21 @@ async def test_additional_data_in_core_config(hass, hass_storage):
assert config.location_name == "Test Name" assert config.location_name == "Test Name"
async def test_incorrect_internal_external_url(hass, hass_storage, caplog):
"""Test that we warn when detecting invalid internal/extenral url."""
config = ha.Config(hass)
hass_storage[ha.CORE_STORAGE_KEY] = {
"version": 1,
"data": {
"internal_url": "https://community.home-assistant.io/profile",
"external_url": "https://www.home-assistant.io/blue",
},
}
await config.async_load()
assert "Invalid external_url set" in caplog.text
assert "Invalid internal_url set" in caplog.text
async def test_start_events(hass): async def test_start_events(hass):
"""Test events fired when starting Home Assistant.""" """Test events fired when starting Home Assistant."""
hass.state = ha.CoreState.not_running hass.state = ha.CoreState.not_running