From 86c0a31fa8720c234ebc1c9535ada6eab07d9cc8 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Tue, 30 Jun 2026 13:46:50 +0800 Subject: [PATCH 1/3] fix: secure project update temp staging --- astrbot/dashboard/services/update_service.py | 278 ++++++++++--------- 1 file changed, 140 insertions(+), 138 deletions(-) diff --git a/astrbot/dashboard/services/update_service.py b/astrbot/dashboard/services/update_service.py index 388812e70c..711ef07ed4 100644 --- a/astrbot/dashboard/services/update_service.py +++ b/astrbot/dashboard/services/update_service.py @@ -2,6 +2,7 @@ import asyncio import inspect +import tempfile import traceback import uuid import zipfile @@ -18,7 +19,7 @@ from astrbot.core.updator import AstrBotUpdator from astrbot.core.utils.astrbot_path import ( get_astrbot_data_path, - get_astrbot_system_tmp_path, + get_astrbot_temp_path, ) from astrbot.core.utils.io import ( download_dashboard as _download_dashboard, @@ -194,158 +195,166 @@ async def _run_update_project( reboot: Whether to restart AstrBot after applying files. proxy: Optional GitHub proxy URL. """ - update_temp_dir = Path(get_astrbot_system_tmp_path()) / "updates" - update_temp_dir.mkdir(parents=True, exist_ok=True) - update_token = uuid.uuid4().hex - dashboard_zip_path = update_temp_dir / f"{update_token}-dashboard.zip" - core_zip_path = update_temp_dir / f"{update_token}-core.zip" + update_temp_parent = Path(get_astrbot_temp_path()) / "updates" try: - self._set_update_stage( - progress_id, - "dashboard", - "running", - "正在下载 WebUI...", - 0, - ) - await self.download_dashboard( - path=str(dashboard_zip_path), - latest=latest, - version=version, - proxy=proxy or "", - progress_callback=self._make_progress_callback( + update_temp_parent.mkdir(mode=0o700, parents=True, exist_ok=True) + update_temp_parent.chmod(0o700) + with tempfile.TemporaryDirectory( + prefix="project-update-", + dir=update_temp_parent, + ) as update_temp_dir_name: + update_temp_dir = Path(update_temp_dir_name) + update_token = uuid.uuid4().hex + dashboard_zip_path = update_temp_dir / f"{update_token}-dashboard.zip" + core_zip_path = update_temp_dir / f"{update_token}-core.zip" + self._set_update_stage( progress_id, "dashboard", + "running", + "正在下载 WebUI...", 0, - 45, - ), - extract=False, - ) - self._set_update_stage( - progress_id, - "dashboard", - "done", - "WebUI 下载完成。", - 45, - ) - - self._set_update_stage( - progress_id, - "core", - "running", - "正在下载 AstrBot 项目代码...", - 45, - ) - core_zip_path = Path( - await self.astrbot_updator.download_update_package( + ) + await self.download_dashboard( + path=str(dashboard_zip_path), latest=latest, version=version, proxy=proxy or "", - path=core_zip_path, progress_callback=self._make_progress_callback( progress_id, - "core", - 45, + "dashboard", + 0, 45, ), + extract=False, + ) + self._set_update_stage( + progress_id, + "dashboard", + "done", + "WebUI 下载完成。", + 45, ) - ) - self._set_update_stage( - progress_id, - "core", - "done", - "项目代码下载完成。", - 90, - ) - self._set_update_stage( - progress_id, - "verify", - "running", - "下载完成,正在校验更新包...", - 90, - ) + self._set_update_stage( + progress_id, + "core", + "running", + "正在下载 AstrBot 项目代码...", + 45, + ) + core_zip_path = Path( + await self.astrbot_updator.download_update_package( + latest=latest, + version=version, + proxy=proxy or "", + path=core_zip_path, + progress_callback=self._make_progress_callback( + progress_id, + "core", + 45, + 45, + ), + ) + ) + self._set_update_stage( + progress_id, + "core", + "done", + "项目代码下载完成。", + 90, + ) - def _verify_update_packages() -> None: - for zip_path in (dashboard_zip_path, core_zip_path): - with zipfile.ZipFile(zip_path, "r") as archive: - corrupt_member = archive.testzip() - if corrupt_member: - raise UpdateServiceError(f"更新包校验失败: {corrupt_member}") - - await asyncio.to_thread(_verify_update_packages) - self._set_update_stage( - progress_id, - "verify", - "done", - "更新包校验完成。", - 91, - ) + self._set_update_stage( + progress_id, + "verify", + "running", + "下载完成,正在校验更新包...", + 90, + ) - self._set_update_stage( - progress_id, - "apply", - "running", - "下载完成,正在应用更新...", - 91, - ) - await asyncio.to_thread( - self.astrbot_updator.apply_update_package, - core_zip_path, - ) - await self.extract_dashboard( - dashboard_zip_path, - Path(get_astrbot_data_path()), - ) - self._set_update_stage( - progress_id, - "apply", - "done", - "更新文件应用完成。", - 92, - ) + def _verify_update_packages() -> None: + for zip_path in (dashboard_zip_path, core_zip_path): + with zipfile.ZipFile(zip_path, "r") as archive: + corrupt_member = archive.testzip() + if corrupt_member: + raise UpdateServiceError( + f"更新包校验失败: {corrupt_member}" + ) - self._set_update_stage( - progress_id, - "dependencies", - "running", - "正在更新依赖...", - 92, - ) - logger.info("更新依赖中...") - try: - await self.pip_install(requirements_path="requirements.txt") - except Exception as exc: - logger.error(f"更新依赖失败: {exc}") - self._set_update_stage( - progress_id, - "dependencies", - "done", - "依赖更新完成。", - 96, - ) + await asyncio.to_thread(_verify_update_packages) + self._set_update_stage( + progress_id, + "verify", + "done", + "更新包校验完成。", + 91, + ) + + self._set_update_stage( + progress_id, + "apply", + "running", + "下载完成,正在应用更新...", + 91, + ) + await asyncio.to_thread( + self.astrbot_updator.apply_update_package, + core_zip_path, + ) + await self.extract_dashboard( + dashboard_zip_path, + Path(get_astrbot_data_path()), + ) + self._set_update_stage( + progress_id, + "apply", + "done", + "更新文件应用完成。", + 92, + ) - if reboot: self._set_update_stage( progress_id, - "restart", + "dependencies", "running", - "更新成功,正在准备重启...", - 98, + "正在更新依赖...", + 92, + ) + logger.info("更新依赖中...") + try: + await self.pip_install(requirements_path="requirements.txt") + except Exception as exc: + logger.error(f"更新依赖失败: {exc}") + self._set_update_stage( + progress_id, + "dependencies", + "done", + "依赖更新完成。", + 96, ) - await self.core_lifecycle.restart() - message = "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。" - else: - message = "更新成功,AstrBot 将在下次启动时应用新的代码。" - self.update_progress[progress_id].update( - { - "status": "success", - "stage": "done", - "message": message, - "overall_percent": 100, - }, - ) - logger.info(message) + if reboot: + self._set_update_stage( + progress_id, + "restart", + "running", + "更新成功,正在准备重启...", + 98, + ) + await self.core_lifecycle.restart() + message = "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。" + else: + message = "更新成功,AstrBot 将在下次启动时应用新的代码。" + + self.update_progress[progress_id].update( + { + "status": "success", + "stage": "done", + "message": message, + "overall_percent": 100, + }, + ) + logger.info(message) except asyncio.CancelledError: self.update_progress[progress_id].update( { @@ -364,13 +373,6 @@ def _verify_update_packages() -> None: ) logger.error(f"/api/update_project: {traceback.format_exc()}") logger.debug(f"Update task failed: {exc!s}") - finally: - for zip_path in (dashboard_zip_path, core_zip_path): - try: - if zip_path.exists(): - zip_path.unlink() - except Exception as cleanup_exc: - logger.warning(f"清理更新临时文件失败: {zip_path}, {cleanup_exc}") async def update_dashboard(self) -> UpdateServiceResult: try: From c992ebd52d2ed129d92cd369b30eb7ed22f17263 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Tue, 30 Jun 2026 13:51:16 +0800 Subject: [PATCH 2/3] Update astrbot/dashboard/services/update_service.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- astrbot/dashboard/services/update_service.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/astrbot/dashboard/services/update_service.py b/astrbot/dashboard/services/update_service.py index 711ef07ed4..c369180c22 100644 --- a/astrbot/dashboard/services/update_service.py +++ b/astrbot/dashboard/services/update_service.py @@ -197,13 +197,16 @@ async def _run_update_project( """ update_temp_parent = Path(get_astrbot_temp_path()) / "updates" try: + if update_temp_parent.is_symlink(): + update_temp_parent.unlink() update_temp_parent.mkdir(mode=0o700, parents=True, exist_ok=True) update_temp_parent.chmod(0o700) - with tempfile.TemporaryDirectory( + update_temp_dir_obj = tempfile.TemporaryDirectory( prefix="project-update-", dir=update_temp_parent, - ) as update_temp_dir_name: - update_temp_dir = Path(update_temp_dir_name) + ) + try: + update_temp_dir = Path(update_temp_dir_obj.name) update_token = uuid.uuid4().hex dashboard_zip_path = update_temp_dir / f"{update_token}-dashboard.zip" core_zip_path = update_temp_dir / f"{update_token}-core.zip" From f6a1fc3fef44f064849653d0ac40e019559dc83b Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Tue, 30 Jun 2026 14:00:49 +0800 Subject: [PATCH 3/3] fix: close update staging temp context --- astrbot/dashboard/services/update_service.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/astrbot/dashboard/services/update_service.py b/astrbot/dashboard/services/update_service.py index c369180c22..84f3cf36f0 100644 --- a/astrbot/dashboard/services/update_service.py +++ b/astrbot/dashboard/services/update_service.py @@ -201,12 +201,11 @@ async def _run_update_project( update_temp_parent.unlink() update_temp_parent.mkdir(mode=0o700, parents=True, exist_ok=True) update_temp_parent.chmod(0o700) - update_temp_dir_obj = tempfile.TemporaryDirectory( + with tempfile.TemporaryDirectory( prefix="project-update-", dir=update_temp_parent, - ) - try: - update_temp_dir = Path(update_temp_dir_obj.name) + ) as update_temp_dir_name: + update_temp_dir = Path(update_temp_dir_name) update_token = uuid.uuid4().hex dashboard_zip_path = update_temp_dir / f"{update_token}-dashboard.zip" core_zip_path = update_temp_dir / f"{update_token}-core.zip"