VYPR
Medium severity6.6GHSA Advisory· Published Jun 15, 2026· Updated Jun 15, 2026

aiohttp: Incomplete websocket frame payloads bypass memory limits

CVE-2026-54274

Description

Large incomplete websocket frame payloads in aiohttp ≤3.14.0 bypass memory limits, enabling a remote denial-of-service attack.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Large incomplete websocket frame payloads in aiohttp ≤3.14.0 bypass memory limits, enabling a remote denial-of-service attack.

Vulnerability

A memory-limitation bypass exists in aiohttp's WebSocket frame handling. When an attacker sends large, incomplete WebSocket frame payloads, the framework fails to properly enforce size limits on buffered data, allowing unbounded memory consumption. This affects all versions of aiohttp up to and including 3.14.0 [1].

Exploitation

An attacker with network access to a web application that exposes WebSocket endpoints can send a sequence of large, incomplete WebSocket frames. No authentication or prior session is required. By deliberately never completing the frames, the attacker causes the server to hold the partial payloads in memory indefinitely [1].

Impact

Successful exploitation leads to excessive memory usage on the target server. This can cause the application to become unresponsive or crash, resulting in a denial of service (DoS) condition. The attacker gains no code execution or data access, only the ability to exhaust system resources [1].

Mitigation

The vulnerability is fixed in commit 14b6ee851fb16ec199acb950de0c82d476799e7d [2]. Affected users should upgrade to aiohttp version 3.14.1 or later, which includes this patch. No workaround is available; disabling WebSocket endpoints may be considered if patching is not immediately possible [1].

AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
14b6ee851fb1

