VYPR
High severity8.2NVD Advisory· Published May 6, 2024· Updated Apr 15, 2026

CVE-2024-32982

CVE-2024-32982

Description

Litestar and Starlite is an Asynchronous Server Gateway Interface (ASGI) framework. Prior to 2.8.3, 2.7.2, and 2.6.4, a Local File Inclusion (LFI) vulnerability has been discovered in the static file serving component of LiteStar. This vulnerability allows attackers to exploit path traversal flaws, enabling unauthorized access to sensitive files outside the designated directories. Such access can lead to the disclosure of sensitive information or potentially compromise the server. The vulnerability is located in the file path handling mechanism within the static content serving function, specifically at litestar/static_files/base.py. This vulnerability is fixed in versions 2.8.3, 2.7.2, and 2.6.4.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
litestarPyPI
>= 2.8.0, < 2.8.32.8.3
starlitePyPI
>= 1.37.0, < 1.51.161.51.16
litestarPyPI
>= 2.7.0, < 2.7.22.7.2
litestarPyPI
>= 2.0.0, < 2.6.42.6.4

Patches

5
57e706e7effd

Merge pull request from GHSA-83pv-qr33-2vcf

https://github.com/litestar-org/litestarJacob CoffeeMay 6, 2024via ghsa
4 files changed · +130 8
  • litestar/static_files/base.py+15 5 modified
    @@ -1,6 +1,7 @@
    +# ruff: noqa: PTH118
     from __future__ import annotations
     
    -from os.path import commonpath
    +import os.path
     from pathlib import Path
     from typing import TYPE_CHECKING, Literal, Sequence
     
    @@ -12,7 +13,6 @@
     
     __all__ = ("StaticFiles",)
     
    -
     if TYPE_CHECKING:
         from litestar.types import Receive, Scope, Send
         from litestar.types.composite_types import PathType
    @@ -45,7 +45,9 @@ def __init__(
                 headers: Headers that will be sent with every response.
             """
             self.adapter = FileSystemAdapter(file_system)
    -        self.directories = tuple(Path(p).resolve() if resolve_symlinks else Path(p) for p in directories)
    +        self.directories = tuple(
    +            os.path.normpath(Path(p).resolve() if resolve_symlinks else Path(p)) for p in directories
    +        )
             self.is_html_mode = is_html_mode
             self.send_as_attachment = send_as_attachment
             self.headers = headers
    @@ -55,6 +57,12 @@ async def get_fs_info(
         ) -> tuple[Path, FileInfo] | tuple[None, None]:
             """Return the resolved path and a :class:`stat_result <os.stat_result>`.
     
    +        .. versionchanged:: 2.8.3
    +
    +            Prevent `CVE-2024-32982 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-32982>`_
    +            by ensuring that the resolved path is within the configured directory as part of `advisory
    +            GHSA-83pv-qr33-2vcf <https://github.com/advisories/GHSA-83pv-qr33-2vcf>`_.
    +
             Args:
                 directories: A list of directory paths.
                 file_path: A file path to resolve
    @@ -66,8 +74,10 @@ async def get_fs_info(
             for directory in directories:
                 try:
                     joined_path = Path(directory, file_path)
    -                file_info = await self.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)
    +                if os.path.commonpath([directory, normalized_file_path]) == str(directory) and (
    +                    file_info := await self.adapter.info(joined_path)
    +                ):
                         return joined_path, file_info
                 except FileNotFoundError:
                     continue
    
  • tests/e2e/test_routing/test_path_resolution.py+48 0 modified
    @@ -360,3 +360,51 @@ async def pathfinder(path: Optional[Path]) -> str:
     
         assert httpx.get("http://127.0.0.1:9999/").text == "None"
         assert httpx.get("http://127.0.0.1:9999/something").text == "/something"
    +
    +
    +@pytest.mark.parametrize(
    +    "server_command",
    +    [
    +        pytest.param(["uvicorn", "app:app", "--port", "9999", "--root-path", "/test"], id="uvicorn"),
    +        pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999", "--root-path", "/test"], id="hypercorn"),
    +        pytest.param(["daphne", "app:app", "--port", "9999", "--root-path", "/test"], id="daphne"),
    +    ],
    +)
    +@pytest.mark.xdist_group("live_server_test")
    +@pytest.mark.server_integration
    +def test_no_path_traversal_from_static_directory(
    +    tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None]
    +) -> None:
    +    import http.client
    +
    +    static = tmp_path / "static"
    +    static.mkdir()
    +    (static / "index.html").write_text("Hello, World!")
    +
    +    app = """
    +from pathlib import Path
    +from litestar import Litestar
    +from litestar.static_files import create_static_files_router
    +import uvicorn
    +
    +app = Litestar(
    +    route_handlers=[
    +        create_static_files_router(path="/static", directories=["static"]),
    +    ],
    +)
    +    """
    +
    +    def send_request(host: str, port: int, path: str) -> http.client.HTTPResponse:
    +        connection = http.client.HTTPConnection(host, port)
    +        connection.request("GET", path)
    +        resp = connection.getresponse()
    +        connection.close()
    +        return resp
    +
    +    run_server(app, server_command)
    +
    +    response = send_request("127.0.0.1", 9999, "/static/index.html")
    +    assert response.status == 200
    +
    +    response = send_request("127.0.0.1", 9999, "/static/../app.py")
    +    assert response.status == 404
    
  • tests/unit/test_static_files/test_file_serving_resolution.py+29 1 modified
    @@ -10,7 +10,7 @@
     from typing_extensions import TypeAlias
     
     from litestar import MediaType, Router, get
    -from litestar.static_files import StaticFilesConfig, create_static_files_router
    +from litestar.static_files import StaticFiles, StaticFilesConfig, create_static_files_router
     from litestar.status_codes import HTTP_200_OK
     from litestar.testing import create_test_client
     from tests.unit.test_static_files.conftest import MakeConfig
    @@ -295,3 +295,31 @@ 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")
    +    static_files = StaticFiles(is_html_mode=False, directories=[assets], file_system=file_system)
    +    path, info = await static_files.get_fs_info([assets], "../index.html")
    +    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")
    +    static_files = StaticFiles(is_html_mode=False, directories=[static], file_system=file_system)
    +    path, info = await static_files.get_fs_info([static], "../staticsecrets.env")
    +    assert path is None
    +    assert info is None
    
  • tests/unit/test_static_files/test_static_files_validation.py+38 2 modified
    @@ -1,4 +1,6 @@
    -from typing import TYPE_CHECKING, Any, List
    +import asyncio
    +from pathlib import Path, PosixPath
    +from typing import TYPE_CHECKING, Any, List, cast
     
     import pytest
     
    @@ -9,7 +11,7 @@
     from litestar.testing import create_test_client
     
     if TYPE_CHECKING:
    -    from pathlib import Path
    +    from litestar.static_files import StaticFiles
     
     
     @pytest.mark.parametrize("directories", [[], [""]])
    @@ -113,3 +115,37 @@ def test_runtime_validation_of_request_method_create_handler(tmpdir: "Path", met
         with create_test_client(create_static_files_router(path="/static", directories=[tmpdir])) as client:
             response = client.request(method, "/static/test.txt")
             assert response.status_code == expected
    +
    +
    +def test_config_validation_of_path_prevents_directory_traversal(tmpdir: "Path") -> None:
    +    # Setup: Create a 'secret.txt' outside the static directory to simulate sensitive file
    +    secret_path = Path(tmpdir) / "../secret.txt"
    +    secret_path.write_text("This is a secret file.", encoding="utf-8")
    +
    +    # Setup: Create 'test.txt' inside the static directory
    +    test_file_path = Path(tmpdir) / "test.txt"
    +    test_file_path.write_text("This is a test file.", encoding="utf-8")
    +
    +    # Get StaticFiles handler
    +    config = StaticFilesConfig(path="/static", directories=[tmpdir])
    +    asgi_router = config.to_static_files_app()
    +    static_files_handler = cast("StaticFiles", asgi_router.fn)
    +
    +    # Resolve file path with the StaticFiles handler
    +    string_path = Path("../secret.txt").as_posix()
    +
    +    coroutine = static_files_handler.get_fs_info(directories=static_files_handler.directories, file_path=string_path)
    +    resolved_path, fs_info = asyncio.run(coroutine)
    +
    +    assert resolved_path is None  # Because the resolved path is outside the static directory
    +    assert fs_info is None  # Because the file doesn't exist, so there is no info
    +
    +    # Resolve file path with the StaticFiles handler
    +    string_path = Path("test.txt").as_posix()
    +
    +    coroutine = static_files_handler.get_fs_info(directories=static_files_handler.directories, file_path=string_path)
    +    resolved_path, fs_info = asyncio.run(coroutine)
    +
    +    expected_resolved_path = PosixPath(str(tmpdir / "test.txt"))
    +    assert resolved_path == expected_resolved_path  # Because the resolved path is inside the static directory
    +    assert fs_info is not None  # Because the file exists, so there is info
    
a07b79b84d87

Merge pull request from GHSA-83pv-qr33-2vcf

https://github.com/litestar-org/litestarJacob CoffeeMay 6, 2024via ghsa
4 files changed · +42 14
  • docs/release-notes/changelog.rst+6 0 modified
    @@ -1,6 +1,12 @@
     1.x Changelog
     =============
     
    +1.51.15
    +-------
    +
    +* Fix a security issue ([CVE-2024-32982](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-32982) where static files could allow users the ability to escape the configured static files directory
    +  and read arbitrary files on the filesystem.
    +
     1.51.11
     -------
     
    
  • pyproject.toml+14 7 modified
    @@ -2,14 +2,21 @@
     name = "starlite"
     version = "1.51.15"
     description = "Performant, light and flexible ASGI API Framework"
    -authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
    +authors = [
    +  "Janek Nouvertné <provinzkraut@posteo.de>",
    +  "Cody Fincher <cody.fincher@gmail.com>",
    +  "Peter Schutt <peter.github@proton.me>",
    +  "Jacob Coffee <jacob@z7x.org>",
    +  "Na'aman Hirschfeld <nhirschfeld@gmail.com>",
    +]
     maintainers = [
    -    "Cody Fincher <cody@litestar.dev>",
    -    "Janek Nouvertné <provinzkraut@litestar.dev",
    -    "Jacob Coffee <jacob@litestar.dev",
    -    "Peter Schutt <peter@litestar.dev>",
    -    "Visakh Unnikrishnan <guacs@litestar.dev>",
    -    "Alc <alc@litestar.dev>"
    +  "Litestar Developers <hello@litestar.dev>",
    +  "Cody Fincher <cody@litestar.dev>",
    +  "Jacob Coffee <jacob@litestar.dev>",
    +  "Janek Nouvertné <provinzkraut@litestar.dev>",
    +  "Peter Schutt <peter@litestar.dev>",
    +  "Visakh Unnikrishnan <guacs@litestar.dev>",
    +  "Alc <alc@litestar.dev>"
     ]
     license = "MIT"
     readme = "README.md"
    
  • starlite/static_files/base.py+9 7 modified
    @@ -1,4 +1,4 @@
    -from os.path import commonpath, join
    +import os.path
     from typing import TYPE_CHECKING, Literal, Sequence, Tuple, Union
     
     from starlite.enums import ScopeType
    @@ -35,7 +35,7 @@ def __init__(
                  ``attachment`` or ``inline``
             """
             self.adapter = FileSystemAdapter(file_system)
    -        self.directories = directories
    +        self.directories = [os.path.normpath(d) for d in directories]
             self.is_html_mode = is_html_mode
             self.send_as_attachment = send_as_attachment
     
    @@ -54,9 +54,11 @@ async def get_fs_info(
             """
             for directory in directories:
                 try:
    -                joined_path = join(directory, file_path)  # noqa: PL118
    -                file_info = await self.adapter.info(joined_path)
    -                if file_info and commonpath([str(directory), file_info["name"], joined_path]) == str(directory):
    +                joined_path = os.path.join(directory, file_path)
    +                normalized_file_path = os.path.normpath(joined_path)
    +                if os.path.commonpath([directory, normalized_file_path]) == str(directory) and (
    +                    file_info := await self.adapter.info(joined_path)
    +                ):
                         return joined_path, file_info
                 except FileNotFoundError:
                     continue
    @@ -78,7 +80,7 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No
     
             split_path = scope["path"].split("/")
             filename = split_path[-1]
    -        joined_path = join(*split_path)  # noqa: PL118
    +        joined_path = os.path.join(*split_path)  # noqa: PL118
             resolved_path, fs_info = await self.get_fs_info(directories=self.directories, file_path=joined_path)
             content_disposition_type: Literal["inline", "attachment"] = (
                 "attachment" if self.send_as_attachment else "inline"
    @@ -87,7 +89,7 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No
             if self.is_html_mode and fs_info and fs_info["type"] == "directory":
                 filename = "index.html"
                 resolved_path, fs_info = await self.get_fs_info(
    -                directories=self.directories, file_path=join(resolved_path or joined_path, filename)
    +                directories=self.directories, file_path=os.path.join(resolved_path or joined_path, filename)
                 )
     
             if fs_info and fs_info["type"] == "file":
    
  • tests/static_files/test_file_serving_resolution.py+13 0 modified
    @@ -1,5 +1,6 @@
     import gzip
     import mimetypes
    +from pathlib import Path
     from typing import TYPE_CHECKING
     
     import brotli
    @@ -8,6 +9,7 @@
     
     from starlite import MediaType, get
     from starlite.config import StaticFilesConfig
    +from starlite.static_files import StaticFiles
     from starlite.status_codes import HTTP_200_OK
     from starlite.testing import create_test_client
     from starlite.utils.file import BaseLocalFileSystem
    @@ -194,3 +196,14 @@ def test_static_files_content_disposition(tmpdir: "Path", send_as_attachment: bo
             response = client.get("/static/static_part/static/test.txt")
             assert response.status_code == HTTP_200_OK
             assert response.headers["content-disposition"].startswith(disposition)
    +
    +
    +async def test_staticfiles_get_fs_info_no_access_to_non_static_directory(tmp_path: Path,) -> None:
    +    assets = tmp_path / "assets"
    +    assets.mkdir()
    +    index = tmp_path / "index.html"
    +    index.write_text("content", "utf-8")
    +    static_files = StaticFiles(is_html_mode=False, directories=["static"], file_system=BaseLocalFileSystem())
    +    path, info = await static_files.get_fs_info([assets], "../index.html")
    +    assert path is None
    +    assert info is None
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.