diff --git a/litestar/static_files.py b/litestar/static_files.py index 507c691f0a..d8ee22c0ac 100644 --- a/litestar/static_files.py +++ b/litestar/static_files.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from os.path import commonpath from pathlib import Path, PurePath from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence @@ -83,7 +84,7 @@ def create_static_files_router( if file_system is None: file_system = BaseLocalFileSystem() - directories = list(directories) + directories = tuple(os.path.normpath(Path(p).resolve() if resolve_symlinks else Path(p)) for p in directories) _validate_config(path=path, directories=directories, file_system=file_system) path = normalize_path(path) @@ -225,19 +226,26 @@ async def _get_fs_info( try: joined_path = Path(directory, file_path) file_info = await adapter.info(joined_path) - if file_info and commonpath([str(directory), file_info["name"], joined_path]) == str(directory): + normalized_file_path = os.path.normpath(joined_path) + directory_path = str(directory) + if ( + file_info + and commonpath([directory_path, file_info["name"], joined_path]) == directory_path + and os.path.commonpath([directory, normalized_file_path]) == directory_path + and (file_info := await adapter.info(joined_path)) + ): return joined_path, file_info except FileNotFoundError: continue return None, None -def _validate_config(path: str, directories: list[PathType], file_system: Any) -> None: +def _validate_config(path: str, directories: tuple[PathType, ...], file_system: Any) -> None: if not path: - raise ImproperlyConfiguredException("path must be a non-zero length string,") + raise ImproperlyConfiguredException("path must be a non-zero length string") if not directories or not any(bool(d) for d in directories): - raise ImproperlyConfiguredException("directories must include at least one path.") + raise ImproperlyConfiguredException("directories must include at least one path") if "{" in path: raise ImproperlyConfiguredException("path parameters are not supported for static files") diff --git a/tests/unit/test_static_files/test_file_serving_resolution.py b/tests/unit/test_static_files/test_file_serving_resolution.py index 731fa18dea..798b47f20d 100644 --- a/tests/unit/test_static_files/test_file_serving_resolution.py +++ b/tests/unit/test_static_files/test_file_serving_resolution.py @@ -9,7 +9,8 @@ import pytest from litestar import MediaType, get -from litestar.static_files import create_static_files_router +from litestar.file_system import FileSystemAdapter +from litestar.static_files import _get_fs_info, create_static_files_router from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client @@ -251,3 +252,29 @@ def test_resolve_symlinks(tmp_path: Path, resolve: bool) -> None: assert client.get("/test.txt").status_code == 404 else: assert client.get("/test.txt").status_code == 200 + + +async def test_staticfiles_get_fs_info_no_access_to_non_static_directory( + tmp_path: Path, + file_system: FileSystemProtocol, +) -> None: + assets = tmp_path / "assets" + assets.mkdir() + index = tmp_path / "index.html" + index.write_text("content", "utf-8") + path, info = await _get_fs_info([assets], "../index.html", adapter=FileSystemAdapter(file_system)) + assert path is None + assert info is None + + +async def test_staticfiles_get_fs_info_no_access_to_non_static_file_with_prefix( + tmp_path: Path, + file_system: FileSystemProtocol, +) -> None: + static = tmp_path / "static" + static.mkdir() + private_file = tmp_path / "staticsecrets.env" + private_file.write_text("content", "utf-8") + path, info = await _get_fs_info([static], "../staticsecrets.env", adapter=FileSystemAdapter(file_system)) + assert path is None + assert info is None diff --git a/tests/unit/test_static_files/test_static_files_validation.py b/tests/unit/test_static_files/test_static_files_validation.py index 6f696016fa..57ef6a4b59 100644 --- a/tests/unit/test_static_files/test_static_files_validation.py +++ b/tests/unit/test_static_files/test_static_files_validation.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import List import pytest @@ -10,10 +9,9 @@ from litestar.testing import create_test_client -@pytest.mark.parametrize("directories", [[], [""]]) -def test_validation_of_directories(directories: List[str]) -> None: +def test_validation_of_directories() -> None: with pytest.raises(ImproperlyConfiguredException): - create_static_files_router(path="/static", directories=directories) + create_static_files_router(path="/static", directories=[]) def test_validation_of_path(tmpdir: "Path") -> None: