Skip to content

Commit 82eeb8e

Browse files
committed
fix: Port CVE-2024-32982 path traversal fix to v3.0 (#3524)
* Backport static files path traversal fix
1 parent 3bd0946 commit 82eeb8e

File tree

3 files changed

+43
-10
lines changed

3 files changed

+43
-10
lines changed

litestar/static_files.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
from os.path import commonpath
45
from pathlib import Path, PurePath
56
from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence
@@ -83,7 +84,7 @@ def create_static_files_router(
8384
if file_system is None:
8485
file_system = BaseLocalFileSystem()
8586

86-
directories = list(directories)
87+
directories = tuple(os.path.normpath(Path(p).resolve() if resolve_symlinks else Path(p)) for p in directories)
8788

8889
_validate_config(path=path, directories=directories, file_system=file_system)
8990
path = normalize_path(path)
@@ -225,19 +226,26 @@ async def _get_fs_info(
225226
try:
226227
joined_path = Path(directory, file_path)
227228
file_info = await adapter.info(joined_path)
228-
if file_info and commonpath([str(directory), file_info["name"], joined_path]) == str(directory):
229+
normalized_file_path = os.path.normpath(joined_path)
230+
directory_path = str(directory)
231+
if (
232+
file_info
233+
and commonpath([directory_path, file_info["name"], joined_path]) == directory_path
234+
and os.path.commonpath([directory, normalized_file_path]) == directory_path
235+
and (file_info := await adapter.info(joined_path))
236+
):
229237
return joined_path, file_info
230238
except FileNotFoundError:
231239
continue
232240
return None, None
233241

234242

235-
def _validate_config(path: str, directories: list[PathType], file_system: Any) -> None:
243+
def _validate_config(path: str, directories: tuple[PathType, ...], file_system: Any) -> None:
236244
if not path:
237-
raise ImproperlyConfiguredException("path must be a non-zero length string,")
245+
raise ImproperlyConfiguredException("path must be a non-zero length string")
238246

239247
if not directories or not any(bool(d) for d in directories):
240-
raise ImproperlyConfiguredException("directories must include at least one path.")
248+
raise ImproperlyConfiguredException("directories must include at least one path")
241249

242250
if "{" in path:
243251
raise ImproperlyConfiguredException("path parameters are not supported for static files")

tests/unit/test_static_files/test_file_serving_resolution.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import pytest
1010

1111
from litestar import MediaType, get
12-
from litestar.static_files import create_static_files_router
12+
from litestar.file_system import FileSystemAdapter
13+
from litestar.static_files import _get_fs_info, create_static_files_router
1314
from litestar.status_codes import HTTP_200_OK
1415
from litestar.testing import create_test_client
1516

@@ -251,3 +252,29 @@ def test_resolve_symlinks(tmp_path: Path, resolve: bool) -> None:
251252
assert client.get("/test.txt").status_code == 404
252253
else:
253254
assert client.get("/test.txt").status_code == 200
255+
256+
257+
async def test_staticfiles_get_fs_info_no_access_to_non_static_directory(
258+
tmp_path: Path,
259+
file_system: FileSystemProtocol,
260+
) -> None:
261+
assets = tmp_path / "assets"
262+
assets.mkdir()
263+
index = tmp_path / "index.html"
264+
index.write_text("content", "utf-8")
265+
path, info = await _get_fs_info([assets], "../index.html", adapter=FileSystemAdapter(file_system))
266+
assert path is None
267+
assert info is None
268+
269+
270+
async def test_staticfiles_get_fs_info_no_access_to_non_static_file_with_prefix(
271+
tmp_path: Path,
272+
file_system: FileSystemProtocol,
273+
) -> None:
274+
static = tmp_path / "static"
275+
static.mkdir()
276+
private_file = tmp_path / "staticsecrets.env"
277+
private_file.write_text("content", "utf-8")
278+
path, info = await _get_fs_info([static], "../staticsecrets.env", adapter=FileSystemAdapter(file_system))
279+
assert path is None
280+
assert info is None

tests/unit/test_static_files/test_static_files_validation.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from pathlib import Path
2-
from typing import List
32

43
import pytest
54

@@ -10,10 +9,9 @@
109
from litestar.testing import create_test_client
1110

1211

13-
@pytest.mark.parametrize("directories", [[], [""]])
14-
def test_validation_of_directories(directories: List[str]) -> None:
12+
def test_validation_of_directories() -> None:
1513
with pytest.raises(ImproperlyConfiguredException):
16-
create_static_files_router(path="/static", directories=directories)
14+
create_static_files_router(path="/static", directories=[])
1715

1816

1917
def test_validation_of_path(tmpdir: "Path") -> None:

0 commit comments

Comments
 (0)