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

1"""Tests for path traversal prevention. 

2 

3These tests verify that the path validation functions properly prevent 

4path traversal attacks that could access files outside the project root. 

5""" 

6 

7from __future__ import annotations 

8 

9import os 

10from pathlib import Path 

11 

12import pytest 

13from assertpy import assert_that 

14 

15from lintro.utils.path_utils import ( 

16 normalize_file_path_for_display, 

17 validate_safe_path, 

18) 

19 

20# ============================================================================= 

21# Tests for validate_safe_path function 

22# ============================================================================= 

23 

24 

25def test_validate_safe_path_relative_path_within_project(tmp_path: Path) -> None: 

26 """Verify relative path within project is safe. 

27 

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() 

34 

35 result = validate_safe_path(str(test_file), base_dir=tmp_path) 

36 assert_that(result).is_true() 

37 

38 

39def test_validate_safe_path_dot_relative_path_is_safe(tmp_path: Path) -> None: 

40 """Verify ./relative paths are safe. 

41 

42 Args: 

43 tmp_path: Pytest fixture providing temporary directory. 

44 """ 

45 test_file = tmp_path / "file.py" 

46 test_file.touch() 

47 

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) 

55 

56 

57def test_validate_safe_path_traversal_single_level_blocked(tmp_path: Path) -> None: 

58 """Verify single-level path traversal is blocked. 

59 

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() 

65 

66 

67def test_validate_safe_path_traversal_multiple_levels_blocked(tmp_path: Path) -> None: 

68 """Verify multi-level path traversal is blocked. 

69 

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() 

75 

76 

77def test_validate_safe_path_traversal_encoded_blocked(tmp_path: Path) -> None: 

78 """Verify path with traversal in middle is blocked. 

79 

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() 

85 

86 

87def test_validate_safe_path_absolute_path_outside_project_blocked( 

88 tmp_path: Path, 

89) -> None: 

90 """Verify absolute path outside project is blocked. 

91 

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() 

97 

98 

99def test_validate_safe_path_absolute_path_inside_project_allowed( 

100 tmp_path: Path, 

101) -> None: 

102 """Verify absolute path inside project is allowed. 

103 

104 Args: 

105 tmp_path: Pytest fixture providing temporary directory. 

106 """ 

107 test_file = tmp_path / "inside.txt" 

108 test_file.touch() 

109 

110 result = validate_safe_path(str(test_file), base_dir=tmp_path) 

111 assert_that(result).is_true() 

112 

113 

114def test_validate_safe_path_symlink_escape_blocked(tmp_path: Path) -> None: 

115 """Verify symlink that points outside project is blocked. 

116 

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") 

127 

128 

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. 

131 

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() 

140 

141 result = validate_safe_path("./file.py") 

142 assert_that(result).is_true() 

143 

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) 

149 

150 

151def test_validate_safe_path_empty_path_behavior(tmp_path: Path) -> None: 

152 """Verify empty path behavior - resolves relative to cwd, not base_dir. 

153 

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) 

165 

166 

167def test_validate_safe_path_current_dir_is_safe(tmp_path: Path) -> None: 

168 """Verify current directory path is safe when cwd matches base. 

169 

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) 

181 

182 

183def test_validate_safe_path_deeply_nested_path_is_safe(tmp_path: Path) -> None: 

184 """Verify deeply nested path within project is safe. 

185 

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() 

192 

193 result = validate_safe_path(str(deep_path), base_dir=tmp_path) 

194 assert_that(result).is_true() 

195 

196 

197# ============================================================================= 

198# Tests for normalize_file_path_for_display function 

199# ============================================================================= 

200 

201 

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. 

206 

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() 

215 

216 result = normalize_file_path_for_display("file.py") 

217 assert_that(result).starts_with("./") 

218 finally: 

219 os.chdir(old_cwd) 

220 

221 

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. 

226 

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() 

235 

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) 

240 

241 

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("") 

246 

247 

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(" ") 

252 

253 

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. 

258 

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() 

268 

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) 

275 

276 

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. 

281 

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) 

290 

291 outside_file = tmp_path / "outside.txt" 

292 outside_file.touch() 

293 

294 result = normalize_file_path_for_display(str(outside_file)) 

295 assert_that(result).contains("..") 

296 finally: 

297 os.chdir(old_cwd) 

298 

299 

300def test_normalize_file_path_for_display_nested_path_normalized(tmp_path: Path) -> None: 

301 """Verify nested path is properly normalized. 

302 

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() 

312 

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)