Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/common_utils/path_utils.py: 50%
18 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2Safe filesystem path construction for user-controlled inputs.
4Use safe_join() instead of os.path.join() whenever a path component
5comes from user input (request parameters, uploaded filenames, etc.)
6to prevent directory traversal attacks.
7"""
9import os
10from typing import Final
13def safe_join(base_dir: str, *parts: str) -> str:
14 """
15 Join path components and verify the result stays within base_dir.
17 Resolves symlinks and '..' sequences, then checks the final path
18 is a descendant of base_dir. Raises ValueError if traversal is
19 detected.
21 Args:
22 base_dir: The trusted base directory.
23 *parts: User-controlled path components to append.
25 Returns:
26 The resolved absolute path as a string.
28 Raises:
29 ValueError: If the resolved path escapes base_dir.
30 """
31 for part in parts:
32 if "\x00" in part: 32 ↛ 33line 32 didn't jump to line 33 because the condition on line 32 was never true
33 raise ValueError("Path contains null byte")
34 base: Final = os.path.realpath(base_dir)
35 resolved: Final = os.path.realpath(os.path.join(base, *parts))
36 if not (resolved.startswith(base + os.sep) or resolved == base): 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true
37 raise ValueError(f"Path {resolved!r} escapes base directory {base!r}")
38 return resolved
41def safe_filename(filename: str) -> str:
42 """
43 Extract a safe filename from a user-supplied path.
45 Strips all directory components (both Unix and Windows separators),
46 returning only the final name. Use this for uploaded file names
47 before writing to disk.
49 Args:
50 filename: User-supplied filename (may contain path separators).
52 Returns:
53 The basename only, with no directory components.
55 Raises:
56 ValueError: If the resulting filename is empty or contains null bytes.
57 """
58 if "\x00" in filename:
59 raise ValueError("Filename contains null byte")
60 # Normalize backslash separators for cross-platform safety
61 name: Final = filename.replace("\\", "/").rsplit("/", 1)[-1]
62 if not name or name in (".", ".."):
63 raise ValueError("Empty or unsafe filename")
64 return name