diff --git a/docs/release-notes/changelog.rst b/docs/release-notes/changelog.rst index 833bd2d77e..4da06614ff 100644 --- a/docs/release-notes/changelog.rst +++ b/docs/release-notes/changelog.rst @@ -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 ------- diff --git a/pyproject.toml b/pyproject.toml index 87047e32a8..1d299c8cb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,14 +2,21 @@ name = "starlite" version = "1.51.15" description = "Performant, light and flexible ASGI API Framework" -authors = ["Na'aman Hirschfeld "] +authors = [ + "Janek Nouvertné ", + "Cody Fincher ", + "Peter Schutt ", + "Jacob Coffee ", + "Na'aman Hirschfeld ", +] maintainers = [ - "Cody Fincher ", - "Janek Nouvertné ", - "Visakh Unnikrishnan ", - "Alc " + "Litestar Developers ", + "Cody Fincher ", + "Jacob Coffee ", + "Janek Nouvertné ", + "Peter Schutt ", + "Visakh Unnikrishnan ", + "Alc " ] license = "MIT" readme = "README.md" diff --git a/starlite/static_files/base.py b/starlite/static_files/base.py index 42337c7fcb..1cd89d6efb 100644 --- a/starlite/static_files/base.py +++ b/starlite/static_files/base.py @@ -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": diff --git a/tests/static_files/test_file_serving_resolution.py b/tests/static_files/test_file_serving_resolution.py index 13cf665f23..f9a30d5a9c 100644 --- a/tests/static_files/test_file_serving_resolution.py +++ b/tests/static_files/test_file_serving_resolution.py @@ -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