Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:starlette
User: [email protected]
Usertags: pu

[ Reason ]
Fix CVE-2025-62727 [1], a denial of service vulnerability in
FileResponse triggered by crafted HTTP Range headers.

[ Impact ]
An attacker can exploit specially crafted Range headers to cause
excessive resource consumption, potentially leading
to service unavailability.

[ Tests ]
The upstream project added tests to validate the fix.
The package builds successfully and all build-time tests pass.
The proof of concept [2] provided for CVE-2025-62727 was
reproduced on the vulnerable version and is no longer
effective after applying the patch.

[ Risks ]
The change is limited in scope and only affects the handling of
HTTP Range headers in FileResponse.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

[ Changes ]
Update the handling of HTTP Range headers in FileResponse to
properly parse and validate byte ranges, preventing
denial of service via crafted headers.

[ Other info ]
@tijuca has reviewed this backport and will sponsor it.

[1] https://security-tracker.debian.org/tracker/CVE-2025-62727
[2] https://github.com/Kludex/starlette/security/advisories/GHSA-7f5h-v6xp-fcq8
diff -Nru starlette-0.46.1/debian/changelog starlette-0.46.1/debian/changelog
--- starlette-0.46.1/debian/changelog   2025-07-28 09:42:53.000000000 +0000
+++ starlette-0.46.1/debian/changelog   2026-01-19 19:57:07.000000000 +0000
@@ -1,9 +1,19 @@
+starlette (0.46.1-3+deb13u1) trixie; urgency=medium
+
+  * Team upload.
+  * d/p/CVE-2025-62727.patch: Backport Upstream patch to fix CVE-2025-62727
+    (denial of service via crafted HTTP Range header in FileResponse)
+  * d/changelog: Fix changelog indentation
+  * d/gbp.conf: Update to Trixie
+
+ -- Matheus Polkorny <[email protected]>  Mon, 19 Jan 2026 16:57:07 -0300
+
 starlette (0.46.1-3) unstable; urgency=high
 
- [ Yang Wang ]
- * Fix CVE-2025-54121: Avoid event loop blocking during multipart file uploads
-   by writing to disk using thread pool to prevent synchronous blocking when
-   SpooledTemporaryFile rolls over to disk. (Closes: #1109805)
+  [ Yang Wang ]
+  * Fix CVE-2025-54121: Avoid event loop blocking during multipart file uploads
+    by writing to disk using thread pool to prevent synchronous blocking when
+    SpooledTemporaryFile rolls over to disk. (Closes: #1109805)
 
  -- Piotr Ożarowski <[email protected]>  Mon, 28 Jul 2025 11:42:53 +0200
 
diff -Nru starlette-0.46.1/debian/gbp.conf starlette-0.46.1/debian/gbp.conf
--- starlette-0.46.1/debian/gbp.conf    2025-07-28 09:40:03.000000000 +0000
+++ starlette-0.46.1/debian/gbp.conf    2026-01-19 19:57:07.000000000 +0000
@@ -1,3 +1,3 @@
 [DEFAULT]
 pristine-tar = True
-debian-branch = debian/master
+debian-branch = debian/trixie
diff -Nru starlette-0.46.1/debian/patches/CVE-2025-62727.patch 
starlette-0.46.1/debian/patches/CVE-2025-62727.patch
--- starlette-0.46.1/debian/patches/CVE-2025-62727.patch        1970-01-01 
00:00:00.000000000 +0000
+++ starlette-0.46.1/debian/patches/CVE-2025-62727.patch        2026-01-19 
19:57:07.000000000 +0000
@@ -0,0 +1,135 @@
+From: Marcelo Trylesinski <[email protected]>
+Date: Tue, 28 Oct 2025 18:14:01 +0100
+Subject: Merge commit from fork
+
+Origin: upstream, 
https://github.com/Kludex/starlette/commit/4ea6e22b489ec388d6004cfbca52dd5b147127c5
+No changes in the backport.
+---
+ 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):
diff -Nru starlette-0.46.1/debian/patches/series 
starlette-0.46.1/debian/patches/series
--- starlette-0.46.1/debian/patches/series      2025-07-28 09:42:53.000000000 
+0000
+++ starlette-0.46.1/debian/patches/series      2026-01-19 19:57:07.000000000 
+0000
@@ -1,2 +1,3 @@
 json-format.patch
 0002-fix-cve-2024-28849-async-write.patch
+CVE-2025-62727.patch

Reply via email to