Coverage for tests / unit / security / test_path_traversal.py: 99%
134 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for path traversal prevention.
3These tests verify that the path validation functions properly prevent
4path traversal attacks that could access files outside the project root.
5"""
7from __future__ import annotations
9import os
10from pathlib import Path
12import pytest
13from assertpy import assert_that
15from lintro.utils.path_utils import (
16 normalize_file_path_for_display,
17 validate_safe_path,
18)
20# =============================================================================
21# Tests for validate_safe_path function
22# =============================================================================
25def test_validate_safe_path_relative_path_within_project(tmp_path: Path) -> None:
26 """Verify relative path within project is safe.
28 Args:
29 tmp_path: Pytest fixture providing temporary directory.
30 """
31 test_file = tmp_path / "src" / "file.py"
32 test_file.parent.mkdir(parents=True, exist_ok=True)
33 test_file.touch()
35 result = validate_safe_path(str(test_file), base_dir=tmp_path)
36 assert_that(result).is_true()
39def test_validate_safe_path_dot_relative_path_is_safe(tmp_path: Path) -> None:
40 """Verify ./relative paths are safe.
42 Args:
43 tmp_path: Pytest fixture providing temporary directory.
44 """
45 test_file = tmp_path / "file.py"
46 test_file.touch()
48 old_cwd = os.getcwd()
49 try:
50 os.chdir(tmp_path)
51 result = validate_safe_path("./file.py")
52 assert_that(result).is_true()
53 finally:
54 os.chdir(old_cwd)
57def test_validate_safe_path_traversal_single_level_blocked(tmp_path: Path) -> None:
58 """Verify single-level path traversal is blocked.
60 Args:
61 tmp_path: Pytest fixture providing temporary directory.
62 """
63 result = validate_safe_path("../outside.txt", base_dir=tmp_path)
64 assert_that(result).is_false()
67def test_validate_safe_path_traversal_multiple_levels_blocked(tmp_path: Path) -> None:
68 """Verify multi-level path traversal is blocked.
70 Args:
71 tmp_path: Pytest fixture providing temporary directory.
72 """
73 result = validate_safe_path("../../../etc/passwd", base_dir=tmp_path)
74 assert_that(result).is_false()
77def test_validate_safe_path_traversal_encoded_blocked(tmp_path: Path) -> None:
78 """Verify path with traversal in middle is blocked.
80 Args:
81 tmp_path: Pytest fixture providing temporary directory.
82 """
83 result = validate_safe_path("subdir/../../outside.txt", base_dir=tmp_path)
84 assert_that(result).is_false()
87def test_validate_safe_path_absolute_path_outside_project_blocked(
88 tmp_path: Path,
89) -> None:
90 """Verify absolute path outside project is blocked.
92 Args:
93 tmp_path: Pytest fixture providing temporary directory.
94 """
95 result = validate_safe_path("/etc/passwd", base_dir=tmp_path)
96 assert_that(result).is_false()
99def test_validate_safe_path_absolute_path_inside_project_allowed(
100 tmp_path: Path,
101) -> None:
102 """Verify absolute path inside project is allowed.
104 Args:
105 tmp_path: Pytest fixture providing temporary directory.
106 """
107 test_file = tmp_path / "inside.txt"
108 test_file.touch()
110 result = validate_safe_path(str(test_file), base_dir=tmp_path)
111 assert_that(result).is_true()
114def test_validate_safe_path_symlink_escape_blocked(tmp_path: Path) -> None:
115 """Verify symlink that points outside project is blocked.
117 Args:
118 tmp_path: Pytest fixture providing temporary directory.
119 """
120 link_path = tmp_path / "evil_link"
121 try:
122 link_path.symlink_to("/etc")
123 result = validate_safe_path(str(link_path / "passwd"), base_dir=tmp_path)
124 assert_that(result).is_false()
125 except OSError:
126 pytest.skip("Symlinks not supported on this platform")
129def test_validate_safe_path_uses_cwd_when_no_base_dir(tmp_path: Path) -> None:
130 """Verify function uses cwd when base_dir not specified.
132 Args:
133 tmp_path: Pytest fixture providing temporary directory.
134 """
135 old_cwd = os.getcwd()
136 try:
137 os.chdir(tmp_path)
138 test_file = tmp_path / "file.py"
139 test_file.touch()
141 result = validate_safe_path("./file.py")
142 assert_that(result).is_true()
144 # Path traversal should be blocked
145 result = validate_safe_path("../outside.txt")
146 assert_that(result).is_false()
147 finally:
148 os.chdir(old_cwd)
151def test_validate_safe_path_empty_path_behavior(tmp_path: Path) -> None:
152 """Verify empty path behavior - resolves relative to cwd, not base_dir.
154 Args:
155 tmp_path: Pytest fixture providing temporary directory.
156 """
157 old_cwd = os.getcwd()
158 try:
159 os.chdir(tmp_path)
160 # Empty string resolves to cwd, which equals base_dir when chdir'd
161 result = validate_safe_path("", base_dir=tmp_path)
162 assert_that(result).is_true()
163 finally:
164 os.chdir(old_cwd)
167def test_validate_safe_path_current_dir_is_safe(tmp_path: Path) -> None:
168 """Verify current directory path is safe when cwd matches base.
170 Args:
171 tmp_path: Pytest fixture providing temporary directory.
172 """
173 old_cwd = os.getcwd()
174 try:
175 os.chdir(tmp_path)
176 # "." resolves to cwd, which equals base_dir when chdir'd
177 result = validate_safe_path(".", base_dir=tmp_path)
178 assert_that(result).is_true()
179 finally:
180 os.chdir(old_cwd)
183def test_validate_safe_path_deeply_nested_path_is_safe(tmp_path: Path) -> None:
184 """Verify deeply nested path within project is safe.
186 Args:
187 tmp_path: Pytest fixture providing temporary directory.
188 """
189 deep_path = tmp_path / "a" / "b" / "c" / "d" / "e" / "file.py"
190 deep_path.parent.mkdir(parents=True, exist_ok=True)
191 deep_path.touch()
193 result = validate_safe_path(str(deep_path), base_dir=tmp_path)
194 assert_that(result).is_true()
197# =============================================================================
198# Tests for normalize_file_path_for_display function
199# =============================================================================
202def test_normalize_file_path_for_display_relative_path_gets_dot_prefix(
203 tmp_path: Path,
204) -> None:
205 """Verify relative path gets ./ prefix for consistency.
207 Args:
208 tmp_path: Pytest fixture providing temporary directory.
209 """
210 old_cwd = os.getcwd()
211 try:
212 os.chdir(tmp_path)
213 test_file = tmp_path / "file.py"
214 test_file.touch()
216 result = normalize_file_path_for_display("file.py")
217 assert_that(result).starts_with("./")
218 finally:
219 os.chdir(old_cwd)
222def test_normalize_file_path_for_display_already_prefixed_path_unchanged(
223 tmp_path: Path,
224) -> None:
225 """Verify path already starting with ./ isn't double-prefixed.
227 Args:
228 tmp_path: Pytest fixture providing temporary directory.
229 """
230 old_cwd = os.getcwd()
231 try:
232 os.chdir(tmp_path)
233 test_file = tmp_path / "file.py"
234 test_file.touch()
236 result = normalize_file_path_for_display("./file.py")
237 assert_that(result).is_equal_to("./file.py")
238 finally:
239 os.chdir(old_cwd)
242def test_normalize_file_path_for_display_empty_path_returned_as_is() -> None:
243 """Verify empty path is returned unchanged."""
244 result = normalize_file_path_for_display("")
245 assert_that(result).is_equal_to("")
248def test_normalize_file_path_for_display_whitespace_path_returned_as_is() -> None:
249 """Verify whitespace-only path is returned unchanged."""
250 result = normalize_file_path_for_display(" ")
251 assert_that(result).is_equal_to(" ")
254def test_normalize_file_path_for_display_absolute_path_inside_project(
255 tmp_path: Path,
256) -> None:
257 """Verify absolute path inside project is relativized.
259 Args:
260 tmp_path: Pytest fixture providing temporary directory.
261 """
262 old_cwd = os.getcwd()
263 try:
264 os.chdir(tmp_path)
265 test_file = tmp_path / "src" / "file.py"
266 test_file.parent.mkdir(parents=True, exist_ok=True)
267 test_file.touch()
269 result = normalize_file_path_for_display(str(test_file))
270 assert_that(result).starts_with("./")
271 assert_that(result).contains("src")
272 assert_that(result).contains("file.py")
273 finally:
274 os.chdir(old_cwd)
277def test_normalize_file_path_for_display_path_outside_project_returns_relative_with_dotdot(
278 tmp_path: Path,
279) -> None:
280 """Verify path outside project returns path with .. prefix.
282 Args:
283 tmp_path: Pytest fixture providing temporary directory.
284 """
285 old_cwd = os.getcwd()
286 try:
287 project_dir = tmp_path / "project"
288 project_dir.mkdir()
289 os.chdir(project_dir)
291 outside_file = tmp_path / "outside.txt"
292 outside_file.touch()
294 result = normalize_file_path_for_display(str(outside_file))
295 assert_that(result).contains("..")
296 finally:
297 os.chdir(old_cwd)
300def test_normalize_file_path_for_display_nested_path_normalized(tmp_path: Path) -> None:
301 """Verify nested path is properly normalized.
303 Args:
304 tmp_path: Pytest fixture providing temporary directory.
305 """
306 old_cwd = os.getcwd()
307 try:
308 os.chdir(tmp_path)
309 test_file = tmp_path / "src" / "utils" / "helper.py"
310 test_file.parent.mkdir(parents=True, exist_ok=True)
311 test_file.touch()
313 result = normalize_file_path_for_display(str(test_file))
314 assert_that(result).is_equal_to("./src/utils/helper.py")
315 finally:
316 os.chdir(old_cwd)