Feb 28, 2026·6 min read·12 visits
aiohttp failed to restrict the parsing of the HTTP 'Range' header to ASCII digits. Python's default regex engine interprets non-ASCII Unicode digits (e.g., Devanagari numerals) as valid numbers. This discrepancy allows attackers to bypass proxy filters or desynchronize the interpretation of the request, leading to HTTP Request Smuggling or Cache Poisoning. Fixed in version 3.13.3.
A differential interpretation vulnerability exists in aiohttp versions 3.13.2 and earlier, caused by the mishandling of Unicode digits in the HTTP 'Range' header. This flaw allows attackers to smuggle requests or poison web caches by injecting non-ASCII numeric characters that are interpreted as valid digits by the Python backend but treated as invalid or text by upstream proxies.
CVE-2025-69225 represents a semantic parsing vulnerability within the aiohttp library, a widely used asynchronous HTTP client/server framework for Python. The vulnerability resides in the server-side handling of the HTTP Range header, specifically within the aiohttp.web_request module.
Modern web architectures typically involve a chain of systems processing a single HTTP request—load balancers, WAFs, reverse proxies (like Nginx or HAProxy), and finally the application backend. Security relies on these systems having a consistent interpretation of the HTTP protocol. This vulnerability breaks that consistency. While the HTTP standards (RFC 9110/9112) imply that header values like byte ranges should consist of ASCII digits (0-9), aiohttp leveraged Python's permissive string handling to accept a broader range of Unicode numeric characters.
This behavior introduces a classic 'differential interpretation' attack surface. An attacker can craft an HTTP request containing Unicode digits (e.g., '५' for '5') in the Range header. A strict frontend proxy may view this as an invalid header, a distinct textual string, or garbage data, potentially forwarding it without normalization. The aiohttp backend, however, parses these characters as valid integers. This divergence creates the conditions necessary for HTTP Request Smuggling (HRS) and Cache Poisoning.
The root cause of this vulnerability lies in the default behavior of Python's re (regular expression) module combined with the int() constructor, neither of which defaults to strict ASCII enforcement in Python 3.
In Python 3, the regular expression character class \d (digit) matches any character in the Unicode category Nd (Number, Decimal Digit). This includes standard ASCII digits [0-9], but also hundreds of other characters from various scripts, such as:
५ (U+096B, Value: 5)١ (U+0661, Value: 1)1 (U+FF11, Value: 1)The vulnerable code in aiohttp utilized the regex pattern r"^bytes=(\d*)-(\d*)$" without the re.ASCII flag. Consequently, the pattern matches strings like bytes=0-५. When the captured group ५ is passed to Python's int() function, it is successfully converted to the integer 5.
Upstream infrastructure (written in C, Go, or Rust) typically relies on standard ASCII byte processing. When a proxy sees Range: bytes=0-५, it does not interpret it as a valid byte range request because ५ is not in the ASCII range 0x30-0x39. The proxy might ignore the header or forward it as a generic custom header. aiohttp, acting on the same input, processes it as a valid range request bytes=0-5, altering the response content (e.g., returning a 206 Partial Content status). This specific misalignment allows attackers to manipulate the response state without the proxy being aware.
The vulnerability existed in aiohttp/web_request.py within the http_range property. Below is the analysis of the vulnerable code and the remediation applied in version 3.13.3.
# aiohttp/web_request.py
@property
def http_range(self) -> slice:
rng = self.headers.get(hdrs.RANGE)
if rng is not None:
try:
# VULNERABILITY: \d matches Unicode digits by default in Python 3
pattern = r"^bytes=(\d*)-(\d*)$"
start, end = re.findall(pattern, rng)[0]
# int() also accepts Unicode digits
if start:
start = int(start)
if end:
end = int(end)
# ...The fix, applied in commit c7b7a044f88c71cefda95ec75cdcfaa4792b3b96, forces the regex engine to operate in ASCII-only mode. This ensures that \d only matches characters [0-9].
# aiohttp/web_request.py
@property
def http_range(self) -> slice:
rng = self.headers.get(hdrs.RANGE)
if rng is not None:
try:
pattern = r"^bytes=(\d*)-(\d*)$"
# FIX: re.ASCII flag restricts matching to [0-9]
start, end = re.findall(pattern, rng, re.ASCII)[0]
if start:
start = int(start)
# ...This simple one-line change aligns the application's parsing logic with the strict expectations of the HTTP RFCs and upstream infrastructure, effectively neutralizing the attack vector.
The exploitation of this vulnerability relies on the presence of an intermediary system that inspects or acts upon HTTP headers but interprets them differently than aiohttp.
Range headers to prevent cache fragmentation.main.js) with the header Range: bytes=0-५.Range header invalid, and assumes the backend will return the full file (200 OK). It prepares to cache the response associated with the URL.aiohttp parses ५ as 5, interprets the header as bytes=0-5, and returns a 206 Partial Content response containing only the first 6 bytes of the file.main.js.main.js receive the corrupted 6-byte file, causing a denial of service for that asset.The impact of CVE-2025-69225 is classified as Medium (CVSS 5.3). While the mechanism allows for protocol manipulation, the actual impact is highly dependent on the architecture of the deployed environment.
Prerequisites: The attack requires no authentication (PR:N) and can be executed remotely (AV:N). However, it requires a specific infrastructure setup involving a disparity between the proxy and the backend. Standalone aiohttp instances without an upstream proxy are generally not vulnerable to the smuggling aspect, though they will still process the unexpected range.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
aiohttp aio-libs | <= 3.13.2 | 3.13.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-436 |
| Attack Vector | Network |
| CVSS v3.1 | 5.3 (Medium) |
| CVSS v4.0 | 2.7 (Low) |
| Impact | Cache Poisoning / Request Smuggling |
| Exploit Status | PoC Available |
Interpretation Conflict