python3-aiohttp: fix CVE-2026-59881

This patch applies the upstream fix as referenced in [2], using
the commit shown in [1]. The 3.13.5 backport passes the negotiated
compression setting to the client WebSocket reader.

[1] https://github.com/aio-libs/aiohttp/commit/47fb6ae354d4fa22048f4dbe7dbf82b625f0a2f6
[2] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-mq44-7p77-q5h7

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Darsh Kelaiya authored and Anuj Mittal committed 2026-10-29 10:19:00 +05:30
1 parent d193d78617
commit 70575863c3
2 files changed
+104

No files matched your search

@@ -0,0 +1,103 @@
From b052f95658821f726388cb86cfefad1cec264842 Mon Sep 17 00:00:00 2001
From: Sam Bull <git@sambull.org>
Date: Thu, 1 Oct 2026 02:37:30 -0700
Subject: [PATCH] Fix decompressing frames without negotiation (#12976)
(#12978)
CVE: CVE-2026-59881
Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/47fb6ae354d4fa22048f4dbe7dbf82b625f0a2f6]
Backport Changes:
- Passed the negotiated compression flag to the 3.13.5 client reader.
- Kept the reader's compress=True default: both production call
sites now pass the negotiated setting (the server already did).
- Adapted the upstream client regression test to the 3.13.5 API.
- Omitted changes to newer WebSocket APIs and unrelated tests.
(cherry picked from commit 47fb6ae354d4fa22048f4dbe7dbf82b625f0a2f6)
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
aiohttp/client.py | 4 ++-
tests/test_client_ws_functional.py | 40 +++++++++++++++++++++++++++++-
2 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/aiohttp/client.py b/aiohttp/client.py
index 0637eff6b..8cd9add5f 100644
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -1190,7 +1190,9 @@ class ClientSession:
transport = conn.transport
assert transport is not None
reader = WebSocketDataQueue(conn_proto, 2**16, loop=self._loop)
- conn_proto.set_parser(WebSocketReader(reader, max_msg_size), reader)
+ conn_proto.set_parser(
+ WebSocketReader(reader, max_msg_size, compress=bool(compress)), reader
+ )
writer = WebSocketWriter(
conn_proto,
transport,
diff --git a/tests/test_client_ws_functional.py b/tests/test_client_ws_functional.py
index 7b6bd0322..a67769c1c 100644
--- a/tests/test_client_ws_functional.py
+++ b/tests/test_client_ws_functional.py
@@ -1,5 +1,7 @@
import asyncio
+import struct
import sys
+import zlib
from typing import Any, List, NoReturn, Optional
from unittest import mock
@@ -15,8 +17,9 @@ from aiohttp import (
web,
)
from aiohttp._websocket.reader import WebSocketDataQueue
+from aiohttp._websocket.models import WS_DEFLATE_TRAILING
from aiohttp.client_ws import ClientWSTimeout
-from aiohttp.http import WSCloseCode
+from aiohttp.http import WebSocketError, WSCloseCode
from aiohttp.pytest_plugin import AiohttpClient
if sys.version_info >= (3, 11):
@@ -1276,3 +1279,38 @@ async def test_websocket_connection_cancellation(
# Cleanup properly
websocket._response = mock.Mock()
await websocket.close()
+
+
+async def test_client_rejects_compressed_frame_without_negotiation(
+ aiohttp_client: AiohttpClient,
+) -> None:
+ """Reject RSV1 frames when permessage-deflate was not negotiated."""
+ payload = b"this frame should never be decompressed by the client"
+
+ async def handler(request: web.Request) -> web.WebSocketResponse:
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+ transport = request.transport
+ assert transport is not None
+ compressor = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS
+ )
+ compressed = compressor.compress(payload)
+ compressed += compressor.flush(zlib.Z_SYNC_FLUSH)
+ compressed = compressed.removesuffix(WS_DEFLATE_TRAILING)
+ frame = struct.pack("!BB", 0x80 | 0x40 | WSMsgType.TEXT.value,
+ len(compressed)) + compressed
+ transport.write(frame)
+ await ws.receive()
+ return ws
+
+ app = web.Application()
+ app.router.add_get("/", handler)
+ client = await aiohttp_client(app)
+ async with client.ws_connect("/") as ws:
+ assert ws.compress == 0
+ msg = await ws.receive()
+
+ assert msg.type is WSMsgType.ERROR, msg
+ assert isinstance(msg.data, WebSocketError)
+ assert msg.data.code == WSCloseCode.PROTOCOL_ERROR
--
2.35.6
@@ -19,6 +19,7 @@ SRC_URI += " \
file://CVE-2026-54280.patch \
file://CVE-2026-54273_p1.patch \
file://CVE-2026-54273_p2.patch \
file://CVE-2026-59881.patch \
"
CVE_PRODUCT = "aiohttp:aiohttp"