From 178797fd91ad50d26e8a06aec4770960f5052ba6 Mon Sep 17 00:00:00 2001 From: tuanaiseo Date: Tue, 7 Apr 2026 08:32:25 +0700 Subject: [PATCH] fix(security): path traversal bypass in `safe_join_path` prefix c (#820) The path safety check uses `normalized_directory.startswith(os.path.normpath(safe_root))`, which can be bypassed by sibling paths sharing the same prefix (e.g., `/safe/root2` starts with `/safe/root`). This may permit access outside the intended root. Affected files: file_utils.py Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com> --- xiaomusic/utils/file_utils.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/xiaomusic/utils/file_utils.py b/xiaomusic/utils/file_utils.py index 4f5c0b9..f9a868e 100644 --- a/xiaomusic/utils/file_utils.py +++ b/xiaomusic/utils/file_utils.py @@ -83,13 +83,15 @@ def safe_join_path(safe_root: str, directory: str) -> str: Raises: ValueError: 如果路径不在安全根目录内 """ - directory = os.path.join(safe_root, directory) - # Normalize the directory path - normalized_directory = os.path.normpath(directory) - # Ensure the directory is within the safe root - if not normalized_directory.startswith(os.path.normpath(safe_root)): - raise ValueError(f"Access to directory '{directory}' is not allowed.") - return normalized_directory + joined_path = os.path.join(safe_root, directory) + real_safe_root = os.path.realpath(safe_root) + real_directory = os.path.realpath(joined_path) + try: + if os.path.commonpath([real_directory, real_safe_root]) != real_safe_root: + raise ValueError(f"Access to directory '{joined_path}' is not allowed.") + except ValueError as e: + raise ValueError(f"Access to directory '{joined_path}' is not allowed.") from e + return real_directory def _longest_common_prefix(file_names: list) -> str: