From d4c377a8c93cb6fb7c590e38a700c7d8e54d52d3 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Mon, 23 Jun 2025 13:02:06 +0000 Subject: [PATCH] add check for allowed path --- homeassistant/components/immich/services.py | 7 ++++ homeassistant/components/immich/strings.json | 3 ++ tests/components/immich/test_services.py | 38 +++++++++++++++++--- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/immich/services.py b/homeassistant/components/immich/services.py index f31e8fb48c8..023e988cb10 100644 --- a/homeassistant/components/immich/services.py +++ b/homeassistant/components/immich/services.py @@ -54,6 +54,13 @@ async def _async_upload_file(service_call: ServiceCall) -> None: translation_key="config_entry_not_loaded", ) + if not hass.config.is_allowed_path(target_file): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="path_not_allowed", + translation_placeholders={"file": target_file}, + ) + if not os.path.isfile(target_file): raise ServiceValidationError( translation_domain=DOMAIN, diff --git a/homeassistant/components/immich/strings.json b/homeassistant/components/immich/strings.json index efede3378fe..7134a53db05 100644 --- a/homeassistant/components/immich/strings.json +++ b/homeassistant/components/immich/strings.json @@ -97,6 +97,9 @@ "config_entry_not_loaded": { "message": "Config entry not loaded." }, + "path_not_allowed": { + "message": "Cannot read `{file}`, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`." + }, "file_not_found": { "message": "File `{file}` not found." }, diff --git a/tests/components/immich/test_services.py b/tests/components/immich/test_services.py index 0de464a4209..934589b9c32 100644 --- a/tests/components/immich/test_services.py +++ b/tests/components/immich/test_services.py @@ -37,6 +37,7 @@ async def test_upload_file( tmp_path: Path, ) -> None: """Test upload_file service.""" + hass.config.allowlist_external_dirs = {tmp_path} test_file = tmp_path / "image.png" test_file.write_bytes(b"abcdef") @@ -64,6 +65,7 @@ async def test_upload_file_to_album( tmp_path: Path, ) -> None: """Test upload_file service with target album_id.""" + hass.config.allowlist_external_dirs = {tmp_path} test_file = tmp_path / "image.png" test_file.write_bytes(b"abcdef") @@ -130,23 +132,48 @@ async def test_upload_file_config_entry_not_loaded( ) -async def test_upload_file_file_not_found( +async def test_upload_file_path_not_allowed( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, ) -> None: - """Test upload_file service raising file_not_found.""" + """Test upload_file service raising path_not_allowed.""" + hass.config.allowlist_external_dirs = {} await setup_integration(hass, mock_config_entry) with pytest.raises( - ServiceValidationError, match="File `not_existing.file` not found" + ServiceValidationError, + match="Cannot read `/blabla/not_existing.file`, no access to path;", ): await hass.services.async_call( DOMAIN, SERVICE_UPLOAD_FILE, { "config_entry_id": mock_config_entry.entry_id, - "file": "not_existing.file", + "file": "/blabla/not_existing.file", + }, + blocking=True, + ) + + +async def test_upload_file_file_not_found( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test upload_file service raising file_not_found.""" + hass.config.allowlist_external_dirs = {"/blabla"} + await setup_integration(hass, mock_config_entry) + + with pytest.raises( + ServiceValidationError, match="File `/blabla/not_existing.file` not found" + ): + await hass.services.async_call( + DOMAIN, + SERVICE_UPLOAD_FILE, + { + "config_entry_id": mock_config_entry.entry_id, + "file": "/blabla/not_existing.file", }, blocking=True, ) @@ -159,6 +186,7 @@ async def test_upload_file_album_not_found( tmp_path: Path, ) -> None: """Test upload_file service raising album_not_found.""" + hass.config.allowlist_external_dirs = {tmp_path} test_file = tmp_path / "image.png" test_file.write_bytes(b"abcdef") @@ -196,6 +224,7 @@ async def test_upload_file_upload_failed( tmp_path: Path, ) -> None: """Test upload_file service raising upload_failed.""" + hass.config.allowlist_external_dirs = {tmp_path} test_file = tmp_path / "image.png" test_file.write_bytes(b"abcdef") @@ -230,6 +259,7 @@ async def test_upload_file_to_album_upload_failed( tmp_path: Path, ) -> None: """Test upload_file service with target album_id raising upload_failed.""" + hass.config.allowlist_external_dirs = {tmp_path} test_file = tmp_path / "image.png" test_file.write_bytes(b"abcdef")