diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-69243.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-69243.patch
new file mode 100644
index 0000000000..090a82f2f2
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-69243.patch
@@ -0,0 +1,307 @@
+From 3267a26ebb0c28cc70b0caf5836d20ff1b7c2f39 Mon Sep 17 00:00:00 2001
+From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
+Date: Tue, 30 Jun 2026 20:21:57 +0100
+Subject: [PATCH] [PR #13016/8ef76ab8 backport][3.14] Fix body reads after
+ failed WS upgrade (#13017)
+
+This is a backport of PR #13016 as merged into master
+(8ef76ab8029d0f2d88449fe115feb42ded646c7c).
+
+CVE: CVE-2026-69243
+Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/6ae358f0983c3f4d6f67692b2f8e65dc8e091c98]
+
+Backport Changes:
+- Adapted deferred upgrades to the 3.13.5 llhttp pause and tail
+  buffering path, including the CVE-2026-54273 queue limit.
+- Adapted the pure Python parser to its boolean payload result.
+- Used the 3.13.5 buffer size in the earlier 54273 test.
+- Kept the existing pipelined-upgrade regression test and added
+  the upstream body-read regression test.
+- Omitted generated _http_parser.c because the Wrynose recipe
+  regenerates it from _http_parser.pyx during do_configure.
+
+Co-authored-by: Sam Bull <git@sambull.org>
+(cherry picked from commit 6ae358f0983c3f4d6f67692b2f8e65dc8e091c98)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ CHANGES/13016.bugfix.rst     |  1 +
+ aiohttp/_http_parser.pyx     | 20 +++++++++----
+ aiohttp/http_parser.py       | 21 ++++++++++----
+ tests/test_http_parser.py    | 48 ++++++++++++++++++++++++++++++++
+ tests/test_web_functional.py | 54 ++++++++++++++++++++++++++++++++++--
+ 5 files changed, 131 insertions(+), 13 deletions(-)
+ create mode 100644 CHANGES/13016.bugfix.rst
+
+diff --git a/CHANGES/13016.bugfix.rst b/CHANGES/13016.bugfix.rst
+new file mode 100644
+index 000000000..a984f64e3
+--- /dev/null
++++ b/CHANGES/13016.bugfix.rst
+@@ -0,0 +1 @@
++Fixed request body not being read on rejected WebSocket upgrades -- by :user:`Dreamsorcerer`.
+diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
+index 0d0762748..478d82ecb 100644
+--- a/aiohttp/_http_parser.pyx
++++ b/aiohttp/_http_parser.pyx
+@@ -318,6 +318,7 @@ cdef class HttpParser:
+         set     _seen_singletons
+         list    _raw_headers
+         bint    _upgraded
++        bint    _pending_upgrade
+         list    _messages
+         Py_ssize_t _msg_in_flight
+         Py_ssize_t _max_msg_queue_size
+@@ -387,6 +388,7 @@ cdef class HttpParser:
+         self._response_with_body = response_with_body
+         self._read_until_eof = read_until_eof
+         self._upgraded = False
++        self._pending_upgrade = False
+         self._auto_decompress = auto_decompress
+         self._content_encoding = None
+         self._lax = False
+@@ -466,10 +468,13 @@ cdef class HttpParser:
+             h_upg = headers.get("upgrade", "")
+             allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES
+             if allowed or self._cparser.method == cparser.HTTP_CONNECT:
+-                self._upgraded = True
++                # Wait until the complete request body has been parsed.
++                self._pending_upgrade = True
+         else:
+             if upgrade and self._cparser.status_code == 101:
+-                self._upgraded = True
++                # llhttp pauses for a 101 on its own; just mark the pending
++                # switch so feed_data returns the upgraded-protocol tail.
++                self._pending_upgrade = True
+ 
+         # do not support old websocket spec
+         if SEC_WEBSOCKET_KEY1 in headers:
+@@ -597,6 +602,10 @@ cdef class HttpParser:
+         if errno is cparser.HPE_PAUSED_UPGRADE:
+             cparser.llhttp_resume_after_upgrade(self._cparser)
+             nb = cparser.llhttp_get_error_pos(self._cparser) - base
++            if self._pending_upgrade:
++                # A supported upgrade whose request body has now been fully read.
++                self._upgraded = True
++                self._pending_upgrade = False
+         elif errno is cparser.HPE_PAUSED:
+             cparser.llhttp_resume(self._cparser)
+             pos = cparser.llhttp_get_error_pos(self._cparser) - base
+@@ -818,10 +827,9 @@ cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1:
+         pyparser._last_error = exc
+         return -1
+     else:
+-        if pyparser._upgraded or pyparser._cparser.method == cparser.HTTP_CONNECT:
+-            return 2
+-        else:
+-            return 0
++        if not pyparser._response_with_body:
++            return 1
++        return 0
+ 
+ 
+ cdef int cb_on_body(cparser.llhttp_t* parser,
+diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
+index 32375f83a..f4458dc9d 100644
+--- a/aiohttp/http_parser.py
++++ b/aiohttp/http_parser.py
+@@ -289,6 +289,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+         self._lines: List[bytes] = []
+         self._tail = b""
+         self._upgraded = False
++        self._pending_upgrade = False
+         self._payload = None
+         self._payload_parser: Optional[HttpPayloadParser] = None
+         self._auto_decompress = auto_decompress
+@@ -417,9 +418,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+                         if SEC_WEBSOCKET_KEY1 in msg.headers:
+                             raise InvalidHeader(SEC_WEBSOCKET_KEY1)
+ 
+-                        self._upgraded = msg.upgrade and _is_supported_upgrade(
+-                            msg.headers
+-                        )
++                        upgraded = msg.upgrade and _is_supported_upgrade(msg.headers)
+ 
+                         method = getattr(msg, "method", self.method)
+                         # code is only present on responses
+@@ -431,8 +430,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+                             method and method in EMPTY_BODY_METHODS
+                         )
+                         if not empty_body and (
+-                            ((length is not None and length > 0) or msg.chunked)
+-                            and not self._upgraded
++                            (length is not None and length > 0) or msg.chunked
+                         ):
+                             payload = StreamReader(
+                                 self.protocol,
+@@ -457,6 +455,10 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+                             )
+                             if not payload_parser.done:
+                                 self._payload_parser = payload_parser
++                                # https://www.rfc-editor.org/info/rfc9110/#section-7.8-15
++                                # Defer any requested upgrade until the
++                                # complete request has been read.
++                                self._pending_upgrade = upgraded
+                         elif method == METH_CONNECT:
+                             assert isinstance(msg, RawRequestMessage)
+                             payload = StreamReader(
+@@ -501,6 +503,11 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+                             )
+                             if not payload_parser.done:
+                                 self._payload_parser = payload_parser
++                        elif upgraded:
++                            # No body to read, so the connection switches to
++                            # the upgraded protocol immediately.
++                            self._upgraded = True
++                            payload = EMPTY_PAYLOAD
+                         else:
+                             payload = EMPTY_PAYLOAD
+ 
+@@ -548,6 +555,10 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+                     start_pos = 0
+                     data_len = len(data)
+                     self._payload_parser = None
++                    if self._pending_upgrade:
++                        # The body is complete; the protocol switch may begin.
++                        self._upgraded = True
++                        self._pending_upgrade = False
+                     continue
+             else:
+                 break
+diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
+index a4e6bc056..92e59af1c 100644
+--- a/tests/test_http_parser.py
++++ b/tests/test_http_parser.py
+@@ -1217,6 +1217,54 @@ async def test_http_request_upgrade_unknown(parser: Any) -> None:
+     assert await messages[0][-1].read() == b"{}"
+ 
+ 
++@pytest.mark.parametrize("chunked", (False, True), ids=("content-length", "chunked"))
++async def test_http_request_upgrade_with_body_read(
++    parser: HttpRequestParser, chunked: bool
++) -> None:
++    body_request = b"foobarbaz\r\n\r\n"
++    if chunked:
++        framing = b"Transfer-Encoding: chunked\r\n"
++        body = b"%x\r\n%s\r\n0\r\n\r\n" % (len(body_request), body_request)
++    else:
++        framing = b"Content-Length: %d\r\n" % len(body_request)
++        body = body_request
++    after = b"GET /after HTTP/1.1\r\nHost: a\r\n\r\n"
++    text = (
++        b"GET /ws HTTP/1.1\r\nHost: a\r\n"
++        b"Connection: Upgrade\r\nUpgrade: websocket\r\n"
++        + framing
++        + b"\r\n"
++        + body
++        + after
++    )
++    messages, upgrade, tail = parser.feed_data(text)
++    assert len(messages) == 1
++    msg, payload = messages[0]
++    assert msg.method == "GET"
++    assert msg.path == "/ws"
++    assert msg.upgrade
++    assert await payload.read() == body_request
++    # The connection switches protocols only after the body is fully read.
++    assert upgrade
++    assert tail == after
++
++
++def test_http_request_upgrade_empty_body_allowed(parser: HttpRequestParser) -> None:
++    text = (
++        b"GET /ws HTTP/1.1\r\n"
++        b"Host: a\r\n"
++        b"Connection: Upgrade\r\n"
++        b"Upgrade: websocket\r\n"
++        b"Content-Length: 0\r\n\r\n"
++        b"some raw data"
++    )
++    messages, upgrade, tail = parser.feed_data(text)
++    msg = messages[0][0]
++    assert msg.upgrade
++    assert upgrade
++    assert tail == b"some raw data"
++
++
+ @pytest.fixture
+ def xfail_c_parser_url(request) -> None:
+     if isinstance(request.getfixturevalue("parser"), HttpRequestParserPy):
+diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
+index b958bef9d..0d2824a9c 100644
+--- a/tests/test_web_functional.py
++++ b/tests/test_web_functional.py
+@@ -25,7 +25,6 @@ from aiohttp import (
+ )
+ from aiohttp.compression_utils import ZLibBackend, ZLibCompressObjProtocol
+ from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING
+-from aiohttp.helpers import DEFAULT_CHUNK_SIZE
+ from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer
+ from aiohttp.streams import StreamReader
+ from aiohttp.typedefs import Handler
+@@ -1923,7 +1922,7 @@ async def test_unread_compressed_body_drain_is_bounded(
+             await writer.wait_closed()
+ 
+     # Bounded by the buffer, not the decompressed size.
+-    assert max(drain_reads) <= 3 * DEFAULT_CHUNK_SIZE
++    assert max(drain_reads) <= 3 * 2**16
+     assert max(drain_reads) < decompressed_size
+ 
+ 
+@@ -2624,3 +2623,54 @@ async def test_keepalive_expires_on_time(aiohttp_client: AiohttpClient) -> None:
+ 
+         # Ensure the keep alive handle expires
+         assert request_handler._keepalive_handle is None
++
++
++async def test_declined_websocket_upgrade_reads_body(
++    aiohttp_server: AiohttpServer,
++) -> None:
++    body_read = b""
++
++    async def ws_handler(request: web.Request) -> web.Response:
++        nonlocal body_read
++        # Decline the upgrade; read the body as a normal handler may.
++        body_read = await request.read()
++        return web.Response(text="declined")
++
++    async def after_handler(request: web.Request) -> web.Response:
++        return web.Response(text="after")
++
++    app = web.Application()
++    app.router.add_get("/ws", ws_handler)
++    app.router.add_get("/after", after_handler)
++    server = await aiohttp_server(app)
++
++    body = b"FooBarBaz\r\n\r\n"
++    # Use raw connection in order to pipeline requests.
++    pipeline = (
++        (
++            b"GET /ws HTTP/1.1\r\n"
++            b"Host: localhost\r\n"
++            b"Connection: Upgrade\r\n"
++            b"Upgrade: websocket\r\n"
++            b"Content-Length: %d\r\n\r\n" % len(body)
++        )
++        + body
++        + b"GET /after HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
++    )
++
++    reader, writer = await asyncio.open_connection(server.host, server.port)
++    try:
++        writer.write(pipeline)
++        await writer.drain()
++        # The trailing request sends Connection: close, so the server closes
++        # once it has answered it -- reading to EOF gathers both responses.
++        response = await asyncio.wait_for(reader.read(), 5)
++    finally:
++        writer.close()
++        with suppress(ConnectionResetError, BrokenPipeError):
++            await writer.wait_closed()
++
++    assert body_read == body
++    # Both the upgrade request and the pipelined request were served.
++    assert response.count(b"HTTP/1.") == 2, response
++    assert response.count(b" 200 ") == 2, response
+-- 
+2.35.6
+
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb
index b34a0fa3eb..417760c9ca 100644
--- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb
+++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb
@@ -20,6 +20,7 @@ SRC_URI += " \
     file://CVE-2026-54273_p1.patch \
     file://CVE-2026-54273_p2.patch \
     file://CVE-2026-59881.patch \
+    file://CVE-2026-69243.patch \
 "
 
 CVE_PRODUCT = "aiohttp:aiohttp"
