diff --git a/supervisor/addons/const.py b/supervisor/addons/const.py index 7db9d38ee..06d9f8c26 100644 --- a/supervisor/addons/const.py +++ b/supervisor/addons/const.py @@ -26,3 +26,5 @@ ADDON_UPDATE_CONDITIONS = [ JobCondition.PLUGINS_UPDATED, JobCondition.SUPERVISOR_UPDATED, ] + +RE_SLUG = r"[-_.A-Za-z0-9]+" diff --git a/supervisor/addons/validate.py b/supervisor/addons/validate.py index bfb5073ce..e0a98489f 100644 --- a/supervisor/addons/validate.py +++ b/supervisor/addons/validate.py @@ -109,7 +109,7 @@ from ..validate import ( uuid_match, version_tag, ) -from .const import ATTR_BACKUP, ATTR_CODENOTARY, AddonBackupMode +from .const import ATTR_BACKUP, ATTR_CODENOTARY, RE_SLUG, AddonBackupMode from .options import RE_SCHEMA_ELEMENT _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -147,6 +147,8 @@ RE_MACHINE = re.compile( r")$" ) +RE_SLUG_FIELD = re.compile(r"^" + RE_SLUG + r"$") + def _warn_addon_config(config: dict[str, Any]): """Warn about miss configs.""" @@ -252,7 +254,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema( { vol.Required(ATTR_NAME): str, vol.Required(ATTR_VERSION): version_tag, - vol.Required(ATTR_SLUG): str, + vol.Required(ATTR_SLUG): vol.Match(RE_SLUG_FIELD), vol.Required(ATTR_DESCRIPTON): str, vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)], vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()), diff --git a/supervisor/api/middleware/security.py b/supervisor/api/middleware/security.py index 2881ed6db..955d215f7 100644 --- a/supervisor/api/middleware/security.py +++ b/supervisor/api/middleware/security.py @@ -8,6 +8,7 @@ from aiohttp.web import Request, RequestHandler, Response, middleware from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPUnauthorized from awesomeversion import AwesomeVersion +from ...addons.const import RE_SLUG from ...const import ( REQUEST_FROM, ROLE_ADMIN, @@ -27,7 +28,7 @@ _CORE_VERSION: Final = AwesomeVersion("2023.3.0") _CORE_FRONTEND_PATHS: Final = ( r"|/app/.*\.(?:js|gz|json|map)" - r"|/(store/)?addons/[^/]+/(logo|icon)" + r"|/(store/)?addons/" + RE_SLUG + r"/(logo|icon)" ) CORE_FRONTEND: Final = re.compile( @@ -51,7 +52,7 @@ NO_SECURITY_CHECK: Final = re.compile( r"|/core/api/.*" r"|/core/websocket" r"|/supervisor/ping" - r"|/ingress/[^/]+/.*" + r"|/ingress/[-_A-Za-z0-9]+/.*" + _CORE_FRONTEND_PATHS + r")$" ) @@ -98,7 +99,7 @@ ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = { ROLE_MANAGER: re.compile( r"^(?:" r"|/.+/info" - r"|/addons(?:/[^/]+/(?!security).+|/reload)?" + r"|/addons(?:/" + RE_SLUG + r"/(?!security).+|/reload)?" r"|/audio/.+" r"|/auth/cache" r"|/cli/.+" diff --git a/tests/addons/test_config.py b/tests/addons/test_config.py index 588ccd129..a761d1e56 100644 --- a/tests/addons/test_config.py +++ b/tests/addons/test_config.py @@ -249,3 +249,38 @@ def test_watchdog_url(): ): config["watchdog"] = test_options assert vd.SCHEMA_ADDON_CONFIG(config) + + +def test_valid_slug(): + """Test valid and invalid addon slugs.""" + config = load_json_fixture("basic-addon-config.json") + + # All examples pulled from https://analytics.home-assistant.io/addons.json + config["slug"] = "uptime-kuma" + assert vd.SCHEMA_ADDON_CONFIG(config) + + config["slug"] = "hassio_google_drive_backup" + assert vd.SCHEMA_ADDON_CONFIG(config) + + config["slug"] = "paradox_alarm_interface_3.x" + assert vd.SCHEMA_ADDON_CONFIG(config) + + config["slug"] = "Lupusec2Mqtt" + assert vd.SCHEMA_ADDON_CONFIG(config) + + # No whitespace + config["slug"] = "my addon" + with pytest.raises(vol.Invalid): + assert vd.SCHEMA_ADDON_CONFIG(config) + + # No url control chars (or other non-word ascii characters) + config["slug"] = "a/b_&_c\\d_@ddon$:_test=#2?" + with pytest.raises(vol.Invalid): + assert vd.SCHEMA_ADDON_CONFIG(config) + + # No unicode + config["slug"] = "complemento telefónico" + with pytest.raises(vol.Invalid): + assert vd.SCHEMA_ADDON_CONFIG(config) + + #