From 2c3c3933f6ff824c6881940bcf5462c7a28a2bad Mon Sep 17 00:00:00 2001 From: starlet-dx <15929766099@163.com> Date: Fri, 11 Oct 2024 15:15:31 +0800 Subject: [PATCH] Fix CVE-2024-42367 --- CVE-2024-42367.patch | 198 +++++++++++++++++++++++++++++++++++++++++++ python-aiohttp.spec | 6 +- 2 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 CVE-2024-42367.patch diff --git a/CVE-2024-42367.patch b/CVE-2024-42367.patch new file mode 100644 index 0000000..2e1b426 --- /dev/null +++ b/CVE-2024-42367.patch @@ -0,0 +1,198 @@ +From f98240ad2279c3e97b65eddce40d37948f383416 Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Thu, 8 Aug 2024 11:19:28 -0500 +Subject: [PATCH] Do not follow symlinks for compressed file variants (#8652) + +Co-authored-by: Steve Repsher +(cherry picked from commit b0536ae6babf160105d4025ea87c02b9fa5629f1) +--- + CHANGES/8652.bugfix.rst | 1 + + aiohttp/web_fileresponse.py | 9 ++++++-- + tests/test_web_sendfile.py | 37 ++++++++++++++++++++------------- + tests/test_web_urldispatcher.py | 32 ++++++++++++++++++++++++++++ + 4 files changed, 63 insertions(+), 16 deletions(-) + create mode 100644 CHANGES/8652.bugfix.rst + +diff --git a/CHANGES/8652.bugfix.rst b/CHANGES/8652.bugfix.rst +new file mode 100644 +index 0000000..3a1003e +--- /dev/null ++++ b/CHANGES/8652.bugfix.rst +@@ -0,0 +1 @@ ++Fixed incorrectly following symlinks for compressed file variants -- by :user:`steverep`. +diff --git a/aiohttp/web_fileresponse.py b/aiohttp/web_fileresponse.py +index 6496ffa..acb0579 100644 +--- a/aiohttp/web_fileresponse.py ++++ b/aiohttp/web_fileresponse.py +@@ -2,6 +2,7 @@ import asyncio + import mimetypes + import os + import pathlib ++from stat import S_ISREG + from typing import ( # noqa + IO, + TYPE_CHECKING, +@@ -136,12 +137,16 @@ class FileResponse(StreamResponse): + if check_for_gzipped_file: + gzip_path = filepath.with_name(filepath.name + ".gz") + try: +- return gzip_path, gzip_path.stat(), True ++ st = gzip_path.lstat() ++ if S_ISREG(st.st_mode): ++ return gzip_path, st, True + except OSError: + # Fall through and try the non-gzipped file + pass + +- return filepath, filepath.stat(), False ++ st = filepath.lstat() ++ if S_ISREG(st.st_mode): ++ return filepath, st, False + + async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]: + loop = asyncio.get_event_loop() +diff --git a/tests/test_web_sendfile.py b/tests/test_web_sendfile.py +index 2817e08..da9e6ae 100644 +--- a/tests/test_web_sendfile.py ++++ b/tests/test_web_sendfile.py +@@ -1,10 +1,13 @@ + from pathlib import Path ++from stat import S_IFREG, S_IRUSR, S_IWUSR + from unittest import mock + + from aiohttp import hdrs + from aiohttp.test_utils import make_mocked_coro, make_mocked_request + from aiohttp.web_fileresponse import FileResponse + ++MOCK_MODE = S_IFREG | S_IRUSR | S_IWUSR ++ + + def test_using_gzip_if_header_present_and_file_available(loop) -> None: + request = make_mocked_request( +@@ -12,8 +15,9 @@ def test_using_gzip_if_header_present_and_file_available(loop) -> None: + ) + + gz_filepath = mock.create_autospec(Path, spec_set=True) +- gz_filepath.stat.return_value.st_size = 1024 +- gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291 ++ gz_filepath.lstat.return_value.st_size = 1024 ++ gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 ++ gz_filepath.lstat.return_value.st_mode = MOCK_MODE + + filepath = mock.create_autospec(Path, spec_set=True) + filepath.name = "logo.png" +@@ -33,14 +37,16 @@ def test_gzip_if_header_not_present_and_file_available(loop) -> None: + request = make_mocked_request("GET", "http://python.org/logo.png", headers={}) + + gz_filepath = mock.create_autospec(Path, spec_set=True) +- gz_filepath.stat.return_value.st_size = 1024 +- gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291 ++ gz_filepath.lstat.return_value.st_size = 1024 ++ gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 ++ gz_filepath.lstat.return_value.st_mode = MOCK_MODE + + filepath = mock.create_autospec(Path, spec_set=True) + filepath.name = "logo.png" + filepath.with_name.return_value = gz_filepath +- filepath.stat.return_value.st_size = 1024 +- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_size = 1024 ++ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_mode = MOCK_MODE + + file_sender = FileResponse(filepath) + file_sender._path = filepath +@@ -56,13 +62,14 @@ def test_gzip_if_header_not_present_and_file_not_available(loop) -> None: + request = make_mocked_request("GET", "http://python.org/logo.png", headers={}) + + gz_filepath = mock.create_autospec(Path, spec_set=True) +- gz_filepath.stat.side_effect = OSError(2, "No such file or directory") ++ gz_filepath.lstat.side_effect = OSError(2, "No such file or directory") + + filepath = mock.create_autospec(Path, spec_set=True) + filepath.name = "logo.png" + filepath.with_name.return_value = gz_filepath +- filepath.stat.return_value.st_size = 1024 +- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_size = 1024 ++ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_mode = MOCK_MODE + + file_sender = FileResponse(filepath) + file_sender._path = filepath +@@ -80,13 +87,14 @@ def test_gzip_if_header_present_and_file_not_available(loop) -> None: + ) + + gz_filepath = mock.create_autospec(Path, spec_set=True) +- gz_filepath.stat.side_effect = OSError(2, "No such file or directory") ++ gz_filepath.lstat.side_effect = OSError(2, "No such file or directory") + + filepath = mock.create_autospec(Path, spec_set=True) + filepath.name = "logo.png" + filepath.with_name.return_value = gz_filepath +- filepath.stat.return_value.st_size = 1024 +- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_size = 1024 ++ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_mode = MOCK_MODE + + file_sender = FileResponse(filepath) + file_sender._path = filepath +@@ -103,8 +111,9 @@ def test_status_controlled_by_user(loop) -> None: + + filepath = mock.create_autospec(Path, spec_set=True) + filepath.name = "logo.png" +- filepath.stat.return_value.st_size = 1024 +- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_size = 1024 ++ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 ++ filepath.lstat.return_value.st_mode = MOCK_MODE + + file_sender = FileResponse(filepath, status=203) + file_sender._path = filepath +diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py +index 0441890..62cb0de 100644 +--- a/tests/test_web_urldispatcher.py ++++ b/tests/test_web_urldispatcher.py +@@ -440,6 +440,38 @@ async def test_access_symlink_loop( + assert r.status == 404 + + ++async def test_access_compressed_file_as_symlink( ++ tmp_path: pathlib.Path, aiohttp_client: AiohttpClient ++) -> None: ++ """Test that compressed file variants as symlinks are ignored.""" ++ private_file = tmp_path / "private.txt" ++ private_file.write_text("private info") ++ www_dir = tmp_path / "www" ++ www_dir.mkdir() ++ gz_link = www_dir / "file.txt.gz" ++ gz_link.symlink_to(f"../{private_file.name}") ++ ++ app = web.Application() ++ app.router.add_static("/", www_dir) ++ client = await aiohttp_client(app) ++ ++ # Symlink should be ignored; response reflects missing uncompressed file. ++ resp = await client.get(f"/{gz_link.stem}", auto_decompress=False) ++ assert resp.status == 404 ++ resp.release() ++ ++ # Again symlin is ignored, and then uncompressed is served. ++ txt_file = gz_link.with_suffix("") ++ txt_file.write_text("public data") ++ resp = await client.get(f"/{txt_file.name}") ++ assert resp.status == 200 ++ assert resp.headers.get("Content-Encoding") is None ++ assert resp.content_type == "text/plain" ++ assert await resp.text() == "public data" ++ resp.release() ++ await client.close() ++ ++ + async def test_access_special_resource( + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient + ) -> None: +-- +2.33.0 + diff --git a/python-aiohttp.spec b/python-aiohttp.spec index b72e9b9..145006b 100644 --- a/python-aiohttp.spec +++ b/python-aiohttp.spec @@ -1,7 +1,7 @@ %global _empty_manifest_terminate_build 0 Name: python-aiohttp Version: 3.9.3 -Release: 4 +Release: 5 Summary: Async http client/server framework (asyncio) License: Apache 2 URL: https://github.com/aio-libs/aiohttp @@ -16,6 +16,7 @@ Patch2: CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dis Patch3: CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch # https://github.com/aio-libs/aiohttp/commit/9ba9a4e531599b9cb2f8cc80effbde40c7eab0bd Patch4: Fix-Python-parser-to-mark-responses-without-length-a.patch +Patch5: CVE-2024-42367.patch Requires: python3-attrs Requires: python3-charset-normalizer @@ -91,6 +92,9 @@ mv %{buildroot}/doclist.lst . %{_docdir}/* %changelog +* Fri Oct 11 2024 yaoxin - 3.9.3-5 +- Fix CVE-2024-42367 + * Mon May 06 2024 xiaozai - 3.9.3-4 - Fix Python parser to mark responses without length as closing