[PR #12817/69344c6e backport][3.14] Improve websocket checks (#12818)

https://github.com/aio-libs/aiohttppatchback[bot]Jun 5, 2026Fixed in 3.14.1via llm-release-walk
3 files changed · +98 13
  • aiohttp/_websocket/reader_py.py+29 13 modified
    @@ -208,12 +208,6 @@ def _handle_frame(
                     if opcode != OP_CODE_CONTINUATION:
                         self._opcode = opcode
                     self._partial += payload
    -                if self._max_msg_size and len(self._partial) >= self._max_msg_size:
    -                    raise WebSocketError(
    -                        WSCloseCode.MESSAGE_TOO_BIG,
    -                        f"Message size {len(self._partial)} "
    -                        f"exceeds limit {self._max_msg_size}",
    -                    )
                     return
     
                 has_partial = bool(self._partial)
    @@ -236,13 +230,6 @@ def _handle_frame(
                 else:
                     assembled_payload = payload
     
    -            if self._max_msg_size and len(assembled_payload) >= self._max_msg_size:
    -                raise WebSocketError(
    -                    WSCloseCode.MESSAGE_TOO_BIG,
    -                    f"Message size {len(assembled_payload)} "
    -                    f"exceeds limit {self._max_msg_size}",
    -                )
    -
                 # Decompress process must to be done after all packets
                 # received.
                 if compressed:
    @@ -376,6 +363,19 @@ def _feed_data(self, data: bytes) -> None:
                             "Received frame with non-zero reserved bits",
                         )
     
    +                if opcode not in {
    +                    OP_CODE_CONTINUATION,
    +                    OP_CODE_TEXT,
    +                    OP_CODE_BINARY,
    +                    OP_CODE_CLOSE,
    +                    OP_CODE_PING,
    +                    OP_CODE_PONG,
    +                }:
    +                    raise WebSocketError(
    +                        WSCloseCode.PROTOCOL_ERROR,
    +                        f"Unexpected opcode={opcode!r}",
    +                    )
    +
                     if opcode > 0x7 and fin == 0:
                         raise WebSocketError(
                             WSCloseCode.PROTOCOL_ERROR,
    @@ -428,6 +428,22 @@ def _feed_data(self, data: bytes) -> None:
                     else:
                         self._payload_bytes_to_read = len_flag
     
    +                # Reject oversized data frames before buffering any payload
    +                # bytes. Control frames are capped at 125 bytes (checked in
    +                # READ_HEADER) so only text/binary/continuation need this.
    +                if self._max_msg_size and self._frame_opcode in {
    +                    OP_CODE_TEXT,
    +                    OP_CODE_BINARY,
    +                    OP_CODE_CONTINUATION,
    +                }:
    +                    projected_size = self._payload_bytes_to_read + len(self._partial)
    +                    if projected_size >= self._max_msg_size:
    +                        raise WebSocketError(
    +                            WSCloseCode.MESSAGE_TOO_BIG,
    +                            f"Message size {projected_size} "
    +                            f"exceeds limit {self._max_msg_size}",
    +                        )
    +
                     self._state = READ_PAYLOAD_MASK if self._has_mask else READ_PAYLOAD
     
                 # read payload mask
    
  • CHANGES/12817.bugfix.rst+1 0 added
    @@ -0,0 +1 @@
    +Tightened up some websocket parser checks -- by :user:`Dreamsorcerer`.
    
  • tests/test_websocket_parser.py+68 0 modified
    @@ -643,6 +643,74 @@ def test_compressed_msg_too_large(out) -> None:
         assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG
     
     
    +@pytest.mark.parametrize("fin", (0x80, 0x00), ids=("fin", "non-fin"))
    +def test_msg_too_large_at_header(out: WebSocketDataQueue, fin: int) -> None:
    +    max_msg_size = 256
    +    parser = WebSocketReader(out, max_msg_size, compress=False)
    +
    +    # Header alone: TEXT, 64-bit length, declares 1 MiB of payload.
    +    header = PACK_LEN3(fin | WSMsgType.TEXT, 127, 1024 * 1024)
    +    with pytest.raises(
    +        WebSocketError, match=r"^Message size 1048576 exceeds limit 256$"
    +    ) as ctx:
    +        parser._feed_data(header)
    +    assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG
    +
    +
    +def test_msg_too_large_across_fragments(out: WebSocketDataQueue) -> None:
    +    # Individual fragments fit under max_msg_size but accumulate past it.
    +    max_msg_size = 256
    +    parser = WebSocketReader(out, max_msg_size, compress=False)
    +
    +    first = build_frame(b"a" * 100, WSMsgType.TEXT, is_fin=False)
    +    parser._feed_data(first)
    +    middle = build_frame(b"b" * 100, WSMsgType.CONTINUATION, is_fin=False)
    +    parser._feed_data(middle)
    +
    +    # Third 100-byte fragment would push the accumulated total to 300.
    +    last = build_frame(b"c" * 100, WSMsgType.CONTINUATION, is_fin=False)
    +    with pytest.raises(
    +        WebSocketError, match=r"^Message size 300 exceeds limit 256$"
    +    ) as ctx:
    +        parser._feed_data(last)
    +    assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG
    +
    +
    +def test_msg_too_large_text_after_non_fin_text(out: WebSocketDataQueue) -> None:
    +    # Protocol-violating sequence: a fresh TEXT arrives while a fragmented
    +    # message is still open.
    +    max_msg_size = 256
    +    parser = WebSocketReader(out, max_msg_size, compress=False)
    +
    +    first = build_frame(b"a" * 200, WSMsgType.TEXT, is_fin=False)
    +    parser._feed_data(first)
    +
    +    # Second TEXT header alone announces 100 bytes; 100 + 200 partial = 300.
    +    second_header = PACK_LEN1(WSMsgType.TEXT, 100)
    +    with pytest.raises(
    +        WebSocketError, match=r"^Message size 300 exceeds limit 256$"
    +    ) as ctx:
    +        parser._feed_data(second_header)
    +    assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG
    +
    +
    +@pytest.mark.parametrize(
    +    "opcode",
    +    (0x3, 0x4, 0x5, 0x6, 0x7, 0xB, 0xC, 0xD, 0xE, 0xF),
    +    ids=lambda v: f"0x{v:x}",
    +)
    +def test_reserved_opcode_rejected_at_header(
    +    out: WebSocketDataQueue, opcode: int
    +) -> None:
    +    # RFC 6455 reserves opcodes 0x3-0x7 (non-control) and 0xB-0xF (control).
    +    parser = WebSocketReader(out, max_msg_size=256, compress=False)
    +
    +    header = PACK_LEN3(0x80 | opcode, 127, 1024 * 1024)
    +    with pytest.raises(WebSocketError, match=rf"^Unexpected opcode={opcode}$") as ctx:
    +        parser._feed_data(header)
    +    assert ctx.value.code == WSCloseCode.PROTOCOL_ERROR
    +
    +
     class TestWebSocketError:
         def test_ctor(self) -> None:
             err = WebSocketError(WSCloseCode.PROTOCOL_ERROR, "Something invalid")
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

2

News mentions

0

No linked articles in our index yet.