Feb 28, 2026·5 min read·20 visits
The `aiohttp` library failed to correctly accumulate the total size of `multipart/form-data` payloads in its `post()` method. Attackers can exploit this by sending requests composed of numerous small parts that individually satisfy size limits but collectively exhaust server memory. The vulnerability is fixed in version 3.13.3.
A logic error in the `aiohttp` asynchronous HTTP framework allows remote attackers to bypass request size limits (`client_max_size`) using crafted multipart/form-data requests. By failing to cumulatively track the size of incoming fields, the server permits unbounded memory allocation, leading to denial of service via Out-of-Memory (OOM) crashes.
CVE-2025-69228 identifies a high-severity Denial of Service (DoS) vulnerability in aiohttp, a widely used asynchronous HTTP client/server framework for Python's asyncio. The flaw exists within the server-side request parsing logic, specifically affecting how multipart/form-data bodies are processed when the application calls request.post().
The vulnerability is classified under CWE-770 (Allocation of Resources Without Limits or Throttling). While aiohttp intends to enforce a client_max_size limit to prevent resource exhaustion, a logic error in the implementation renders this protection ineffective against specific payload structures. This allows an unauthenticated remote attacker to force the server to allocate memory far exceeding the configured limits, leading to process termination by the operating system's OOM (Out-of-Memory) killer or general system instability.
The root cause of this vulnerability lies in the scope of the variable used to track the request body size within aiohttp/web_request.py. The post() method is designed to parse form data and populate a MultiDict. To prevent DoS attacks, it is supposed to check if the total bytes read exceed self._client_max_size.
In the vulnerable implementation, the size variable—intended to act as the accumulator for the total payload size—was initialized to 0 inside the while loop that iterates over the multipart fields. Consequently, the size check validated only the size of the current field against the global limit, rather than the sum of all fields processed so far.
Mathematically, the intended logic was sum(field_sizes) > limit. The actual implemented logic was any(field_size > limit). As long as an attacker splits a massive payload into chunks (fields) smaller than client_max_size, the limit is never triggered, and the server continues to accept data until memory is exhausted.
The vulnerability is a classic scope error. Below is a comparative analysis of the post() method in aiohttp/web_request.py before and after the fix.
In the vulnerable code, the size variable resets for every iteration of the loop (while field is not None). This prevents the method from tracking the cumulative memory usage of the request.
# aiohttp/web_request.py (Vulnerable)
async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
# ... setup ...
multipart = await self.multipart()
max_size = self._client_max_size
field = await multipart.next()
while field is not None:
size = 0 # <--- CRITICAL FLAW: Resets to 0 for every new field
field_ct = field.headers.get(hdrs.CONTENT_TYPE)
# ... reading chunk ...
size += len(chunk)
if 0 < max_size < size:
raise HTTPRequestEntityTooLarge
field = await multipart.next()The fix involves moving the initialization of size outside the while loop. This ensures that size persists across iterations, correctly accumulating the total byte count of the entire request body.
# aiohttp/web_request.py (Fixed)
async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
# ... setup ...
multipart = await self.multipart()
max_size = self._client_max_size
size = 0 # <--- FIX: Initialize once before processing fields
field = await multipart.next()
while field is not None:
# size = 0 <--- REMOVED: Do not reset here
field_ct = field.headers.get(hdrs.CONTENT_TYPE)
# ... reading chunk ...
size += len(chunk)
if 0 < max_size < size:
raise HTTPRequestEntityTooLarge
field = await multipart.next()To exploit this vulnerability, an attacker constructs a multipart/form-data POST request containing thousands of small parts.
Prerequisites:
aiohttp <= 3.13.2.await request.post().client_max_size is configured (default is 1024**2, or 1MB).Attack Steps:
size becomes 500. Since 500 < 1MB, it proceeds. The loop repeats. size is reset to 0. The second field is processed. size becomes 500. It proceeds.MultiDict continues to grow. If the attacker scales this to millions of fields (e.g., streaming the request), the server attempts to hold the entire structure in RAM, resulting in a Denial of Service.The primary impact of CVE-2025-69228 is Service Availability Loss.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (Score 7.5). The attack vector is network-based, requires no privileges or user interaction, and has a high impact on availability.The only complete remediation is to upgrade the vulnerable library. The logic error is internal to the aiohttp codebase and cannot be fully mitigated by configuration changes within the application logic itself if request.post() is used.
Immediate Action:
Upgrade aiohttp to version 3.13.3 or later. This version correctly implements cumulative size tracking.
Defense in Depth (WAF): If an immediate upgrade is not feasible, a Web Application Firewall (WAF) can mitigate the attack by enforcing strict limits on:
Content-Length header (though chunked transfer encoding may bypass this if the WAF does not buffer).Configuration Hardening: Version 3.13.3 also introduces new parameters to further harden multipart handling. Developers should review and configure:
max_field_size: Maximum size for a single field.max_parts: Maximum number of multipart parts allowed.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
aiohttp aio-libs | <= 3.13.2 | 3.13.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-770 |
| CVSS v3.1 | 7.5 (High) |
| Attack Vector | Network |
| Privileges Required | None |
| Impact | Denial of Service (DoS) |
| EPSS Score | 0.0006 |