Skip to content

Commit

Permalink
fix: Port CVE-2024-32982 path traversal fix to v3.0 (#3524)
Browse files Browse the repository at this point in the history
* Backport static files path traversal fix
  • Loading branch information
provinzkraut committed May 25, 2024
1 parent 15f2e17 commit 8aae5d8
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 10 deletions.
18 changes: 13 additions & 5 deletions litestar/static_files.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
29 changes: 28 additions & 1 deletion tests/unit/test_static_files/test_file_serving_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
6 changes: 2 additions & 4 deletions tests/unit/test_static_files/test_static_files_validation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
from typing import List

import pytest

Expand All @@ -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:
Expand Down

0 comments on commit 8aae5d8

Please sign in to comment.