From 15ee437fd4b5aa384bdc4c4e2536afaff66a7c65 Mon Sep 17 00:00:00 2001 From: yujingbo Date: Wed, 29 Oct 2025 09:12:18 +0800 Subject: [PATCH] fix CVE-2025-62727 (cherry picked from commit cb4b23ff1a9dd1f91c3ee4d4a15265e5592967e1) --- CVE-2025-62727.patch | 137 ++++++++++++++++++++++++++++++++++++++++++ python-starlette.spec | 6 +- 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 CVE-2025-62727.patch diff --git a/CVE-2025-62727.patch b/CVE-2025-62727.patch new file mode 100644 index 0000000..37c3bfb --- /dev/null +++ b/CVE-2025-62727.patch @@ -0,0 +1,137 @@ +From 4ea6e22b489ec388d6004cfbca52dd5b147127c5 Mon Sep 17 00:00:00 2001 +From: Marcelo Trylesinski +Date: Tue, 28 Oct 2025 18:14:01 +0100 +Subject: [PATCH] Merge commit from fork + +--- + starlette/responses.py | 46 ++++++++++++++++++++++++++++------------- + tests/test_responses.py | 28 +++++++++++++++++++++++++ + 2 files changed, 60 insertions(+), 14 deletions(-) + +diff --git a/starlette/responses.py b/starlette/responses.py +index 81e89fa..dbb087a 100644 +--- a/starlette/responses.py ++++ b/starlette/responses.py +@@ -4,7 +4,6 @@ import hashlib + import http.cookies + import json + import os +-import re + import stat + import typing + import warnings +@@ -283,9 +282,6 @@ class RangeNotSatisfiable(Exception): + self.max_size = max_size + + +-_RANGE_PATTERN = re.compile(r"(\d*)-(\d*)") +- +- + class FileResponse(Response): + chunk_size = 64 * 1024 + +@@ -443,8 +439,8 @@ class FileResponse(Response): + def _should_use_range(self, http_if_range: str) -> bool: + return http_if_range == self.headers["last-modified"] or http_if_range == self.headers["etag"] + +- @staticmethod +- def _parse_range_header(http_range: str, file_size: int) -> list[tuple[int, int]]: ++ @classmethod ++ def _parse_range_header(cls, http_range: str, file_size: int) -> list[tuple[int, int]]: + ranges: list[tuple[int, int]] = [] + try: + units, range_ = http_range.split("=", 1) +@@ -456,14 +452,7 @@ class FileResponse(Response): + if units != "bytes": + raise MalformedRangeHeader("Only support bytes range") + +- ranges = [ +- ( +- int(_[0]) if _[0] else file_size - int(_[1]), +- int(_[1]) + 1 if _[0] and _[1] and int(_[1]) < file_size else file_size, +- ) +- for _ in _RANGE_PATTERN.findall(range_) +- if _ != ("", "") +- ] ++ ranges = cls._parse_ranges(range_, file_size) + + if len(ranges) == 0: + raise MalformedRangeHeader("Range header: range must be requested") +@@ -495,6 +484,35 @@ class FileResponse(Response): + + return result + ++ @classmethod ++ def _parse_ranges(cls, range_: str, file_size: int) -> list[tuple[int, int]]: ++ ranges: list[tuple[int, int]] = [] ++ ++ for part in range_.split(","): ++ part = part.strip() ++ ++ # If the range is empty or a single dash, we ignore it. ++ if not part or part == "-": ++ continue ++ ++ # If the range is not in the format "start-end", we ignore it. ++ if "-" not in part: ++ continue ++ ++ start_str, end_str = part.split("-", 1) ++ start_str = start_str.strip() ++ end_str = end_str.strip() ++ ++ try: ++ start = int(start_str) if start_str else file_size - int(end_str) ++ end = int(end_str) + 1 if start_str and end_str and int(end_str) < file_size else file_size ++ ranges.append((start, end)) ++ except ValueError: ++ # If the range is not numeric, we ignore it. ++ continue ++ ++ return ranges ++ + def generate_multipart( + self, + ranges: typing.Sequence[tuple[int, int]], +diff --git a/tests/test_responses.py b/tests/test_responses.py +index 0a00e93..fad0b4b 100644 +--- a/tests/test_responses.py ++++ b/tests/test_responses.py +@@ -746,6 +746,34 @@ def test_file_response_insert_ranges(file_response_client: TestClient) -> None: + ] + + ++def test_file_response_range_without_dash(file_response_client: TestClient) -> None: ++ response = file_response_client.get("/", headers={"Range": "bytes=100, 0-50"}) ++ assert response.status_code == 206 ++ assert response.headers["content-range"] == f"bytes 0-50/{len(README.encode('utf8'))}" ++ ++ ++def test_file_response_range_empty_start_and_end(file_response_client: TestClient) -> None: ++ response = file_response_client.get("/", headers={"Range": "bytes= - , 0-50"}) ++ assert response.status_code == 206 ++ assert response.headers["content-range"] == f"bytes 0-50/{len(README.encode('utf8'))}" ++ ++ ++def test_file_response_range_ignore_non_numeric(file_response_client: TestClient) -> None: ++ response = file_response_client.get("/", headers={"Range": "bytes=abc-def, 0-50"}) ++ assert response.status_code == 206 ++ assert response.headers["content-range"] == f"bytes 0-50/{len(README.encode('utf8'))}" ++ ++ ++def test_file_response_suffix_range(file_response_client: TestClient) -> None: ++ # Test suffix range (last N bytes) - line 523 with empty start_str ++ response = file_response_client.get("/", headers={"Range": "bytes=-100"}) ++ assert response.status_code == 206 ++ file_size = len(README.encode("utf8")) ++ assert response.headers["content-range"] == f"bytes {file_size - 100}-{file_size - 1}/{file_size}" ++ assert response.headers["content-length"] == "100" ++ assert response.content == README.encode("utf8")[-100:] ++ ++ + @pytest.mark.anyio + async def test_file_response_multi_small_chunk_size(readme_file: Path) -> None: + class SmallChunkSizeFileResponse(FileResponse): +-- +2.33.0 + diff --git a/python-starlette.spec b/python-starlette.spec index c79e8a5..85bc912 100644 --- a/python-starlette.spec +++ b/python-starlette.spec @@ -1,12 +1,13 @@ Name: python-starlette Version: 0.46.1 -Release: 4 +Release: 5 Summary: The little ASGI library that shines License: BSD-3-Clause URL: https://www.starlette.io/ Source: https://github.com/encode/starlette/archive/%{version}/starlette-%{version}.tar.gz Patch1: CVE-2025-54121.patch +Patch2: CVE-2025-62727.patch BuildArch: noarch BuildRequires: python3-devel @@ -80,6 +81,9 @@ Summary: %{summary} %changelog +* Wed Oct 29 2025 yujingbo - 0.46.1-5 +- Fix CVE-2025-62727 + * Tue Jul 22 2025 Dongxing Wang - 0.46.1-4 - Fix CVE-2025-54121 -- Gitee