diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index a12b2fd579..00974bf460 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1054,7 +1054,7 @@ class DownloadBinaryRequestHandler(BaseHandler): # fallback to type=, but prioritize file= file_name = self.get_argument("type", None) file_name = self.get_argument("file", file_name) - if file_name is None: + if file_name is None or not file_name.strip(): self.send_error(400) return # get requested download name, or build it based on filename @@ -1087,7 +1087,7 @@ class DownloadBinaryRequestHandler(BaseHandler): found = False for image in idedata.extra_flash_images: - if image.path.endswith(file_name): + if image.path.as_posix().endswith(file_name): path = image.path download_name = file_name found = True diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 19e19c7d8d..95afe19899 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -614,6 +614,28 @@ async def test_download_binary_handler_no_firmware_bin_path( assert exc_info.value.code == 404 +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +@pytest.mark.parametrize("file_value", ["", " ", "%20"]) +async def test_download_binary_handler_empty_file_name( + dashboard: DashboardTestHelper, + mock_storage_json: MagicMock, + file_value: str, +) -> None: + """Test that download returns 400 for empty or whitespace-only file names.""" + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = Path("/fake/firmware.bin") + mock_storage_json.load.return_value = mock_storage + + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + f"/download.bin?configuration=test.yaml&file={file_value}", + method="GET", + ) + assert exc_info.value.code == 400 + + @pytest.mark.asyncio @pytest.mark.usefixtures("mock_ext_storage_path") async def test_download_binary_handler_multiple_subdirectory_levels(