CVE-2025-69228

Infinite Loop of Doom: Memory Exhaustion in AIOHTTP

Alon Barad
Alon Barad
Software Engineer

Jan 6, 2026·5 min read

Executive Summary (TL;DR)

The `aiohttp` library forgot to persist the total request size counter across multipart fields. By sending a request with thousands of small parts, an attacker can bypass the global `client_max_size` limit, forcing the server to buffer gigabytes of data until it crashes.

A critical logic error in aiohttp's multipart parsing allows attackers to bypass body size limits by resetting the byte counter for every form field, leading to trivial DoS.

The Hook: Convenience Is The Enemy

We all love Python's aiohttp. It's fast, it's async, and it makes writing performant web servers a breeze. But like most modern frameworks, it offers 'convenience methods'—helper functions designed to save you a few lines of code. One such method is request.post(). It dutifully reads the entire POST body, parses form data, and hands you a nice dictionary.

But here is the catch: when you abstract away the data ingestion, you trust the library to handle the safety checks. You assume that if you set a client_max_size of 1MB, the library will stop reading after 1MB.

In aiohttp versions prior to 3.13.3, that trust was misplaced. Due to a comical scoping error, the library was effectively enforcing the limit per field rather than per request. It’s equivalent to a bouncer counting capacity at a club, but resetting the clicker to zero every time a new person walks in. The result? The club fills up until the fire marshal (the Linux OOM Killer) shuts the whole place down.

The Flaw: A Lesson in Variable Scope

The vulnerability lies deep within aiohttp/web_request.py. When the server receives a multipart/form-data payload (the standard format for file uploads), it iterates over the parts using a while loop. The intention was to track the cumulative size of all parts to ensure the request didn't exceed the configured limit.

Here is where it went wrong: the variable size, responsible for tracking the total bytes consumed, was initialized inside the loop.

This means that for every new field encountered in the multipart body, the server forgot how much data it had already processed. It looked at Field A, saw it was 50KB (under the 1MB limit), and accepted it. Then it looked at Field B, reset the counter to 0, saw it was 50KB, and accepted it. An attacker just needs to send enough 'small' fields to consume all available RAM.

The Code: The Smoking Gun

Let's look at the diff. It is painfully simple, yet catastrophic. This is a classic case of what I like to call 'Indentation Vulnerabilities'—where the logic is correct, but the placement is wrong.

Vulnerable Code (aiohttp <= 3.13.2):

async def post(self):
    # ... setup ...
    field = await multipart.next()
    while field is not None:
        size = 0  # <--- CRITICAL FAIL. Resetting on every loop iteration.
        # ... processing field ...
        if 0 < max_size < size:
             raise HTTPRequestEntityTooLarge

Fixed Code (aiohttp 3.13.3):

async def post(self):
    # ... setup ...
    size = 0  # <--- FIX. Initialized once, persists across fields.
    field = await multipart.next()
    while field is not None:
        # ... processing field ...
        if 0 < max_size < size:
             raise HTTPRequestEntityTooLarge

By moving that single line up two notches, the size variable now correctly accumulates the byte count across the entire request lifecycle. The fix is boring, but the bug was devastating.

The Exploit: Death by a Thousand Fields

Exploiting this is trivial. We don't need fancy heap spraying or ROP chains. We just need to construct a valid HTTP POST request that respects the format but abuses the logic.

Let's assume the target server has the default limit of 1MB (client_max_size). To crash it, we construct a multipart/form-data body with thousands of parts. Each part contains a payload slightly smaller than the limit (e.g., 500KB).

Attack Scenario:

  1. Open Connection: Connect to the target.
  2. Start POST: Send headers for a massive multipart request.
  3. Stream Fields:
    • Send Part 1: 500KB of 'A's. Server checks: 0 + 500KB < 1MB. OK.
    • Send Part 2: 500KB of 'B's. Server resets size to 0. Checks: 0 + 500KB < 1MB. OK.
    • Send Part 3: 500KB of 'C's. Server resets size to 0. Checks: 0 + 500KB < 1MB. OK.
  4. Repeat: Send 4,000 parts.

Result: The server has allocated 4,000 * 500KB = 2GB of memory inside a MultiDictProxy. If the server is a container with a 1GB limit, it crashes immediately. If it's a bare metal server, it swaps until it freezes.

The Mitigation: Move the Line

The immediate fix is to upgrade to aiohttp version 3.13.3. This version correctly initializes the size counter outside the loop.

If you cannot upgrade immediately, you have two options:

  1. Stop using request.post(): This method is designed for convenience, not heavy lifting. For robust applications, use request.multipart() and iterate over the fields yourself, manually counting bytes and breaking the loop if a global threshold is exceeded. This allows you to stream data to disk or discard it without buffering everything in RAM.
  2. WAF Intervention: Configure your Web Application Firewall (WAF) or Nginx reverse proxy to strictly limit the total request body size (client_max_body_size in Nginx). The WAF sees the raw bytes before Python does, so it won't be fooled by the multipart structure.

Fix Analysis (1)

Technical Appendix

CVSS Score
6.6/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:U

Affected Systems

Python web applications using aiohttpMicroservices relying on aiohttp.web.Request.post()

Affected Versions Detail

Product
Affected Versions
Fixed Version
aiohttp
aio-libs
<= 3.13.23.13.3
AttributeDetail
CWE IDCWE-770
Attack VectorNetwork (HTTP)
CVSS v4.06.6 (Medium)
ImpactDenial of Service (Memory Exhaustion)
Exploit StatusTrivial / PoC Available
Fix Version3.13.3
CWE-770
Allocation of Resources Without Limits or Throttling

Allocation of Resources Without Limits or Throttling

Vulnerability Timeline

Published
2026-01-06
Patch Released (v3.13.3)
2026-01-06

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.