new file mode 100644
@@ -0,0 +1,182 @@
+From f971aaedd8dc9cab84f09b97a412744084115113 Mon Sep 17 00:00:00 2001
+From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
+Date: Sat, 6 Jun 2026 00:05:19 +0100
+Subject: [PATCH] [PR #12817/69344c6e backport][3.14] Improve websocket checks
+ (#12818)
+
+**This is a backport of PR #12817 as merged into master
+(69344c6efa3e5dd80b1c88079fa06d4e902a3b83).**
+
+CVE: CVE-2026-54274
+Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/14b6ee851fb16ec199acb950de0c82d476799e7d]
+
+Co-authored-by: Sam Bull <git@sambull.org>
+(cherry picked from commit 14b6ee851fb16ec199acb950de0c82d476799e7d)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ CHANGES/12817.bugfix.rst | 1 +
+ aiohttp/_websocket/reader_py.py | 42 +++++++++++++-------
+ tests/test_websocket_parser.py | 68 +++++++++++++++++++++++++++++++++
+ 3 files changed, 98 insertions(+), 13 deletions(-)
+ create mode 100644 CHANGES/12817.bugfix.rst
+
+diff --git a/CHANGES/12817.bugfix.rst b/CHANGES/12817.bugfix.rst
+new file mode 100644
+index 000000000..c8a35e309
+--- /dev/null
++++ b/CHANGES/12817.bugfix.rst
+@@ -0,0 +1 @@
++Tightened up some websocket parser checks -- by :user:`Dreamsorcerer`.
+diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py
+index 5166d7ec2..16c5f3be6 100644
+--- a/aiohttp/_websocket/reader_py.py
++++ b/aiohttp/_websocket/reader_py.py
+@@ -202,12 +202,6 @@ class WebSocketReader:
+ 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)
+@@ -230,13 +224,6 @@ class WebSocketReader:
+ 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:
+@@ -361,6 +348,19 @@ class WebSocketReader:
+ "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,
+@@ -413,6 +413,22 @@ class WebSocketReader:
+ 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
+diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py
+index 603310958..3a283e0a3 100644
+--- a/tests/test_websocket_parser.py
++++ b/tests/test_websocket_parser.py
+@@ -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")
@@ -10,6 +10,7 @@ SRC_URI += " \
file://CVE-2026-34993.patch \
file://CVE-2026-47265.patch \
file://CVE-2026-50269.patch \
+ file://CVE-2026-54274.patch \
"
CVE_PRODUCT = "aiohttp"