@@ -64,6 +64,70 @@ def test_file_inside_allowed_root(self, tmp_path: Path) -> None:
6464 result = resolve_path (str (f ), allowed_paths = [str (tmp_path )])
6565 assert result == f .resolve ()
6666
67+ def test_path_prefix_attack_blocked (self , tmp_path : Path ) -> None :
68+ # Regression for #672: str.startswith() would allow "/allowed_dir_suffix"
69+ # to pass when "/allowed_dir" is the allowed root.
70+ # Path.is_relative_to() correctly rejects this because the directory
71+ # boundary is respected (it's not a child path).
72+ allowed = tmp_path / "data"
73+ allowed .mkdir ()
74+ attack = tmp_path / "data_extra"
75+ attack .mkdir ()
76+ with pytest .raises (ApiError ) as exc_info :
77+ resolve_path (str (attack ), allowed_paths = [str (allowed )])
78+ assert exc_info .value .status_code == 403
79+ assert exc_info .value .error == "path_not_allowed"
80+
81+ def test_dotdot_traversal_via_subdirectory_blocked (self , tmp_path : Path ) -> None :
82+ # Ensure that a path with embedded ".." that would escape the root is blocked.
83+ allowed = tmp_path / "allowed"
84+ allowed .mkdir ()
85+ outside = tmp_path / "outside"
86+ outside .mkdir ()
87+ # Construct a path that goes through the allowed root and back out
88+ traversal = str (allowed ) + "/../outside"
89+ with pytest .raises (ApiError ) as exc_info :
90+ resolve_path (traversal , allowed_paths = [str (allowed )])
91+ assert exc_info .value .status_code == 403
92+
93+ def test_symlink_escaping_allowed_root_blocked (self , tmp_path : Path ) -> None :
94+ # A symlink inside the allowed root that points outside must be blocked.
95+ allowed = tmp_path / "allowed"
96+ allowed .mkdir ()
97+ outside = tmp_path / "secret"
98+ outside .mkdir ()
99+ # Create a symlink inside allowed that points to outside
100+ link = allowed / "escape_link"
101+ link .symlink_to (outside )
102+ with pytest .raises (ApiError ) as exc_info :
103+ resolve_path (str (link ), allowed_paths = [str (allowed )])
104+ assert exc_info .value .status_code == 403
105+
106+ def test_windows_drive_qualified_path_blocked (self , tmp_path : Path ) -> None :
107+ # Drive-qualified input must never be accepted when it is outside the
108+ # configured allowlist root (Windows and non-Windows behavior alike).
109+ allowed = tmp_path / "allowed"
110+ allowed .mkdir ()
111+ with pytest .raises (ApiError ) as exc_info :
112+ resolve_path (r"C:\Windows\System32" , allowed_paths = [str (allowed )])
113+ assert exc_info .value .status_code == 403
114+ assert exc_info .value .error == "path_not_allowed"
115+
116+ def test_unc_path_blocked_when_outside_allowlist (self , tmp_path : Path ) -> None :
117+ # UNC-style paths are treated as out-of-scope unless explicitly under an
118+ # allowed root; this guards Windows network-share escape cases.
119+ allowed = tmp_path / "allowed"
120+ allowed .mkdir ()
121+ with pytest .raises (ApiError ) as exc_info :
122+ resolve_path (r"\\server\share\secret.txt" , allowed_paths = [str (allowed )])
123+ assert exc_info .value .status_code == 403
124+ assert exc_info .value .error == "path_not_allowed"
125+
126+ def test_returns_path_object (self , tmp_path : Path ) -> None :
127+ result = resolve_path (str (tmp_path ), allowed_paths = [str (tmp_path )])
128+ assert isinstance (result , Path )
129+ assert result .is_absolute ()
130+
67131
68132# ---------------------------------------------------------------------------
69133# is_hidden
0 commit comments