From c8cf19e9d793e13655b6c3dbed8cbcb07a360386 Mon Sep 17 00:00:00 2001 From: "moyai-berriai[bot]" <336287033+moyai-berriai[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:35:41 +0000 Subject: [PATCH] fix(paths): accept filesystem roots in containment checks --- litellm/litellm_core_utils/path_utils.py | 6 ++--- .../litellm_core_utils/test_path_utils.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/path_utils.py b/litellm/litellm_core_utils/path_utils.py index 3494a4c3fa0..6b2fe417097 100644 --- a/litellm/litellm_core_utils/path_utils.py +++ b/litellm/litellm_core_utils/path_utils.py @@ -33,7 +33,7 @@ def safe_join(base_dir: str, *parts: str) -> str: raise ValueError("Path contains null byte") base: Final = os.path.realpath(base_dir) resolved: Final = os.path.realpath(os.path.join(base, *parts)) - if not (resolved.startswith(base + os.sep) or resolved == base): + if not (resolved.startswith(os.path.join(base, "")) or resolved == base): raise ValueError(f"Path {resolved!r} escapes base directory {base!r}") return resolved @@ -50,7 +50,7 @@ def is_within(path: str, base_dir: str) -> bool: """True when path, with symlinks resolved, is base_dir or sits inside it.""" base: Final = os.path.realpath(base_dir) resolved: Final = os.path.realpath(path) - return resolved.startswith(base + os.sep) or resolved == base + return resolved.startswith(os.path.join(base, "")) or resolved == base def join_within(base_dir: str, *parts: str) -> str | None: @@ -65,7 +65,7 @@ def join_within(base_dir: str, *parts: str) -> str | None: return None base: Final = os.path.normpath(os.path.abspath(base_dir)) joined: Final = os.path.normpath(os.path.join(base, *parts)) - if not joined.startswith(base + os.sep): + if joined == base or not joined.startswith(os.path.join(base, "")): return None return joined diff --git a/tests/unit/litellm_core_utils/test_path_utils.py b/tests/unit/litellm_core_utils/test_path_utils.py index 480a6de2b08..6783b9b01b5 100644 --- a/tests/unit/litellm_core_utils/test_path_utils.py +++ b/tests/unit/litellm_core_utils/test_path_utils.py @@ -1,10 +1,32 @@ import os +from collections.abc import Callable +from pathlib import Path +from typing import Final import pytest from litellm.litellm_core_utils.path_utils import is_within, join_within, safe_filename, safe_join, try_safe_join +@pytest.mark.parametrize("join", (safe_join, try_safe_join, join_within)) +def test_join_accepts_filesystem_root(join: Callable[[str, str], str | None], tmp_path: Path) -> None: + root: Final = tmp_path.anchor + relative: Final = os.path.relpath(str(tmp_path / "file.yaml"), root) + assert join(root, relative) == str(tmp_path / "file.yaml") + + +def test_is_within_accepts_filesystem_root(tmp_path: Path) -> None: + assert is_within(str(tmp_path / "file.yaml"), tmp_path.anchor) + assert is_within(tmp_path.anchor, tmp_path.anchor) + + +@pytest.mark.parametrize("use_root", (True, False)) +@pytest.mark.parametrize("parts", ((), (".",), ("subdir", ".."))) +def test_join_within_rejects_base_itself(tmp_path: Path, use_root: bool, parts: tuple[str, ...]) -> None: + base: Final = tmp_path.anchor if use_root else str(tmp_path) + assert join_within(base, *parts) is None + + class TestSafeJoin: def test_normal_path(self, tmp_path): result = safe_join(str(tmp_path), "subdir", "file.yaml")