From 41e920073b7a285dad7188c79436c965314b9216 Mon Sep 17 00:00:00 2001 From: Sebastion Date: Sun, 19 Apr 2026 16:32:24 +0100 Subject: [PATCH 1/4] fix: prevent path traversal in backup importer (CWE-22) Validate that all file write targets resolve within their expected base directories before writing. This prevents crafted backup ZIP files from writing to arbitrary filesystem locations via malicious path values in attachment records, media file paths, or directory entries. --- astrbot/core/backup/importer.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index b51c7d9560..b78f3fab19 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -59,6 +59,21 @@ def _get_major_version(version_str: str) -> str: return "0.0" + + +def _validate_path_within(target_path: Path, base_dir: Path) -> bool: + """Validate that target_path is within base_dir after resolving symlinks. + + Prevents path traversal attacks (CWE-22) by ensuring the resolved + target path starts with the resolved base directory. + """ + try: + resolved = target_path.resolve() + base_resolved = base_dir.resolve() + return resolved == base_resolved or str(resolved).startswith(str(base_resolved) + os.sep) + except (OSError, ValueError): + return False + CMD_CONFIG_FILE_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json") KB_PATH = get_astrbot_knowledge_base_path() DEFAULT_PLATFORM_STATS_INVALID_COUNT_WARN_LIMIT = 5 @@ -765,6 +780,10 @@ async def _import_knowledge_bases( try: rel_path = name[len(media_prefix) :] target_path = kb_dir / rel_path + # Validate path is within kb directory (CWE-22) + if not _validate_path_within(target_path, kb_dir): + logger.warning(f"媒体文件路径越界,已跳过: {target_path}") + continue target_path.parent.mkdir(parents=True, exist_ok=True) with zf.open(name) as src, open(target_path, "wb") as dst: dst.write(src.read()) @@ -827,6 +846,11 @@ async def _import_attachments( else: target_path = attachments_dir / os.path.basename(name) + # Validate path is within attachments directory (CWE-22) + if not _validate_path_within(target_path, attachments_dir): + logger.warning(f"附件路径越界,已跳过: {target_path}") + continue + target_path.parent.mkdir(parents=True, exist_ok=True) with zf.open(name) as src, open(target_path, "wb") as dst: dst.write(src.read()) @@ -904,6 +928,10 @@ async def _import_directories( continue target_path = target_dir / rel_path + # Validate path is within target directory (CWE-22) + if not _validate_path_within(target_path, target_dir): + result.add_warning(f"文件路径越界,已跳过: {name}") + continue target_path.parent.mkdir(parents=True, exist_ok=True) with zf.open(name) as src, open(target_path, "wb") as dst: From 0f135d63b520627d12e554f51aefae8b7485db75 Mon Sep 17 00:00:00 2001 From: Sebastion Date: Sun, 19 Apr 2026 23:18:50 +0100 Subject: [PATCH 2/4] fix: use Path.is_relative_to for robust path containment check --- astrbot/core/backup/importer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index b78f3fab19..6f7f0a715e 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -65,12 +65,12 @@ def _validate_path_within(target_path: Path, base_dir: Path) -> bool: """Validate that target_path is within base_dir after resolving symlinks. Prevents path traversal attacks (CWE-22) by ensuring the resolved - target path starts with the resolved base directory. + target path is relative to the resolved base directory. """ try: resolved = target_path.resolve() base_resolved = base_dir.resolve() - return resolved == base_resolved or str(resolved).startswith(str(base_resolved) + os.sep) + return resolved.is_relative_to(base_resolved) except (OSError, ValueError): return False From 59884be37a6fa69a9dddc2040a0abc37eed39a50 Mon Sep 17 00:00:00 2001 From: Sebastion Date: Mon, 20 Apr 2026 04:24:50 +0100 Subject: [PATCH 3/4] fix: add explicit strict=False to Path.resolve() calls --- astrbot/core/backup/importer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index 6f7f0a715e..a5a60e749e 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -68,8 +68,8 @@ def _validate_path_within(target_path: Path, base_dir: Path) -> bool: target path is relative to the resolved base directory. """ try: - resolved = target_path.resolve() - base_resolved = base_dir.resolve() + resolved = target_path.resolve(strict=False) + base_resolved = base_dir.resolve(strict=False) return resolved.is_relative_to(base_resolved) except (OSError, ValueError): return False From c64d8cd998982d2b1d91769f6b7759d85d82eeda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Mon, 20 Apr 2026 14:08:50 +0900 Subject: [PATCH 4/4] style: format backup importer --- astrbot/core/backup/importer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index a5a60e749e..e994242a88 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -59,8 +59,6 @@ def _get_major_version(version_str: str) -> str: return "0.0" - - def _validate_path_within(target_path: Path, base_dir: Path) -> bool: """Validate that target_path is within base_dir after resolving symlinks. @@ -74,6 +72,7 @@ def _validate_path_within(target_path: Path, base_dir: Path) -> bool: except (OSError, ValueError): return False + CMD_CONFIG_FILE_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json") KB_PATH = get_astrbot_knowledge_base_path() DEFAULT_PLATFORM_STATS_INVALID_COUNT_WARN_LIMIT = 5