From patchwork Mon Aug 31 04:57:36 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96881 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1283FC624C8 for ; Mon, 31 Aug 2026 04:57:52 +0000 (UTC) Received: from alln-iport-6.cisco.com (alln-iport-6.cisco.com [173.37.142.93]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.22957.1788152269275233289 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=f6F+eVjv; spf=pass (domain: cisco.com, ip: 173.37.142.93, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=8351; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=LEs985LE3WRjSvdleMTa3lNSEc2CXA7+w/sYRat5l7Q=; b=f6F+eVjvhC9mmqr34GerUvaZ+zHF6GtSMwkrqKjfrVnPAH+6G0uKjxYT NZZ5L6m5C1uwb2zSmZrWIe2A7in5ILOWbKCQVyYxdtplAZl7U08/ICFfH VbzvzzA+2NeBHxj011y7JQy1JF87iVpRElzRAjvSklWZ55Ww/OtStYd6F PNJQNEvF0L+9mXbdMJnFhug/OB1oazmv1QF1qixORepeTBlQaKE6L5kBa jhBsEFofcgopEXIKIiNT7A1VOA2CqTCEOnpaRxYhCWexv0SaodR7yd1qL /ycknPjeZfWjjWaOGUM72hmTf8IxO3rCKSOZBzwpGKLyyhe9iVvhVvgoI Q==; X-CSE-ConnectionGUID: g95PSEaVQ8Kw2U6XGcAKgg== X-CSE-MsgGUID: SotVk6j+QHm30PxzyB7MNg== X-IPAS-Result: A0BLAgAsCZVq/5UQJK1aHgEBCxIMggULgld0XkNJhFeRcwOeG4F+DwEBAQ9EDQQBAYQ/RgKNcwImNAkOAQIEAwIDAQEBAQEBAQEBAQELAQEFAQEBAgEHBYEOE4ZPDYZaAQIBAyMECwEYAS0QHAMBAgMCJgICKyMIEAmDAgGCdAMRwgF6fzOBAYMoAT8CQ1DbMAELFAGBCi6FP4MfAYUCXRgBhHwnGxuBcoQIdoEFgVwBAYU7gmoEgiKBDIFaHpIBSIECHANZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPhc1WBsGBYEdgSiEDyMZNnqBCV6BKylgARIXgQmCCAKCWoIFAgFJQw4HR1MJBAsYDUgRLDcVGQQ9AW4HjnofgksBgQ0BKgEXgX0pEZNYkXyhDwoog3aMIpU6GjOqbAuYfY4KlgBQhGmBaDyBRwsHcBWDIglKGQ+OKg4Lg2CBf8o6JzICCTIBAQcCBw4DC4FokAItgU8BAQ IronPort-Data: A9a23:zQ8YJKzfA93y0ajmDVJ6t+dmxyrEfRIJ4+MujC+fZmUNrF6WrkUFm mEdWW/UbPjeazb1KdxwYIywoRkAvZDWy9JgSgo9+1hgHilAwSbn6Xt1DatR0we6dJCroJdPt p1GAjX4BJlqCCea/VH1buSJQUBUjcmgXqD7BPPPJhd/TAplTDZJoR94kobVuKYw6TSCK13L4 46aT/H3Ygf/hWYkaTpMsspvlTs21BjMkGJA1rABTagjUG/2zxE9EJ8ZLKetGHr0KqE8NvK6X evK0Iai9Wrf+Ro3Yvv9+losWhRXKlJ6FVHmZkt+A8BOsDAbzsAB+vpT2M4nVKtio27hc+adZ zl6ncfYpQ8BZsUgkQmGOvVSO3kW0aZuoNcrLZUj2CCe5xWuTpfi/xlhJE5nYLUj1+9JODFt6 qYldgAiZTmgiO3jldpXSsE07igiBMDvOIVavjRryivUSK98B5vCWK7No9Rf2V/chOgXQq2YP JRfMGQpNUiRC/FMEg9/5JYWkOSlgnD+YjRwo1OOrq1x6G/WpOB0+Oi3a4aJJoLUHa25mG6jo X74/Dr7QSg6MYfH8SqX41jzo8nAyHaTtIU6UefQGuRRqFqLy2oeDRcbWVe2rby1h1CzX/pbK lcI4WwptaU0+UmhQ9XxUhH+p2SL1iPwQPJZF+k8rQXIwa3O7kPAXC4PTyVKb5ots8peqSEW6 2JlVujBXVRH2IB5g1rEnltIhVte4RQoEFI= IronPort-HdrOrdr: A9a23:d28VO6mVu2P/h0MA4q/Thn1nH3rpDfL03DAbv31ZSRFFG/FwWf rAoB19726TtN9xYgBGpTnuAsi9qB/nmKKdpLNhX4tKPzOW3FdAUrsD0WKK+VSJcEfDH6xmpM JdmsNFZuEYY2IXsS+D2njaL/8QhP+a7auvmeDSi11pTQ1sduVcyj0RMHfjLqWzLzM2fqbQ0/ Gnl7J6mwY= X-Talos-CUID: 9a23:1adXZWnFAM1Ak/7g1CXRGeDL3UrXOXKMkU/rDkaGMDZKV+eoF2+/84pHnMU7zg== X-Talos-MUID: 9a23:xoeM/A9QIgLXG9o65bZdN1OQf/xv/5qtVkJOqLUD4JG8JT5hEWi4qjviFw== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="823444660" Received: from alln-l-core-12.cisco.com ([173.36.16.149]) by alln-iport-6.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-12.cisco.com (Postfix) with ESMTPS id 37DAD18000149; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id C5E16CCD9B2; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 1/9] python3-aiohttp: fix CVE-2025-69224 Date: Sun, 30 Aug 2026 21:57:36 -0700 Message-Id: <20260831045744.3321483-2-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-12.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129602 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1] and [2]. The advisory identifying the fix is referenced in [3]. The generated aiohttp/_http_parser.c changes are omitted. Add python3-cython-native and regenerate the C source from the patched _http_parser.pyx during do_configure. [1] https://github.com/aio-libs/aiohttp/commit/32677f2adfd907420c078dda6b79225c6f4ebce0 [2] https://github.com/aio-libs/aiohttp/commit/5affd64f86d28a16a8f8e6fea2d217c99bf7831f [3] https://nvd.nist.gov/vuln/detail/CVE-2025-69224 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2025-69224.patch | 161 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 10 ++ 2 files changed, 171 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69224.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69224.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69224.patch new file mode 100644 index 0000000000..d1a830e077 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69224.patch @@ -0,0 +1,161 @@ +From 4c27b675ef3d1c5b3b0f379525be575ec0d8d15e Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sat, 3 Jan 2026 00:02:45 +0000 +Subject: [PATCH] Reject non-ascii characters in some headers (#11886) (#11902) + +CVE: CVE-2025-69224 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/32677f2adfd907420c078dda6b79225c6f4ebce0] + +Backport Changes: +- Adapted the pure-Python `Transfer-Encoding` validation inline because + aiohttp 3.9.5 lacks the upstream `_is_chunked_te()` call path. +- Backported the request/response upgrade distinction needed to apply the + non-ASCII request-header check while preserving CONNECT and HTTP 101 + response handling. +- Added the `ALLOWED_UPGRADES` definition from upstream prerequisite commit + c99a1e27375285149ea82cbdcc2f2c40e57596dc because aiohttp 3.9.5 + predates it and the Cython parser otherwise fails to compile. +- Retained aiohttp 3.9.5's supported `gzip`, `deflate`, and `br` content + encodings; omitted upstream zstd-specific code and test context because + this version lacks zstd decompression support. +- Retained target-compatible `Any` test annotations because the older test + module does not import `HttpRequestParser`. + +(cherry picked from commit 5affd64f86d28a16a8f8e6fea2d217c99bf7831f) +(cherry picked from commit 32677f2adfd907420c078dda6b79225c6f4ebce0) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/_http_parser.pyx | 18 +++++++++++++----- + aiohttp/http_parser.py | 8 +++++--- + tests/test_http_parser.py | 32 ++++++++++++++++++++++++++++++-- + 3 files changed, 48 insertions(+), 10 deletions(-) + +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index 7ea9b32ca..f1c130395 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -47,6 +47,7 @@ include "_headers.pxi" + + from aiohttp cimport _find_header + ++ALLOWED_UPGRADES = frozenset({"websocket"}) + DEF DEFAULT_FREELIST_SIZE = 250 + + cdef extern from "Python.h": +@@ -425,8 +426,14 @@ cdef class HttpParser: + raw_headers = tuple(self._raw_headers) + headers = CIMultiDictProxy(self._headers) + +- if upgrade or self._cparser.method == cparser.HTTP_CONNECT: +- self._upgraded = True ++ if self._cparser.type == cparser.HTTP_REQUEST: ++ 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 ++ else: ++ if upgrade and self._cparser.status_code == 101: ++ self._upgraded = True + + # do not support old websocket spec + if SEC_WEBSOCKET_KEY1 in headers: +@@ -436,9 +443,10 @@ cdef class HttpParser: + enc = self._content_encoding + if enc is not None: + self._content_encoding = None +- enc = enc.lower() +- if enc in ('gzip', 'deflate', 'br'): +- encoding = enc ++ if enc.isascii(): ++ enc = enc.lower() ++ if enc in ('gzip', 'deflate', 'br'): ++ encoding = enc + + if self._cparser.type == cparser.HTTP_REQUEST: + msg = _new_request_message( +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index 0a80c5c6d..5768bd623 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -232,7 +232,9 @@ class HeadersParser: + + def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool: + """Check if the upgrade header is supported.""" +- return headers.get(hdrs.UPGRADE, "").lower() in {"tcp", "websocket"} ++ u = headers.get(hdrs.UPGRADE, "") ++ # .lower() can transform non-ascii characters. ++ return u.isascii() and u.lower() in {"tcp", "websocket"} + + + class HttpParser(abc.ABC, Generic[_MsgT]): +@@ -542,7 +544,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + + # encoding + enc = headers.get(hdrs.CONTENT_ENCODING) +- if enc: ++ if enc and enc.isascii(): + enc = enc.lower() + if enc in ("gzip", "deflate", "br"): + encoding = enc +@@ -550,7 +552,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + # chunking + te = headers.get(hdrs.TRANSFER_ENCODING) + if te is not None: +- if "chunked" == te.lower(): ++ if te.isascii() and "chunked" == te.lower(): + chunked = True + else: + raise BadHttpMessage("Request has invalid `Transfer-Encoding`") +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 2f34f0bc0..021b6e4ae 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -482,6 +482,20 @@ def test_request_chunked(parser) -> None: + assert isinstance(payload, streams.StreamReader) + + ++def test_te_header_non_ascii(parser: Any) -> None: ++ # K = Kelvin sign, not valid ascii. ++ text = "GET /test HTTP/1.1\r\nTransfer-Encoding: chunKed\r\n\r\n" ++ with pytest.raises(http_exceptions.BadHttpMessage): ++ parser.feed_data(text.encode()) ++ ++ ++def test_upgrade_header_non_ascii(parser: Any) -> None: ++ # K = Kelvin sign, not valid ascii. ++ text = "GET /test HTTP/1.1\r\nUpgrade: websocKet\r\n\r\n" ++ messages, upgrade, tail = parser.feed_data(text.encode()) ++ assert not upgrade ++ ++ + def test_request_te_chunked_with_content_length(parser: Any) -> None: + text = ( + b"GET /test HTTP/1.1\r\n" +@@ -555,8 +569,22 @@ def test_compression_brotli(parser) -> None: + assert msg.compression == "br" + + +-def test_compression_unknown(parser) -> None: +- text = b"GET /test HTTP/1.1\r\n" b"content-encoding: compress\r\n\r\n" ++@pytest.mark.parametrize( ++ "enc", ++ ( ++ "deflate".encode(), # "fl".upper() == "FL" ++ ), ++) ++def test_compression_non_ascii(parser: Any, enc: bytes) -> None: ++ text = b"GET /test HTTP/1.1\r\ncontent-encoding: " + enc + b"\r\n\r\n" ++ messages, upgrade, tail = parser.feed_data(text) ++ msg = messages[0][0] ++ # Non-ascii input should not evaluate to a valid encoding scheme. ++ assert msg.compression is None ++ ++ ++def test_compression_unknown(parser: Any) -> None: ++ text = b"GET /test HTTP/1.1\r\ncontent-encoding: compress\r\n\r\n" + messages, upgrade, tail = parser.feed_data(text) + msg = messages[0][0] + assert msg.compression is None +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index 84a9f2e668..ffdc25791b 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -8,6 +8,7 @@ SRC_URI[sha256sum] = "edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2025-53643.patch \ + file://CVE-2025-69224.patch \ file://CVE-2025-69225.patch \ file://CVE-2025-69226.patch \ file://CVE-2025-69228.patch \ @@ -16,6 +17,15 @@ SRC_URI += "file://CVE-2024-52304.patch \ PYPI_PACKAGE = "aiohttp" inherit python_setuptools_build_meta pypi +DEPENDS += "python3-cython-native" + +do_configure:prepend() { + cython3 -3 -Werror \ + -I ${S}/aiohttp \ + -o ${S}/aiohttp/_http_parser.c \ + ${S}/aiohttp/_http_parser.pyx +} + RDEPENDS:${PN} = "\ python3-aiohappyeyeballs \ python3-aiosignal \ From patchwork Mon Aug 31 04:57:37 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96882 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 3B339C624CD for ; Mon, 31 Aug 2026 04:57:52 +0000 (UTC) Received: from alln-iport-4.cisco.com (alln-iport-4.cisco.com [173.37.142.91]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.23003.1788152269274811978 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=hB4TD05F; spf=pass (domain: cisco.com, ip: 173.37.142.91, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=7802; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=yvNIlaahXAFNHC6RAXEVRnfNJqxlQty/6HpkoP+kFEY=; b=hB4TD05FjOdCqBhUbMxt0KLenbLWxstfsdSF/FGCasw2ZWN26R00HPAL kSwkX96xATfNzpqsNMIUBgAvLzCHvXqZCp21kf8OxtqEAlTHz/tf9rHWt 9pThyU507VIZtQJtxH3i2375dIMJfaKNdS1pI/phboJ2sSok7xJhI+TC0 liCTkZhXO1diIGzG9+naavhk8A18CPHsnlP9vtzB97nxjS0/aQCMMen22 IUk34pnNJYdu55sJWBk1thdH97A5ZQ2/RTK4pmIUWFQMr/5aF6Kh7zMR0 bRlXvGolZ2EfL01AOJq68Zb6DPOj4xU+Bee7TnpOYYBQ3Jj88VUmXaXsZ A==; X-CSE-ConnectionGUID: qpQE+WA3Q1e2Nv/oMCxlDw== X-CSE-MsgGUID: mmcSRXN2Toy6/fjHfE2SXA== X-IPAS-Result: A0BHAgAsCZVq/5QQJK1aHgEBCxIMggULgld0XkNJlkoDnhuBfg8BAQEPRA0EAQGEP0YCjXMCJjQJDgECBAMCAwEBAQEBAQEBAQEBCwEBBQEBAQIBBwWBDhOGTw2GWgECAQMnCwEYAS0QHAMBAi8rIwgZgwIBgnQDEcIBgXkzgQGDKAE/AkNQ2zABCxQBgTiFP4MDgRCED10YAYR8JxsbgXKEfoEFgVwBAYglBIIigQyBWolfiEBIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQ0bBgWBHYEohA8jGTZ6gQlegSspYAESF4EJgggCglqCBQIBSUMOB0dTCQQLGA1IESw3FRkEPm4HjnofgksBcxoBCiGBNGBhApJ0kBaCIaEPCiiDdowilToaM4QEgVeSQJJRC5h9jgqVYBNdhGmBaDyBRwsHcBWDIglKGQ+OKgMLC4NggX+CUYEUxlUnMgIJMgEBBwIHDgMLgWiRfgEB IronPort-Data: A9a23:8cR5bqxrBL8Zuavnxg16t+dmxyrEfRIJ4+MujC+fZmUNrF6WrkUDn 2RJWTqDa/eLYGqmLttyPY6x9UxV7JbRmNRhGlc4r1hgHilAwSbn6Xt1DatR0we6dJCroJdPt p1GAjX4BJlqCCea/VH1buSJQUBUjcmgXqD7BPPPJhd/TAplTDZJoR94kobVuKYw6TSCK13L4 46aT/H3Ygf/hWYkaTpMsspvlTs21BjMkGJA1rABTagjUG/2zxE9EJ8ZLKetGHr0KqE8NvK6X evK0Iai9Wrf+Ro3Yvv9+losWhRXKlJ6FVHmZkt+A8BOsDAbzsAB+vpT2M4nVKtio27hc+adZ zl6ncfYpQ8BZsUgkQmGOvVSO3kW0aZuoNcrLZUj2CCe5xWuTpfi/xlhJFguBKBbxc8sODhDr 9oGbzcoRQCHq/3jldpXSsE07igiBMDvOIVavjRryivUSK9/B5vCWK7No9Rf2V/chOgXQq2YP JRfMGQpNUiQC/FMEg9/5JYWkOSlgnD+YjRwo1OOrq1x6G/WpOB0+Oi3bIaFJ4zTHq25mG6do 2n9xVTfWC0dJYLYxzil/F/2vd/myHaTtIU6UefQGuRRqFqLy2oeDRcbWVe2rby1h1CzX/pbK lcI4WwptaU0+UmhQ9XxUhH+p2SL1iPwQPJZF+k8rQXIwa3O7kPAXy4PTyVKb5ots8peqSEW6 2JlVujBXVRH2IB5g1rEnltIhVte4RQoEFI= IronPort-HdrOrdr: A9a23:08KsJ6gzAHiz4e2cppWHqwfBOnBQXvgji2hC6mlwRA09TyVXra +TdZMgpHjJYVkqOU3I9ersBEDEewK/yXcX2/h0AV7dZmnbUQKTRekIh7cKgQeQfhEWndQy6U 4PScRD4aXLfDtHpPe/xhWkGNA9x9TC2qWpieDCi0pJd2hRGsddB8MTMHfiLqWwLzM2fKYEKA == X-Talos-CUID: 9a23:IKAJ+mHZ2iLcwPBAqmJ+7Hc5Wf8MKUSEwS32PQi/LE15TJGKHAo= X-Talos-MUID: 9a23:7zdnrQoSkY4X+4IgwJwezzRnP/xS04L0NEccz9IIuemKGHZLCx7I2Q== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="824485813" Received: from alln-l-core-11.cisco.com ([173.36.16.148]) by alln-iport-4.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-11.cisco.com (Postfix) with ESMTPS id 377FD180000B5; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id CA5A9CD02B9; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 2/9] python3-aiohttp: fix CVE-2025-69227 Date: Sun, 30 Aug 2026 21:57:37 -0700 Message-Id: <20260831045744.3321483-3-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-11.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129600 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1] and [2]. The advisory identifying the fix is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/bc1319ec3cbff9438a758951a30907b072561259 [2] https://github.com/aio-libs/aiohttp/commit/d5bf65f15c0c718b6b95e9bc9d0914a92c51e60f [3] https://nvd.nist.gov/vuln/detail/CVE-2025-69227 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2025-69227.patch | 163 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 164 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69227.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69227.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69227.patch new file mode 100644 index 0000000000..8cc045933b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69227.patch @@ -0,0 +1,163 @@ +From b05e3498d7b7b48f7b3a512a638fabea4ca4cd9f Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sat, 3 Jan 2026 04:53:29 +0000 +Subject: [PATCH] Replace asserts with exceptions (#11897) (#11914) + +CVE: CVE-2025-69227 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/bc1319ec3cbff9438a758951a30907b072561259] + +Backport Changes: +- Adapted the _read_chunk_from_stream() EOF check to aiohttp 3.9.5, + where _content_eof is updated outside the newer upstream read loop. +- Imported CIMultiDict and CIMultiDictProxy explicitly because + aiohttp 3.9.5's test_multipart module does not import them. + +(cherry picked from commit d5bf65f15c0c718b6b95e9bc9d0914a92c51e60f) + +Co-authored-by: J. Nick Koston +(cherry picked from commit bc1319ec3cbff9438a758951a30907b072561259) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/multipart.py | 10 ++++------ + aiohttp/web_request.py | 8 +++----- + tests/test_multipart.py | 13 ++++++++++++- + tests/test_web_request.py | 24 +++++++++++++++++++++++- + 4 files changed, 42 insertions(+), 13 deletions(-) + +diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py +index 520ee539e..9e5ff9b41 100644 +--- a/aiohttp/multipart.py ++++ b/aiohttp/multipart.py +@@ -325,11 +325,8 @@ class BodyPartReader: + self._read_bytes += len(chunk) + if self._read_bytes == self._length: + self._at_eof = True +- if self._at_eof: +- clrf = await self._content.readline() +- assert ( +- b"\r\n" == clrf +- ), "reader did not read all the data or it is malformed" ++ if self._at_eof and await self._content.readline() != b"\r\n": ++ raise ValueError("Reader did not read all the data or it is malformed") + return chunk + + async def _read_chunk_from_length(self, size: int) -> bytes: +@@ -354,7 +351,8 @@ class BodyPartReader: + + chunk = await self._content.read(size) + self._content_eof += int(self._content.at_eof()) +- assert self._content_eof < 3, "Reading after EOF" ++ if self._content_eof > 2: ++ raise ValueError("Reading after EOF") + assert self._prev_chunk is not None + window = self._prev_chunk + chunk + sub = b"\r\n" + self._boundary +diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py +index b3d614186..cd77b7bde 100644 +--- a/aiohttp/web_request.py ++++ b/aiohttp/web_request.py +@@ -713,12 +713,12 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + max_size = self._client_max_size + + size = 0 +- field = await multipart.next() +- while field is not None: ++ while (field := await multipart.next()) is not None: + field_ct = field.headers.get(hdrs.CONTENT_TYPE) + + if isinstance(field, BodyPartReader): +- assert field.name is not None ++ if field.name is None: ++ raise ValueError("Multipart field missing name.") + + # Note that according to RFC 7578, the Content-Type header + # is optional, even for files, so we can't assume it's +@@ -770,8 +770,6 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + raise ValueError( + "To decode nested multipart you need " "to use custom reader", + ) +- +- field = await multipart.next() + else: + data = await self.read() + if data: +diff --git a/tests/test_multipart.py b/tests/test_multipart.py +index 436b70957..e46eb29bd5 100644 +--- a/tests/test_multipart.py ++++ b/tests/test_multipart.py +@@ -6,6 +6,7 @@ + from unittest import mock + + import pytest ++from multidict import CIMultiDict, CIMultiDictProxy + + import aiohttp + from aiohttp import payload +@@ -200,11 +200,21 @@ class TestPartReader: + with Stream(data) as stream: + obj = aiohttp.BodyPartReader(BOUNDARY, {}, stream) + result = b"" +- with pytest.raises(AssertionError): ++ with pytest.raises(ValueError): + for _ in range(4): + result += await obj.read_chunk(7) + assert data == result + ++ async def test_read_with_content_length_malformed_crlf(self) -> None: ++ # Content-Length is correct but data after content is not \r\n ++ content = b"Hello" ++ h = CIMultiDictProxy(CIMultiDict({"CONTENT-LENGTH": str(len(content))})) ++ # Malformed: "XX" instead of "\r\n" after content ++ with Stream(content + b"XX--:--") as stream: ++ obj = aiohttp.BodyPartReader(BOUNDARY, h, stream) ++ with pytest.raises(ValueError, match="malformed"): ++ await obj.read() ++ + async def test_read_boundary_with_incomplete_chunk(self) -> None: + with Stream(b"") as stream: + +diff --git a/tests/test_web_request.py b/tests/test_web_request.py +index 704fc189a..962092999 100644 +--- a/tests/test_web_request.py ++++ b/tests/test_web_request.py +@@ -10,6 +10,7 @@ from multidict import CIMultiDict, CIMultiDictProxy, MultiDict + from yarl import URL + + from aiohttp import HttpVersion ++from aiohttp.base_protocol import BaseProtocol + from aiohttp.http_parser import RawRequestMessage + from aiohttp.streams import StreamReader + from aiohttp.test_utils import make_mocked_request +@@ -629,7 +630,28 @@ async def test_multipart_formdata(protocol) -> None: + assert dict(result) == {"a": "b", "c": "d"} + + +-async def test_multipart_formdata_file(protocol) -> None: ++async def test_multipart_formdata_field_missing_name(protocol: BaseProtocol) -> None: ++ # Ensure ValueError is raised when Content-Disposition has no name ++ payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) ++ payload.feed_data( ++ b"-----------------------------326931944431359\r\n" ++ b"Content-Disposition: form-data\r\n" # Missing name! ++ b"\r\n" ++ b"value\r\n" ++ b"-----------------------------326931944431359--\r\n" ++ ) ++ content_type = ( ++ "multipart/form-data; boundary=---------------------------326931944431359" ++ ) ++ payload.feed_eof() ++ req = make_mocked_request( ++ "POST", "/", headers={"CONTENT-TYPE": content_type}, payload=payload ++ ) ++ with pytest.raises(ValueError, match="Multipart field missing name"): ++ await req.post() ++ ++ ++async def test_multipart_formdata_file(protocol: BaseProtocol) -> None: + # Make sure file uploads work, even without a content type + payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) + payload.feed_data( +-- +2.44.4 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index ffdc25791b..765796b8cd 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -12,6 +12,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2025-69225.patch \ file://CVE-2025-69226.patch \ file://CVE-2025-69228.patch \ + file://CVE-2025-69227.patch \ " PYPI_PACKAGE = "aiohttp" From patchwork Mon Aug 31 04:57:38 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96880 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id ECA6FC624C9 for ; Mon, 31 Aug 2026 04:57:51 +0000 (UTC) Received: from alln-iport-3.cisco.com (alln-iport-3.cisco.com [173.37.142.90]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.22958.1788152269429499428 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=gcQHYdZx; spf=pass (domain: cisco.com, ip: 173.37.142.90, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=15818; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=7GFiNmy6LGnB0nWc7KXsar3NRs8VKjyVJwkueT62OeI=; b=gcQHYdZxDQaMHW80haywSzHFExvnTVRaE6JDIOIJOzWC2rXfHJ5QWhPy XAblPaVKWb2Vpv1hyij5wQl+PLZ/FtfQ81Mfpa4MxG0l0kFdzF7/5CxXR S3A04FxW37ZxESb7xGGbAVMpKLbc+Mb1LflSTxpjmBGIng9Wpx6jr4rEb X6/Gq3OZ0ZmrhfsQcS1Pb+9GVFGjBohaLKwp/fDqyEEywok9b0xZ3IfH4 OzyalcX5/exivLDI3X2LIxtlsKL4XP2wI1ecRZN3q2SB0XTrFpCUE1xu6 5hbytykPyxp8dMsLjQy4ew4hVYxOZQhArScugei3OIzdFLzqjnzCk+N7F A==; X-CSE-ConnectionGUID: msKgOcuZS7+EGfKuIEPm4A== X-CSE-MsgGUID: g0HuDPdBSLOV0pOlWoi90A== X-IPAS-Result: A0DNAgCvCJVq/5UQJK1aglmCGD90XkNJlkoDi2SSN4F+DwEBAQ9EDQQBAYQ/RgKNcwImNQgOAQIEAwIDAQEBAQEBAQEBAQEBCgEBBQEBAQIBBwWBDhOGFQEHMg2GWgECAQMnCwEYAS0QHAMBAi8gCyMIGYIqWAGCOgM3AxHCA4F5M4EBgygBPwJDUNhLDYJYAQsUAYE4hT+CfwSBEIQPXRgBhHwnGxuBcoR+gQWBGkIBAYEnBguGbQSCInoSgVqBF4hIiEBIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQ0bBgWBHYEohA8jGTZ6gQlegSspYAESF4EJgggCglqCBQIBSUMOB0dTCQQLGA1IESw3FRkEPm4HjnofgVhTID03GgEqASJjPkEQUhGlK6AecQoog3aMIo8+hXwaM4QEgVeSQJJRC5h9jgqECZFXcIRpgWoCOIFHCwdwFTuCZwlKGQ+DN4oOZQ4Lg2CBf4NlxlUnMgIJMgEBBwIHDgMLgWiQAAEngVYBAQ IronPort-Data: A9a23:b+O+VKlfdG5Oq1W25bFWYgjo5gzQJ0RdPkR7XQ2eYbSJt1+Wr1Gzt xJKXmHQafaIZWWnLtF0PI22/UMEvJPUyoBlTVNqrChnHltH+JHPbTi7wugcHM8zwunrFh8PA xA2M4GYRCwMZiaC4Errav6+/SEUOZigHtLUEPTDNj16WThqQSIgjQMLs+Mii+aEu/Dha++2k Y20+ZC31GONgWYubDpEsPrb8XuDgdyr0N8mlg1mDRx0lAe2e0k9VPo3Oay3Jn3kdYhYdsbSb /rD1ryw4lTC9B4rDN6/+p6jGqHdauePVeQmoiM+t5mK2nCulARrukoIHKZ0hXNsttm8t4sZJ OOhGnCHYVxB0qXkwIzxWvTDes10FfUuFLTveRBTvSEPpqHLWyOE/hlgMK05FYgD2elQCnx8z qNbBR8DTQ2iqtqznJvuH4GAhux7RCXqFIobvnclyXTSCuwrBMmZBa7L/tRfmjw3g6iiH96HO JFfMmQpNUqGOkEeUrsUIMpWcOOAinrydzRZuVu9rqss6G+Vxwt0uFToGIqJI4faGpkExy50o ErJp0TkLikWHue77gTd6lWouejOzHvSDdd6+LqQs6QCbEeo7msLBRsbUFG2rfW0hguyVsxSL 2QQ+zEytu417EGtQ9z3UhG0rXLCuQQTM+e8CMUz7AWLj66R6AGDCy1dFHhKaccts4k9QjlCO kK1ou4FzAdH6NW9IU9xPJ/Oxd9uEUD59VM/WBI= IronPort-HdrOrdr: A9a23:3BAzI6D+kgQQHzLlHemO55DYdb4zR+YMi2TDGXofdfUzSL3+qy nAppUmPHPP5Qr5HUtQ++xoW5PwJU80i6QU3WB5B97LN2PbUSmTXeRfBODZrQEIdReTygck79 YCT4FOTPvtEFN9kcH2pCO8E9om3Z271ZrAv5a585+oJjsaE52JKGxCe3+mLnE= X-Talos-CUID: 9a23:3VsKH2PJsyJ/0O5DXTJE+2kfB/IeLT7w43nzPm6VMWM1YejA X-Talos-MUID: 9a23:j5EG0Qwr4br+l6m4hegqstkUhvmaqKv2KF0hqZksgO2jC3RoHDOiizXmGqZyfw== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="841387491" Received: from alln-l-core-12.cisco.com ([173.36.16.149]) by alln-iport-3.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-12.cisco.com (Postfix) with ESMTPS id 37E2818000159; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id CF22BCD02BA; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 3/9] python3-aiohttp: fix CVE-2025-69229 Date: Sun, 30 Aug 2026 21:57:38 -0700 Message-Id: <20260831045744.3321483-4-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-12.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129603 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1], [2], [3], and [4]. The advisory identifying the fix is referenced in [5]. [1] https://github.com/aio-libs/aiohttp/commit/dc3170b56904bdf814228fae70a5501a42a6c712 [2] https://github.com/aio-libs/aiohttp/commit/271532ea355c65480c8ecc14137dfbb72aec8f6f [3] https://github.com/aio-libs/aiohttp/commit/4ed97a4e46eaf61bd0f05063245f613469700229 [4] https://github.com/aio-libs/aiohttp/commit/1e4120e87daec963c67f956111e6bca44d7c3dea [5] https://nvd.nist.gov/vuln/detail/CVE-2025-69229 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2025-69229_p1.patch | 113 ++++++++ .../python3-aiohttp/CVE-2025-69229_p2.patch | 256 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 2 + 3 files changed, 371 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p2.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p1.patch new file mode 100644 index 0000000000..363b0c442e --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p1.patch @@ -0,0 +1,113 @@ +From cf6672ba61da0d4e52483ade0b06dea11cf4be55 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sat, 3 Jan 2026 03:57:17 +0000 +Subject: [PATCH] Use collections.deque for chunk splits (#11892) (#11912) + +CVE: CVE-2025-69229 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/dc3170b56904bdf814228fae70a5501a42a6c712] + +(cherry picked from commit 271532ea355c65480c8ecc14137dfbb72aec8f6f) + +--------- + +Co-authored-by: Finder +(cherry picked from commit dc3170b56904bdf814228fae70a5501a42a6c712) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/streams.py | 8 ++++---- + tests/test_http_parser.py | 14 +++++++++----- + 2 files changed, 13 insertions(+), 9 deletions(-) + +diff --git a/aiohttp/streams.py b/aiohttp/streams.py +index b9b9c3fd9..eb652ff45 100644 +--- a/aiohttp/streams.py ++++ b/aiohttp/streams.py +@@ -122,7 +122,7 @@ class StreamReader(AsyncStreamReaderMixin): + self._loop = loop + self._size = 0 + self._cursor = 0 +- self._http_chunk_splits: Optional[List[int]] = None ++ self._http_chunk_splits: Optional[Deque[int]] = None + self._buffer: Deque[bytes] = collections.deque() + self._buffer_offset = 0 + self._eof = False +@@ -263,7 +263,7 @@ class StreamReader(AsyncStreamReaderMixin): + raise RuntimeError( + "Called begin_http_chunk_receiving when" "some data was already fed" + ) +- self._http_chunk_splits = [] ++ self._http_chunk_splits = collections.deque() + + def end_http_chunk_receiving(self) -> None: + if self._http_chunk_splits is None: +@@ -419,7 +419,7 @@ class StreamReader(AsyncStreamReaderMixin): + raise self._exception + + while self._http_chunk_splits: +- pos = self._http_chunk_splits.pop(0) ++ pos = self._http_chunk_splits.popleft() + if pos == self._cursor: + return (b"", True) + if pos > self._cursor: +@@ -491,7 +491,7 @@ class StreamReader(AsyncStreamReaderMixin): + chunk_splits = self._http_chunk_splits + # Prevent memory leak: drop useless chunk splits + while chunk_splits and chunk_splits[0] < self._cursor: +- chunk_splits.pop(0) ++ chunk_splits.popleft() + + if self._size < self._low_water and self._protocol._reading_paused: + self._protocol.resume_reading() +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 0a868f286..62830c2bd 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -1188,7 +1188,8 @@ def test_http_request_chunked_payload(parser) -> None: + parser.feed_data(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n") + + assert b"dataline" == b"".join(d for d in payload._buffer) +- assert [4, 8] == payload._http_chunk_splits ++ assert payload._http_chunk_splits is not None ++ assert [4, 8] == list(payload._http_chunk_splits) + assert payload.is_eof() + + +@@ -1203,7 +1204,8 @@ def test_http_request_chunked_payload_and_next_message(parser) -> None: + ) + + assert b"dataline" == b"".join(d for d in payload._buffer) +- assert [4, 8] == payload._http_chunk_splits ++ assert payload._http_chunk_splits is not None ++ assert [4, 8] == list(payload._http_chunk_splits) + assert payload.is_eof() + + assert len(messages) == 1 +@@ -1227,12 +1229,13 @@ def test_http_request_chunked_payload_chunks(parser) -> None: + parser.feed_data(b"test: test\r\n") + + assert b"dataline" == b"".join(d for d in payload._buffer) +- assert [4, 8] == payload._http_chunk_splits ++ assert payload._http_chunk_splits is not None ++ assert [4, 8] == list(payload._http_chunk_splits) + assert not payload.is_eof() + + parser.feed_data(b"\r\n") + assert b"dataline" == b"".join(d for d in payload._buffer) +- assert [4, 8] == payload._http_chunk_splits ++ assert [4, 8] == list(payload._http_chunk_splits) + assert payload.is_eof() + + +@@ -1243,7 +1246,8 @@ def test_parse_chunked_payload_chunk_extension(parser) -> None: + parser.feed_data(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\ntest: test\r\n\r\n") + + assert b"dataline" == b"".join(d for d in payload._buffer) +- assert [4, 8] == payload._http_chunk_splits ++ assert payload._http_chunk_splits is not None ++ assert [4, 8] == list(payload._http_chunk_splits) + assert payload.is_eof() + + +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p2.patch new file mode 100644 index 0000000000..48c5695c6d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69229_p2.patch @@ -0,0 +1,256 @@ +From 3f47a9e32de41ca21d63510eec253f6e5aed55f2 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sat, 3 Jan 2026 15:23:14 +0000 +Subject: [PATCH] Limit number of chunks before pausing reading (#11894) + (#11916) + +CVE: CVE-2025-69229 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/4ed97a4e46eaf61bd0f05063245f613469700229] + +Backport Changes: +- Omitted the entire `StreamReader.__slots__` block because + aiohttp 3.9.5 does not use `__slots__` and allows instance + attributes through its normal instance dictionary. + The new `_high_water_chunks` and `_low_water_chunks` + attributes are initialized directly in `__init__`, so no + slots declaration is required. + +(cherry picked from commit 1e4120e87daec963c67f956111e6bca44d7c3dea) + +Co-authored-by: J. Nick Koston +(cherry picked from commit 4ed97a4e46eaf61bd0f05063245f613469700229) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/streams.py | 23 +++++- + tests/test_streams.py | 170 ++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 192 insertions(+), 1 deletion(-) + +diff --git a/aiohttp/streams.py b/aiohttp/streams.py +index eb652ff45..121528842 100644 +--- a/aiohttp/streams.py ++++ b/aiohttp/streams.py +@@ -119,6 +119,11 @@ class StreamReader(AsyncStreamReaderMixin): + self._high_water = limit * 2 + if loop is None: + loop = asyncio.get_event_loop() ++ # Ensure high_water_chunks >= 3 so it's always > low_water_chunks. ++ self._high_water_chunks = max(3, limit // 4) ++ # Use max(2, ...) because there's always at least 1 chunk split remaining ++ # (the current position), so we need low_water >= 2 to allow resume. ++ self._low_water_chunks = max(2, self._high_water_chunks // 2) + self._loop = loop + self._size = 0 + self._cursor = 0 +@@ -289,6 +294,15 @@ class StreamReader(AsyncStreamReaderMixin): + + self._http_chunk_splits.append(self.total_bytes) + ++ # If we get too many small chunks before self._high_water is reached, then any ++ # .read() call becomes computationally expensive, and could block the event loop ++ # for too long, hence an additional self._high_water_chunks here. ++ if ( ++ len(self._http_chunk_splits) > self._high_water_chunks ++ and not self._protocol._reading_paused ++ ): ++ self._protocol.pause_reading() ++ + # wake up readchunk when end of http chunk received + waiter = self._waiter + if waiter is not None: +@@ -493,7 +507,14 @@ class StreamReader(AsyncStreamReaderMixin): + while chunk_splits and chunk_splits[0] < self._cursor: + chunk_splits.popleft() + +- if self._size < self._low_water and self._protocol._reading_paused: ++ if ( ++ self._protocol._reading_paused ++ and self._size < self._low_water ++ and ( ++ self._http_chunk_splits is None ++ or len(self._http_chunk_splits) < self._low_water_chunks ++ ) ++ ): + self._protocol.resume_reading() + return data + +diff --git a/tests/test_streams.py b/tests/test_streams.py +index 115371c80..ed65b567a 100644 +--- a/tests/test_streams.py ++++ b/tests/test_streams.py +@@ -1550,3 +1550,173 @@ async def test_stream_reader_iter_chunks_chunked_encoding(protocol) -> None: + + def test_isinstance_check() -> None: + assert isinstance(streams.EMPTY_PAYLOAD, streams.StreamReader) ++ ++ ++async def test_stream_reader_pause_on_high_water_chunks( ++ protocol: mock.Mock, ++) -> None: ++ """Test that reading is paused when chunk count exceeds high water mark.""" ++ loop = asyncio.get_event_loop() ++ # Use small limit so high_water_chunks is small: limit // 4 = 10 ++ stream = streams.StreamReader(protocol, limit=40, loop=loop) ++ ++ assert stream._high_water_chunks == 10 ++ assert stream._low_water_chunks == 5 ++ ++ # Feed chunks until we exceed high_water_chunks ++ for i in range(12): ++ stream.begin_http_chunk_receiving() ++ stream.feed_data(b"x") # 1 byte per chunk ++ stream.end_http_chunk_receiving() ++ ++ # pause_reading should have been called when chunk count exceeded 10 ++ protocol.pause_reading.assert_called() ++ ++ ++async def test_stream_reader_resume_on_low_water_chunks( ++ protocol: mock.Mock, ++) -> None: ++ """Test that reading resumes when chunk count drops below low water mark.""" ++ loop = asyncio.get_event_loop() ++ # Use small limit so high_water_chunks is small: limit // 4 = 10 ++ stream = streams.StreamReader(protocol, limit=40, loop=loop) ++ ++ assert stream._high_water_chunks == 10 ++ assert stream._low_water_chunks == 5 ++ ++ # Feed chunks until we exceed high_water_chunks ++ for i in range(12): ++ stream.begin_http_chunk_receiving() ++ stream.feed_data(b"x") # 1 byte per chunk ++ stream.end_http_chunk_receiving() ++ ++ # Simulate that reading was paused ++ protocol._reading_paused = True ++ protocol.pause_reading.reset_mock() ++ ++ # Read data to reduce both size and chunk count ++ # Reading will consume chunks and reduce _http_chunk_splits ++ data = await stream.read(10) ++ assert data == b"xxxxxxxxxx" ++ ++ # resume_reading should have been called when both size and chunk count ++ # dropped below their respective low water marks ++ protocol.resume_reading.assert_called() ++ ++ ++async def test_stream_reader_no_resume_when_chunks_still_high( ++ protocol: mock.Mock, ++) -> None: ++ """Test that reading doesn't resume if chunk count is still above low water.""" ++ loop = asyncio.get_event_loop() ++ # Use small limit so high_water_chunks is small: limit // 4 = 10 ++ stream = streams.StreamReader(protocol, limit=40, loop=loop) ++ ++ # Feed many chunks ++ for i in range(12): ++ stream.begin_http_chunk_receiving() ++ stream.feed_data(b"x") ++ stream.end_http_chunk_receiving() ++ ++ # Simulate that reading was paused ++ protocol._reading_paused = True ++ ++ # Read only a few bytes - chunk count will still be high ++ data = await stream.read(2) ++ assert data == b"xx" ++ ++ # resume_reading should NOT be called because chunk count is still >= low_water_chunks ++ protocol.resume_reading.assert_not_called() ++ ++ ++async def test_stream_reader_read_non_chunked_response( ++ protocol: mock.Mock, ++) -> None: ++ """Test that non-chunked responses work correctly (no chunk tracking).""" ++ loop = asyncio.get_event_loop() ++ stream = streams.StreamReader(protocol, limit=40, loop=loop) ++ ++ # Non-chunked: just feed data without begin/end_http_chunk_receiving ++ stream.feed_data(b"Hello World") ++ ++ # _http_chunk_splits should be None for non-chunked responses ++ assert stream._http_chunk_splits is None ++ ++ # Reading should work without issues ++ data = await stream.read(5) ++ assert data == b"Hello" ++ ++ data = await stream.read(6) ++ assert data == b" World" ++ ++ ++async def test_stream_reader_resume_non_chunked_when_paused( ++ protocol: mock.Mock, ++) -> None: ++ """Test that resume works for non-chunked responses when paused due to size.""" ++ loop = asyncio.get_event_loop() ++ # Small limit so we can trigger pause via size ++ stream = streams.StreamReader(protocol, limit=10, loop=loop) ++ ++ # Feed data that exceeds high_water (limit * 2 = 20) ++ stream.feed_data(b"x" * 25) ++ ++ # Simulate that reading was paused due to size ++ protocol._reading_paused = True ++ protocol.pause_reading.assert_called() ++ ++ # Read enough to drop below low_water (limit = 10) ++ data = await stream.read(20) ++ assert data == b"x" * 20 ++ ++ # resume_reading should be called (size is now 5 < low_water 10) ++ protocol.resume_reading.assert_called() ++ ++ ++@pytest.mark.parametrize("limit", [1, 2, 4]) ++async def test_stream_reader_small_limit_resumes_reading( ++ protocol: mock.Mock, ++ limit: int, ++) -> None: ++ """Test that small limits still allow resume_reading to be called. ++ ++ Even with very small limits, high_water_chunks should be at least 3 ++ and low_water_chunks should be at least 2, with high > low to ensure ++ proper flow control. ++ """ ++ loop = asyncio.get_event_loop() ++ stream = streams.StreamReader(protocol, limit=limit, loop=loop) ++ ++ # Verify minimum thresholds are enforced and high > low ++ assert stream._high_water_chunks >= 3 ++ assert stream._low_water_chunks >= 2 ++ assert stream._high_water_chunks > stream._low_water_chunks ++ ++ # Set up pause/resume side effects ++ def pause_reading() -> None: ++ protocol._reading_paused = True ++ ++ protocol.pause_reading.side_effect = pause_reading ++ ++ def resume_reading() -> None: ++ protocol._reading_paused = False ++ ++ protocol.resume_reading.side_effect = resume_reading ++ ++ # Feed 4 chunks (triggers pause at > high_water_chunks which is >= 3) ++ for char in b"abcd": ++ stream.begin_http_chunk_receiving() ++ stream.feed_data(bytes([char])) ++ stream.end_http_chunk_receiving() ++ ++ # Reading should now be paused ++ assert protocol._reading_paused is True ++ assert protocol.pause_reading.called ++ ++ # Read all data - should resume (chunk count drops below low_water_chunks) ++ data = stream.read_nowait() ++ assert data == b"abcd" ++ assert stream._size == 0 ++ ++ protocol.resume_reading.assert_called() ++ assert protocol._reading_paused is False +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index 765796b8cd..69cad8cb44 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -12,6 +12,8 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2025-69225.patch \ file://CVE-2025-69226.patch \ file://CVE-2025-69228.patch \ + file://CVE-2025-69229_p1.patch \ + file://CVE-2025-69229_p2.patch \ file://CVE-2025-69227.patch \ " From patchwork Mon Aug 31 04:57:39 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96874 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id AD60EC624A4 for ; Mon, 31 Aug 2026 04:57:50 +0000 (UTC) Received: from alln-iport-5.cisco.com (alln-iport-5.cisco.com [173.37.142.92]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.22959.1788152269771553020 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=RfS5nV7t; spf=pass (domain: cisco.com, ip: 173.37.142.92, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=33266; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=MsnMvl9K2CW4ZlDEdCMMuFvrQ7tnIYCb/iseg6XYxPw=; b=RfS5nV7tQ2lIFPe7V0UMO01q/wLcYHVY6mwjUY2FVXOW83LXXChCUawS IC9i7zPhC/Z04DeJ2pnAkDjD5VJUSRo/1Xr3opt0neu1lkPW7a23/zfsC rxL7H4L0xmVL/GY2mNSWBM04haZarVUBp6Rn1EHtoPXWqB3HB3GXXs0Ci bGe7SyxCqHIKfUR3UzrnkAYnA/WpNig174JxwjVQw/mHut8qWB8c4a3Cz Jkjtxo7njd0qvWFJAPKYDiT7EEzJwl/29h8A9NXFmFGvn5x4M7PHfrxB5 9PZPNYUAfOQo1+ky0QOCYRKDLmP+U6IU590ojROERvadS/FvicbcyXcCh A==; X-CSE-ConnectionGUID: gOup6JJWTWap5PmvYQ04TQ== X-CSE-MsgGUID: vGxuAlEzR6SBmVSzoG0bsQ== X-IPAS-Result: A0ArAAAsCZVq/5MQJK1RCR0BAQEBCQESAQUFAYF8CAELAYJWdF4KOUmEV4gbiVgDnhuBfg8BAQEPRA0EAQGEP0YCjXMCJjQJDgECBAMCAwEBAQEBAQEBAQEBCwEBBQEBAQIBBwWBDhOGTw2GWgECAQMaAQgECwEYAS0QCRMDAQIDAiYCAisjCBmDAgGCdAMRpl6bI3p/M4EBgygBPwJDUNswAQsUAYEKLgGFPoMfAYUCXRgBRIQ4JxsbgXKCUIIugQWBXAEBAoErBwSEA4JqBIIigQyBWoEXkQhIgQIcA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+FzVYGwYFgR2BKIQPIxk2eoEJXoErKWABEheBCYIIAoJaggUCAUlDDgdHUwkECxgNSBEsNxUZBD0BbgeOeh+CBxcsAQFXAgctAQogAQSBDQc1NxAeCwsGLJJpAQcTHgqPW4IhgTWfWgoog3aMIpU6GjOEBIFXiz2HA5JRC5h9jgqVPSMgUIRpgWg8gUcLB3AVO4JnCQpAGQ+OKgMLC4NggX9Xgw7GVScyAgkyAQEHAgcOAwuBaJABgX0BAQ IronPort-Data: A9a23:sZ7p1aBvXxiaIxVW/3jiw5YqxClBgxIJ4kV8jS/XYbTApDMr0zUPn DFJW2DVPfaPNGX1ett1Ot/g8h9T7MLUmtZjOVdlrnsFo1CmBibm6XV1Cm+qYkt+++WaFBoPA /02M4eGdIZvCCeA+n9BC5C5xVFkz6aEW7HgP+DNPyF1VGdMRTwo4f5Zs7ZRbrVA357jXmthh fuo+5eBYA/+g2YtWo4pw/vrRC1H7ayaVAww5jTSVdgT1HfCmn8cCo4oJK3ZBxPQXolOE+emc P3Ixbe/83mx109F5gSNy+uTnuUiG9Y+DCDW4pZkc/HKbitq+kTe5p0G2M80Mi+7vdkmc+dZk 72hvbToIesg0zaldO41C3G0GAkmVUFKFSOuzXWX6aSuI0P6n3TEzNJzFB8UH7Ikxe9vXFlL6 80hcGwxV0XW7w626OrTpuhEj8AnKozveYgYoHwllGmfBvc9SpeFSKLPjTNa9G5v3YYVQ7CHO YxANWUHgBfoO3WjPn8bC586lea5j1H0ciZTrxSeoq9fD237nFYsj+O3boOLEjCMbdt6jluxo k/UxTrCKBQkD86Y5mKP6Uv504cjmgu+Aur+DoaQ8eZnhlCWzGEfBBAaEFe2v/S9okq/QM5Eb UsM9ywjqKI/+ECmQp/6RRLQnZKflhcYX9wVF6gx7xuAj/OMpQ2YHWMDCDVGbbTKqfMLeNDj7 XfR9/uBONClmOf9pa61nltMkQ6PBA== IronPort-HdrOrdr: A9a23:spPnrq2AIMCcHhDohS5twAqjBGokLtp133Aq2lEZdPWaSKOlfq eV7ZImPH7P+VEssR4b+OxoVJPsfZqYz+8W3WBzB8bHYOCZgguVxehZhOOIqQEIWReOk9K1vp 0PT0ERMrHN5CBB/KXHyTj9Nco8y9+a963tr+Lfw3BxCTxOUchbnn5E4sLxKDwMeOGAbqBJbK ah2g== X-Talos-CUID: 9a23:0Shn0G4g9R7fbb/hRNss+2kJAp4HUELh837Ifne9F1dQEOGpcArF X-Talos-MUID: 9a23:W7QLzg/wXC8VOSrNQyvaOrKQf9lH87mHDG0fq8s9kcy6Dj53FG+Xlw3iFw== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="825732616" Received: from alln-l-core-10.cisco.com ([173.36.16.147]) by alln-iport-5.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-10.cisco.com (Postfix) with ESMTPS id 448E71800014C; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id D3DA4CD02BB; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 4/9] python3-aiohttp: fix CVE-2025-69223 Date: Sun, 30 Aug 2026 21:57:39 -0700 Message-Id: <20260831045744.3321483-5-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-10.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:50 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129608 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1] and [2]. The advisory identifying the fix is referenced in [3]. The python3-brotli 1.2.0 upgrade commit [4] is omitted because Scarthgap currently provides python3-brotli 1.1.0. Brotli 1.2.0 adds the bounded decompression API required by the upstream aiohttp fix. Consequently, this backport disables optional Brotli response decoding while retaining bounded decompression for the supported zlib path. [1] https://github.com/aio-libs/aiohttp/commit/2b920c39002cee0ec5b402581779bbaaf7c9138a [2] https://github.com/aio-libs/aiohttp/commit/92477c5a74c43dfe0474bd24f8de11875daa2298 [3] https://nvd.nist.gov/vuln/detail/CVE-2025-69223 [4] https://github.com/openembedded/meta-openembedded/commit/382e4de7d8b7d0e980fefcda7a06e5f20f5f26c0 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2025-69223.patch | 848 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 849 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69223.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69223.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69223.patch new file mode 100644 index 0000000000..e1fca9b8b9 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2025-69223.patch @@ -0,0 +1,848 @@ +From 7c9340cd2be5c8dd6d829a62220bb9129e7a9d8a Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sat, 3 Jan 2026 15:56:02 +0000 +Subject: [PATCH] Use decompressor max_length parameter (#11898) (#11918) + +--------- + +CVE: CVE-2025-69223 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/2b920c39002cee0ec5b402581779bbaaf7c9138a] + +Backport Changes: +- Adapted the decompression base class to aiohttp 3.9.5's + direct `zlib` implementation because the newer upstream + buffer and backend wrappers are absent. +- Kept `BodyPartReader.decode()` and `_decode_content()` + synchronous. Applied the output limit through + `ZLibDecompressor.decompress_sync()` to preserve the + aiohttp 3.9.5 API while bounding gzip and deflate output. +- Omitted the upstream `aiohttp/web_request.py` change from + `field.decode(chunk)` to `await field.decode(chunk)`. + That change is required only for upstream's asynchronous + `BodyPartReader.decode()` conversion. This backport keeps + synchronous decoding, so the existing call remains valid. +- Did not carry the Brotli 1.2 dependency updates from + `pyproject.toml` and `requirements/runtime-deps.in`. + Scarthgap supplies python3-brotli 1.1.0, which lacks the + bounded-output API required by the upstream fix. +- Disabled Brotli decoding by forcing `HAS_BROTLI` to + `False`. As a result, `Content-Encoding: br` is rejected + before a Brotli decoder is created. This closes the + decompression-bomb path at the cost of Brotli support. +- Omitted upstream Zstandard implementation, dependency, + and test changes because aiohttp 3.9.5 does not support + Zstandard content decoding. +- Adapted the client, parser, and multipart tests to the + aiohttp 3.9.5 fixtures and synchronous decoding API. + +(cherry picked from commit 92477c5a74c43dfe0474bd24f8de11875daa2298) +Co-authored-by: J. Nick Koston +(cherry picked from commit 2b920c39002cee0ec5b402581779bbaaf7c9138a) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/11898.breaking.rst | 3 ++ + aiohttp/compression_utils.py | 92 +++++++++++++++++++++------------ + aiohttp/http_exceptions.py | 4 ++ + aiohttp/http_parser.py | 29 +++++++++-- + aiohttp/multipart.py | 19 +++++-- + docs/spelling_wordlist.txt | 1 + + tests/test_client_functional.py | 80 +++++++++++++++++++++++++++- + tests/test_http_parser.py | 44 +++++++++++++++- + tests/test_multipart.py | 92 +++++++++++++++++++++++++-------- + 9 files changed, 299 insertions(+), 65 deletions(-) + create mode 100644 CHANGES/11898.breaking.rst + +diff --git a/CHANGES/11898.breaking.rst b/CHANGES/11898.breaking.rst +new file mode 100644 +index 000000000..228b69baa +--- /dev/null ++++ b/CHANGES/11898.breaking.rst +@@ -0,0 +1,3 @@ ++``Brotli`` decoding is disabled in the Scarthgap backport because its ++``python3-brotli`` recipe provides version 1.1.0, not the required 1.2. ++Decompression now has a default maximum output size of 32MiB per decompress call -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py +index 9631d377e..fe762c755 100644 +--- a/aiohttp/compression_utils.py ++++ b/aiohttp/compression_utils.py +@@ -1,5 +1,6 @@ + import asyncio + import zlib ++from abc import ABC, abstractmethod + from concurrent.futures import Executor + from typing import Optional, cast + +@@ -13,7 +14,17 @@ try: + except ImportError: # pragma: no cover + HAS_BROTLI = False + +-MAX_SYNC_CHUNK_SIZE = 1024 ++# Scarthgap provides python3-brotli 1.1.0, whose Decompressor API does not ++# support the max_length argument required by the bounded decoder below. ++# Do not advertise or instantiate Brotli decoding until the recipe provides ++# Brotli 1.2 or newer. ++HAS_BROTLI = False ++ ++MAX_SYNC_CHUNK_SIZE = 4096 ++DEFAULT_MAX_DECOMPRESS_SIZE = 2**25 # 32MiB ++ ++# Unlimited decompression constant ++ZLIB_MAX_LENGTH_UNLIMITED = 0 # zlib uses 0 to mean unlimited + + + def encoding_to_mode( +@@ -26,19 +37,37 @@ def encoding_to_mode( + return -zlib.MAX_WBITS if suppress_deflate_header else zlib.MAX_WBITS + + +-class ZlibBaseHandler: ++class DecompressionBaseHandler(ABC): + def __init__( + self, +- mode: int, + executor: Optional[Executor] = None, + max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE, + ): +- self._mode = mode ++ """Base class for decompression handlers.""" + self._executor = executor + self._max_sync_chunk_size = max_sync_chunk_size + ++ @abstractmethod ++ def decompress_sync( ++ self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ++ ) -> bytes: ++ """Decompress the given data.""" ++ ++ async def decompress( ++ self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ++ ) -> bytes: ++ """Decompress the given data.""" ++ if ( ++ self._max_sync_chunk_size is not None ++ and len(data) > self._max_sync_chunk_size ++ ): ++ return await asyncio.get_event_loop().run_in_executor( ++ self._executor, self.decompress_sync, data, max_length ++ ) ++ return self.decompress_sync(data, max_length) ++ + +-class ZLibCompressor(ZlibBaseHandler): ++class ZLibCompressor: + def __init__( + self, + encoding: Optional[str] = None, +@@ -49,12 +78,12 @@ class ZLibCompressor(ZlibBaseHandler): + executor: Optional[Executor] = None, + max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE, + ): +- super().__init__( +- mode=encoding_to_mode(encoding, suppress_deflate_header) ++ self._executor = executor ++ self._max_sync_chunk_size = max_sync_chunk_size ++ self._mode = ( ++ encoding_to_mode(encoding, suppress_deflate_header) + if wbits is None +- else wbits, +- executor=executor, +- max_sync_chunk_size=max_sync_chunk_size, ++ else wbits + ) + if level is None: + self._compressor = zlib.compressobj(wbits=self._mode, strategy=strategy) +@@ -86,7 +115,7 @@ class ZLibCompressor(ZlibBaseHandler): + return self._compressor.flush(mode) + + +-class ZLibDecompressor(ZlibBaseHandler): ++class ZLibDecompressor(DecompressionBaseHandler): + def __init__( + self, + encoding: Optional[str] = None, +@@ -94,26 +123,15 @@ class ZLibDecompressor(ZlibBaseHandler): + executor: Optional[Executor] = None, + max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE, + ): +- super().__init__( +- mode=encoding_to_mode(encoding, suppress_deflate_header), +- executor=executor, +- max_sync_chunk_size=max_sync_chunk_size, +- ) ++ super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size) ++ self._mode = encoding_to_mode(encoding, suppress_deflate_header) + self._decompressor = zlib.decompressobj(wbits=self._mode) + +- def decompress_sync(self, data: bytes, max_length: int = 0) -> bytes: ++ def decompress_sync( ++ self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ++ ) -> bytes: + return self._decompressor.decompress(data, max_length) + +- async def decompress(self, data: bytes, max_length: int = 0) -> bytes: +- if ( +- self._max_sync_chunk_size is not None +- and len(data) > self._max_sync_chunk_size +- ): +- return await asyncio.get_event_loop().run_in_executor( +- self._executor, self.decompress_sync, data, max_length +- ) +- return self.decompress_sync(data, max_length) +- + def flush(self, length: int = 0) -> bytes: + return ( + self._decompressor.flush(length) +@@ -134,24 +152,34 @@ class ZLibDecompressor(ZlibBaseHandler): + return self._decompressor.unused_data + + +-class BrotliDecompressor: ++class BrotliDecompressor(DecompressionBaseHandler): + # Supports both 'brotlipy' and 'Brotli' packages + # since they share an import name. The top branches + # are for 'brotlipy' and bottom branches for 'Brotli' +- def __init__(self) -> None: ++ def __init__( ++ self, ++ executor: Optional[Executor] = None, ++ max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE, ++ ) -> None: ++ """Decompress data using the Brotli library.""" + if not HAS_BROTLI: + raise RuntimeError( + "The brotli decompression is not available. " + "Please install `Brotli` module" + ) + self._obj = brotli.Decompressor() ++ super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size) + +- def decompress_sync(self, data: bytes) -> bytes: ++ def decompress_sync( ++ self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED ++ ) -> bytes: ++ """Decompress the given data.""" + if hasattr(self._obj, "decompress"): +- return cast(bytes, self._obj.decompress(data)) +- return cast(bytes, self._obj.process(data)) ++ return cast(bytes, self._obj.decompress(data, max_length)) ++ return cast(bytes, self._obj.process(data, max_length)) + + def flush(self) -> bytes: ++ """Flush the decompressor.""" + if hasattr(self._obj, "flush"): + return cast(bytes, self._obj.flush()) + return b"" +diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py +index 72eac3a3c..877b07d4c 100644 +--- a/aiohttp/http_exceptions.py ++++ b/aiohttp/http_exceptions.py +@@ -75,6 +75,10 @@ class ContentLengthError(PayloadEncodingError): + """Not enough data for satisfy content length header.""" + + ++class DecompressSizeError(PayloadEncodingError): ++ """Decompressed size exceeds the configured limit.""" ++ ++ + class LineTooLong(BadHttpMessage): + def __init__( + self, line: str, limit: str = "Unknown", actual_size: str = "Unknown" +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index 5768bd623..cdf3fc89a 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -26,7 +26,12 @@ from yarl import URL + + from . import hdrs + from .base_protocol import BaseProtocol +-from .compression_utils import HAS_BROTLI, BrotliDecompressor, ZLibDecompressor ++from .compression_utils import ( ++ DEFAULT_MAX_DECOMPRESS_SIZE, ++ HAS_BROTLI, ++ BrotliDecompressor, ++ ZLibDecompressor, ++) + from .helpers import ( + _EXC_SENTINEL, + DEBUG, +@@ -41,6 +46,7 @@ from .http_exceptions import ( + BadStatusLine, + ContentEncodingError, + ContentLengthError, ++ DecompressSizeError, + InvalidHeader, + InvalidURLError, + LineTooLong, +@@ -959,7 +965,12 @@ class DeflateBuffer: + + decompressor: Any + +- def __init__(self, out: StreamReader, encoding: Optional[str]) -> None: ++ def __init__( ++ self, ++ out: StreamReader, ++ encoding: Optional[str], ++ max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE, ++ ) -> None: + self.out = out + self.size = 0 + self.encoding = encoding +@@ -976,6 +987,8 @@ class DeflateBuffer: + else: + self.decompressor = ZLibDecompressor(encoding=encoding) + ++ self._max_decompress_size = max_decompress_size ++ + def set_exception( + self, + exc: BaseException, +@@ -1004,7 +1017,10 @@ class DeflateBuffer: + ) + + try: +- chunk = self.decompressor.decompress_sync(chunk) ++ # Decompress with limit + 1 so we can detect if output exceeds limit ++ chunk = self.decompressor.decompress_sync( ++ chunk, max_length=self._max_decompress_size + 1 ++ ) + except Exception: + raise ContentEncodingError( + "Can not decode content-encoding: %s" % self.encoding +@@ -1012,6 +1028,13 @@ class DeflateBuffer: + + self._started_decoding = True + ++ # Check if decompression limit was exceeded ++ if len(chunk) > self._max_decompress_size: ++ raise DecompressSizeError( ++ "Decompressed data exceeds the configured limit of %d bytes" ++ % self._max_decompress_size ++ ) ++ + if chunk: + self.out.feed_data(chunk, len(chunk)) + +diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py +index 9e5ff9b41..baf07fd16 100644 +--- a/aiohttp/multipart.py ++++ b/aiohttp/multipart.py +@@ -27,7 +27,12 @@ from urllib.parse import parse_qsl, unquote, urlencode + + from multidict import CIMultiDict, CIMultiDictProxy + +-from .compression_utils import ZLibCompressor, ZLibDecompressor ++from .abc import AbstractStreamWriter ++from .compression_utils import ( ++ DEFAULT_MAX_DECOMPRESS_SIZE, ++ ZLibCompressor, ++ ZLibDecompressor, ++) + from .hdrs import ( + CONTENT_DISPOSITION, + CONTENT_ENCODING, +@@ -263,6 +268,7 @@ class BodyPartReader: + *, + subtype: str = "mixed", + default_charset: Optional[str] = None, ++ max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE, + ) -> None: + self.headers = headers + self._boundary = boundary +@@ -278,6 +284,7 @@ class BodyPartReader: + self._prev_chunk: Optional[bytes] = None + self._content_eof = 0 + self._cache: Dict[str, Any] = {} ++ self._max_decompress_size = max_decompress_size + + def __aiter__(self) -> AsyncIterator["BodyPartReader"]: + return self # type: ignore[return-value] +@@ -471,7 +478,7 @@ class BodyPartReader: + return ZLibDecompressor( + encoding=encoding, + suppress_deflate_header=True, +- ).decompress_sync(data) ++ ).decompress_sync(data, max_length=self._max_decompress_size) + + raise RuntimeError(f"unknown content encoding: {encoding}") + +@@ -528,7 +535,7 @@ class BodyPartReaderPayload(Payload): + if params: + self.set_content_disposition("attachment", True, **params) + +- async def write(self, writer: Any) -> None: ++ async def write(self, writer: AbstractStreamWriter) -> None: + field = self._value + chunk = await field.read_chunk(size=2**16) + while chunk: +@@ -927,7 +934,9 @@ class MultipartWriter(Payload): + total += 2 + len(self._boundary) + 4 # b'--'+self._boundary+b'--\r\n' + return total + +- async def write(self, writer: Any, close_boundary: bool = True) -> None: ++ async def write( ++ self, writer: AbstractStreamWriter, close_boundary: bool = True ++ ) -> None: + """Write body.""" + for part, encoding, te_encoding in self._parts: + if self._is_form_data: +@@ -956,7 +965,7 @@ class MultipartWriter(Payload): + + + class MultipartPayloadWriter: +- def __init__(self, writer: Any) -> None: ++ def __init__(self, writer: AbstractStreamWriter) -> None: + self._writer = writer + self._encoding: Optional[str] = None + self._compress: Optional[ZLibCompressor] = None +diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt +index 514477e8f..34399e6ba 100644 +--- a/docs/spelling_wordlist.txt ++++ b/docs/spelling_wordlist.txt +@@ -182,6 +182,7 @@ lowercased + Mako + manylinux + metadata ++MiB + microservice + middleware + middlewares +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index dbb2dff5a..7d126d185 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -8,9 +8,18 @@ import json + import pathlib + import socket + import ssl ++import zlib + from typing import Any, AsyncIterator + from unittest import mock + ++try: ++ try: ++ import brotlicffi as brotli ++ except ImportError: ++ import brotli ++except ImportError: ++ brotli = None # pragma: no cover ++ + import pytest + from multidict import MultiDict + from yarl import URL +@@ -19,6 +28,8 @@ import aiohttp + from aiohttp import Fingerprint, ServerFingerprintMismatch, hdrs, web + from aiohttp.abc import AbstractResolver + from aiohttp.client_exceptions import TooManyRedirects ++from aiohttp.compression_utils import DEFAULT_MAX_DECOMPRESS_SIZE, HAS_BROTLI ++from aiohttp.http_exceptions import DecompressSizeError + from aiohttp.pytest_plugin import AiohttpClient, TestClient + from aiohttp.test_utils import unused_port + +@@ -1903,8 +1914,73 @@ async def test_bad_payload_compression(aiohttp_client) -> None: + resp.close() + + +-async def test_bad_payload_chunked_encoding(aiohttp_client) -> None: +- async def handler(request): ++async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> None: ++ """Test that decompression size limit triggers DecompressSizeError. ++ ++ When a compressed payload expands beyond the configured limit, ++ we raise DecompressSizeError. ++ """ ++ # Create a highly compressible payload that exceeds the decompression limit. ++ # 64MiB of repeated bytes compresses to ~32KB but expands beyond the ++ # 32MiB per-call limit. ++ original = b"A" * (64 * 2**20) ++ compressed = zlib.compress(original) ++ assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE ++ ++ async def handler(request: web.Request) -> web.Response: ++ # Send compressed data with Content-Encoding header ++ resp = web.Response(body=compressed) ++ resp.headers["Content-Encoding"] = "deflate" ++ return resp ++ ++ app = web.Application() ++ app.router.add_get("/", handler) ++ client = await aiohttp_client(app) ++ ++ async with client.get("/") as resp: ++ assert resp.status == 200 ++ ++ with pytest.raises(aiohttp.ClientPayloadError) as exc_info: ++ await resp.read() ++ ++ assert isinstance(exc_info.value.__cause__, DecompressSizeError) ++ assert "Decompressed data exceeds" in str(exc_info.value.__cause__) ++ ++ ++@pytest.mark.skipif( ++ brotli is None or not HAS_BROTLI, reason="brotli decoding is unavailable" ++) ++async def test_payload_decompress_size_limit_brotli( ++ aiohttp_client: AiohttpClient, ++) -> None: ++ """Test that brotli decompression size limit triggers DecompressSizeError.""" ++ assert brotli is not None ++ # Create a highly compressible payload that exceeds the decompression limit. ++ original = b"A" * (64 * 2**20) ++ compressed = brotli.compress(original) ++ assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE ++ ++ async def handler(request: web.Request) -> web.Response: ++ resp = web.Response(body=compressed) ++ resp.headers["Content-Encoding"] = "br" ++ return resp ++ ++ app = web.Application() ++ app.router.add_get("/", handler) ++ client = await aiohttp_client(app) ++ ++ async with client.get("/") as resp: ++ assert resp.status == 200 ++ ++ with pytest.raises(aiohttp.ClientPayloadError) as exc_info: ++ await resp.read() ++ ++ assert isinstance(exc_info.value.__cause__, DecompressSizeError) ++ assert "Decompressed data exceeds" in str(exc_info.value.__cause__) ++ ++ ++async def test_bad_payload_chunked_encoding(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.StreamResponse: + resp = web.StreamResponse() + resp.force_close() + resp._length_check = False +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 0fcefdefd..9449c4061 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -2,6 +2,7 @@ + + import asyncio + import re ++import zlib + from contextlib import nullcontext + from typing import Any, Dict, List + from unittest import mock +@@ -14,6 +15,7 @@ from yarl import URL + import aiohttp + from aiohttp import http_exceptions, streams + from aiohttp.base_protocol import BaseProtocol ++from aiohttp.compression_utils import HAS_BROTLI + from aiohttp.http_parser import ( + NO_EXTENSIONS, + DeflateBuffer, +@@ -561,7 +563,9 @@ def test_compression_gzip(parser) -> None: + assert msg.compression == "gzip" + + +-@pytest.mark.skipif(brotli is None, reason="brotli is not installed") ++@pytest.mark.skipif( ++ brotli is None or not HAS_BROTLI, reason="brotli decoding is unavailable" ++) + def test_compression_brotli(parser) -> None: + text = b"GET /test HTTP/1.1\r\n" b"content-encoding: br\r\n\r\n" + messages, upgrade, tail = parser.feed_data(text) +@@ -1736,7 +1740,9 @@ class TestParsePayload: + assert p.done + assert out.is_eof() + +- @pytest.mark.skipif(brotli is None, reason="brotli is not installed") ++ @pytest.mark.skipif( ++ brotli is None or not HAS_BROTLI, reason="brotli decoding is unavailable" ++ ) + async def test_http_payload_brotli(self, stream) -> None: + compressed = brotli.compress(b"brotli data") + out = aiohttp.FlowControlDataQueue( +@@ -1816,6 +1822,7 @@ class TestDeflateBuffer: + dbuf.feed_eof() + assert [b"line"] == list(d for d, _ in buf._buffer) + ++ @pytest.mark.skipif(not HAS_BROTLI, reason="brotli decoding is unavailable") + async def test_feed_eof_no_err_brotli(self, stream) -> None: + buf = aiohttp.FlowControlDataQueue( + stream, 2**16, loop=asyncio.get_event_loop() +@@ -1837,3 +1844,36 @@ class TestDeflateBuffer: + dbuf.feed_eof() + + assert buf.at_eof() ++ ++ @pytest.mark.parametrize( ++ "chunk_size", ++ [1024, 2**14, 2**16], # 1KB, 16KB, 64KB ++ ids=["1KB", "16KB", "64KB"], ++ ) ++ async def test_streaming_decompress_large_payload( ++ self, protocol: BaseProtocol, chunk_size: int ++ ) -> None: ++ """Test that large payloads decompress correctly when streamed in chunks. ++ ++ This simulates real HTTP streaming where compressed data arrives in ++ small network chunks. Each chunk's decompressed output should be within ++ the max_decompress_size limit, allowing full recovery of the original data. ++ """ ++ # Create a large payload (3MiB) that compresses well ++ original = b"A" * (3 * 2**20) ++ compressed = zlib.compress(original) ++ ++ buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) ++ dbuf = DeflateBuffer(buf, "deflate") ++ ++ # Feed compressed data in chunks (simulating network streaming) ++ for i in range(0, len(compressed), chunk_size): ++ chunk = compressed[i : i + chunk_size] ++ dbuf.feed_data(chunk, len(chunk)) ++ ++ dbuf.feed_eof() ++ ++ # Read all decompressed data ++ result = b"".join(buf._buffer) ++ assert len(result) == len(original) ++ assert result == original +diff --git a/tests/test_multipart.py b/tests/test_multipart.py +index e4a2be1f3..553085ca5 100644 +--- a/tests/test_multipart.py ++++ b/tests/test_multipart.py +@@ -9,6 +9,7 @@ import pytest + + import aiohttp + from aiohttp import payload ++from aiohttp.abc import AbstractStreamWriter + from aiohttp.hdrs import ( + CONTENT_DISPOSITION, + CONTENT_ENCODING, +@@ -32,14 +33,14 @@ def buf(): + + + @pytest.fixture +-def stream(buf): +- writer = mock.Mock() ++def stream(buf: bytearray) -> AbstractStreamWriter: ++ writer = mock.create_autospec(AbstractStreamWriter, instance=True, spec_set=True) + + async def write(chunk): + buf.extend(chunk) + + writer.write.side_effect = write +- return writer ++ return writer # type: ignore[no-any-return] + + + @pytest.fixture +@@ -336,6 +337,17 @@ class TestPartReader: + result = await obj.read(decode=True) + assert b"Time to Relax!" == result + ++ def test_decode_remains_synchronous(self) -> None: ++ data = b"\x0b\xc9\xccMU(\xc9W\x08J\xcdI\xacP\x04\x00" ++ with Stream(b"") as stream: ++ obj = aiohttp.BodyPartReader( ++ BOUNDARY, ++ {CONTENT_ENCODING: "deflate"}, ++ stream, ++ ) ++ result = obj.decode(data) ++ assert b"Time to Relax!" == result ++ + async def test_read_with_content_encoding_identity(self) -> None: + thing = ( + b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x0b\xc9\xccMU" +@@ -1012,7 +1024,9 @@ async def test_writer(writer) -> None: + assert writer.boundary == ":" + + +-async def test_writer_serialize_io_chunk(buf, stream, writer) -> None: ++async def test_writer_serialize_io_chunk( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + with io.BytesIO(b"foobarbaz") as file_handle: + writer.append(file_handle) + await writer.write(stream) +@@ -1022,7 +1036,9 @@ async def test_writer_serialize_io_chunk(buf, stream, writer) -> None: + ) + + +-async def test_writer_serialize_json(buf, stream, writer) -> None: ++async def test_writer_serialize_json( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + writer.append_json({"привет": "мир"}) + await writer.write(stream) + assert ( +@@ -1031,7 +1047,9 @@ async def test_writer_serialize_json(buf, stream, writer) -> None: + ) + + +-async def test_writer_serialize_form(buf, stream, writer) -> None: ++async def test_writer_serialize_form( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + data = [("foo", "bar"), ("foo", "baz"), ("boo", "zoo")] + writer.append_form(data) + await writer.write(stream) +@@ -1039,7 +1057,9 @@ async def test_writer_serialize_form(buf, stream, writer) -> None: + assert b"foo=bar&foo=baz&boo=zoo" in buf + + +-async def test_writer_serialize_form_dict(buf, stream, writer) -> None: ++async def test_writer_serialize_form_dict( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + data = {"hello": "мир"} + writer.append_form(data) + await writer.write(stream) +@@ -1047,7 +1067,9 @@ async def test_writer_serialize_form_dict(buf, stream, writer) -> None: + assert b"hello=%D0%BC%D0%B8%D1%80" in buf + + +-async def test_writer_write(buf, stream, writer) -> None: ++async def test_writer_write( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + writer.append("foo-bar-baz") + writer.append_json({"test": "passed"}) + writer.append_form({"test": "passed"}) +@@ -1093,7 +1115,9 @@ async def test_writer_write(buf, stream, writer) -> None: + ) == bytes(buf) + + +-async def test_writer_write_no_close_boundary(buf, stream) -> None: ++async def test_writer_write_no_close_boundary( ++ buf: bytearray, stream: AbstractStreamWriter ++) -> None: + writer = aiohttp.MultipartWriter(boundary=":") + writer.append("foo-bar-baz") + writer.append_json({"test": "passed"}) +@@ -1125,12 +1149,18 @@ async def test_writer_write_no_close_boundary(buf, stream) -> None: + ) == bytes(buf) + + +-async def test_writer_write_no_parts(buf, stream, writer) -> None: ++async def test_writer_write_no_parts( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + await writer.write(stream) + assert b"--:--\r\n" == bytes(buf) + + +-async def test_writer_serialize_with_content_encoding_gzip(buf, stream, writer): ++async def test_writer_serialize_with_content_encoding_gzip( ++ buf: bytearray, ++ stream: AbstractStreamWriter, ++ writer: aiohttp.MultipartWriter, ++) -> None: + writer.append("Time to Relax!", {CONTENT_ENCODING: "gzip"}) + await writer.write(stream) + headers, message = bytes(buf).split(b"\r\n\r\n", 1) +@@ -1146,7 +1176,9 @@ async def test_writer_serialize_with_content_encoding_gzip(buf, stream, writer): + assert b"Time to Relax!" == data + + +-async def test_writer_serialize_with_content_encoding_deflate(buf, stream, writer): ++async def test_writer_serialize_with_content_encoding_deflate( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + writer.append("Time to Relax!", {CONTENT_ENCODING: "deflate"}) + await writer.write(stream) + headers, message = bytes(buf).split(b"\r\n\r\n", 1) +@@ -1160,7 +1192,9 @@ async def test_writer_serialize_with_content_encoding_deflate(buf, stream, write + assert thing == message + + +-async def test_writer_serialize_with_content_encoding_identity(buf, stream, writer): ++async def test_writer_serialize_with_content_encoding_identity( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + thing = b"\x0b\xc9\xccMU(\xc9W\x08J\xcdI\xacP\x04\x00" + writer.append(thing, {CONTENT_ENCODING: "identity"}) + await writer.write(stream) +@@ -1175,12 +1209,16 @@ async def test_writer_serialize_with_content_encoding_identity(buf, stream, writ + assert thing == message.split(b"\r\n")[0] + + +-def test_writer_serialize_with_content_encoding_unknown(buf, stream, writer): ++def test_writer_serialize_with_content_encoding_unknown( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + with pytest.raises(RuntimeError): + writer.append("Time to Relax!", {CONTENT_ENCODING: "snappy"}) + + +-async def test_writer_with_content_transfer_encoding_base64(buf, stream, writer): ++async def test_writer_with_content_transfer_encoding_base64( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + writer.append("Time to Relax!", {CONTENT_TRANSFER_ENCODING: "base64"}) + await writer.write(stream) + headers, message = bytes(buf).split(b"\r\n\r\n", 1) +@@ -1193,7 +1231,9 @@ async def test_writer_with_content_transfer_encoding_base64(buf, stream, writer) + assert b"VGltZSB0byBSZWxheCE=" == message.split(b"\r\n")[0] + + +-async def test_writer_content_transfer_encoding_quote_printable(buf, stream, writer): ++async def test_writer_content_transfer_encoding_quote_printable( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + writer.append("Привет, мир!", {CONTENT_TRANSFER_ENCODING: "quoted-printable"}) + await writer.write(stream) + headers, message = bytes(buf).split(b"\r\n\r\n", 1) +@@ -1209,7 +1249,9 @@ async def test_writer_content_transfer_encoding_quote_printable(buf, stream, wri + ) + + +-def test_writer_content_transfer_encoding_unknown(buf, stream, writer) -> None: ++def test_writer_content_transfer_encoding_unknown( ++ buf: bytearray, stream: AbstractStreamWriter, writer: aiohttp.MultipartWriter ++) -> None: + with pytest.raises(RuntimeError): + writer.append("Time to Relax!", {CONTENT_TRANSFER_ENCODING: "unknown"}) + +@@ -1333,7 +1375,9 @@ class TestMultipartWriter: + with aiohttp.MultipartWriter(boundary=":") as writer: + writer.append(None) + +- async def test_write_preserves_content_disposition(self, buf, stream) -> None: ++ async def test_write_preserves_content_disposition( ++ self, buf: bytearray, stream: AbstractStreamWriter ++ ) -> None: + with aiohttp.MultipartWriter(boundary=":") as writer: + part = writer.append(b"foo", headers={CONTENT_TYPE: "test/passed"}) + part.set_content_disposition("form-data", filename="bug") +@@ -1350,7 +1394,9 @@ class TestMultipartWriter: + ) + assert message == b"foo\r\n--:--\r\n" + +- async def test_preserve_content_disposition_header(self, buf, stream): ++ async def test_preserve_content_disposition_header( ++ self, buf: bytearray, stream: AbstractStreamWriter ++ ) -> None: + # https://github.com/aio-libs/aiohttp/pull/3475#issuecomment-451072381 + with pathlib.Path(__file__).open("rb") as fobj: + with aiohttp.MultipartWriter("form-data", boundary=":") as writer: +@@ -1374,7 +1420,9 @@ class TestMultipartWriter: + b'Content-Disposition: attachments; filename="bug.py"' + ) + +- async def test_set_content_disposition_override(self, buf, stream): ++ async def test_set_content_disposition_override( ++ self, buf: bytearray, stream: AbstractStreamWriter ++ ) -> None: + # https://github.com/aio-libs/aiohttp/pull/3475#issuecomment-451072381 + with pathlib.Path(__file__).open("rb") as fobj: + with aiohttp.MultipartWriter("form-data", boundary=":") as writer: +@@ -1398,7 +1446,9 @@ class TestMultipartWriter: + b'Content-Disposition: attachments; filename="bug.py"' + ) + +- async def test_reset_content_disposition_header(self, buf, stream): ++ async def test_reset_content_disposition_header( ++ self, buf: bytearray, stream: AbstractStreamWriter ++ ) -> None: + # https://github.com/aio-libs/aiohttp/pull/3475#issuecomment-451072381 + with pathlib.Path(__file__).open("rb") as fobj: + with aiohttp.MultipartWriter("form-data", boundary=":") as writer: +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index 69cad8cb44..feb9039ae1 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -15,6 +15,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2025-69229_p1.patch \ file://CVE-2025-69229_p2.patch \ file://CVE-2025-69227.patch \ + file://CVE-2025-69223.patch \ " PYPI_PACKAGE = "aiohttp" From patchwork Mon Aug 31 04:57:40 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96875 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 94C1FC61DD3 for ; Mon, 31 Aug 2026 04:57:50 +0000 (UTC) Received: from alln-iport-5.cisco.com (alln-iport-5.cisco.com [173.37.142.92]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.22959.1788152269771553020 for ; Sun, 30 Aug 2026 21:57:50 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=Vbsl8INJ; spf=pass (domain: cisco.com, ip: 173.37.142.92, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=39712; q=dns/txt; s=iport01; t=1788152270; x=1789361870; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=tcMmzsj1Aw+v9AE46QNdiIKKYUB3ak72vdPUb+R0e4Y=; b=Vbsl8INJbajfUi4vKdrBfsin6XO9M1T2aQJqsNCrGm9B3If1342oI688 dmqdZ0l8XVb2antRjxBu1yVNqQPKuOx9Kl6EKMnoZpGS2rzw78mlLvVo3 8yElb+9/L2ABklAwKmGgI5fc8pTKGES4pNZxMMg9lEYqALwYwdhgdq1IB dgxrHLRGGAouCRY7FG3GHwjobCc1iWQG72OhE9KTkKKyJWDUMs5lx72nV oD7xVcEK0DVuV3jStXBtgq9/Q1r/7+3nY5UsWRHljZEB80Mr58zC7DVh9 P3nV56/YfUZxkpKF0u5qiNO7tVzvB3+uX+1IpdCoy1/zwKZw2Np1+sqpN w==; X-CSE-ConnectionGUID: bLHrwnYaSm2RiAWhUbwuag== X-CSE-MsgGUID: 9B2h1k+VRJeZIhcfIXG/hg== X-IPAS-Result: A0BKAgAsCZVq/44QJK1aHgEBCxIMggULghg/dF5DSZZKA4ETnQgUgWoPAQEBD0QNBAEBhD9GAo1zAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQsBAQUBAQECAQcFgQ4Thk8NhloBAgEDGgEMCwEYAS0QHAMBAi8rIwgQCYMCAYJ0AxHCAYF5M4EBgygBPwJDUNswAQsUAYE4hT+IIl0YAYNrgREnGxuBcoEVgTuBOHaBBYFcAQECgTYPAQmGVASCIoEMgVqBF5EISIEeA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+F4ENGwYFgR2BKIQPIxk2eoEJXoErKWABEheBCYIIAoJaggUCAUlDDgdHUwkECxgNSBEsNxUZBD5uB456H4FYcwFYCC0BGBEBAYEFgQ8mCAaTGwFCj1uCIaEPCiiDdowilToaM4QEgVeSQJJRC5h9jgqWAFCEaYFoPIFHCwdwFTuCZwlKGQ+OKg4LgRQBAoJJgTRLgxRRxjQhJzICCTIBAQcCBw4DC4FohGECix4BJoFWAQE IronPort-Data: A9a23:uYAEq6uT4r3/xAGKr1FJCvFseefnVAdfMUV32f8akzHdYApBsoF/q tZmKWqAOfmPYGOjKt91OY22900DusSEmoQ3HQFtqiowHy4TgMeUXt7xwmUckM+xwmwvaGo9s q3yv/GZdJhcokf0/0nrav666yEgiclkf5KkYMbcICd9WAR4fykojBNnioYRj5Vh6TSDK1vlV eja/YuFZTdJ5xYuajhKs/LY8Es21BjPkGpwUmIWNKgjUGD2zxH5PLpHTYmtIn3xRJVjH+LSb 47r0LGj82rFyAwmA9Wjn6yTWhVirmn6ZFXmZtJ+AsBOszAazsAA+v9T2Mk0NS+7vw60c+VZk 72hg3AfpTABZcUgkMxFO/VR/roX0aduoNcrKlDn2SCfItGvn3bEm51T4E8K0YIw8MJXG35Ix PUjLWo3bzm8h9+836+yRbw57igjBJGD0II3s3Vky3TdSP0hW52GG/iM7t5D1zB2jcdLdRrcT 5NGMnw0MlKZPVsWZg1/5JEWxI9EglHzcDBcoVOErII84nPYy0p6172F3N/9KobXGp4PwBzCz o7A1zXTWBFDN5+h8xiIwFOsjeHurQy8WI1HQdVU8dYv2jV/3Fc7DwUbU1a+q/S1hkOyHt5SN UEQ0i4vtrQpskuzQ9/wWhe1rHKJslgbQdU4LgEhwAiJzqyR50OSAXIJC2YcLtcnr8QxAzct0 zdlgu/UONCmi5XNIVr1y1tehWja1fQ9RYPaWRI5cA== IronPort-HdrOrdr: A9a23:F4VbLa78StJ9mfS36wPXwOrXdLJyesId70hD6qm+c3Nom6uj5q eTdZsgtCMc5Ax9ZJhko6HjBEDiewK5yXcK2+ks1N6ZNWGM0ldAbrsSiLcKqAePJ8SRzIJgPN 9bAstDIey1K0RmhsDn5wT9OdMhzN6btJ2Mv47lvhFQpcUAUdAZ0++/YTzra3FLeA== X-Talos-CUID: 9a23:88WdRWBYzqDqVOf6EyND1FQLHPEsSXiH9nb5HE+FA3l0QbLAHA== X-Talos-MUID: 9a23:TxH6mgh91p4SCV17N/4YrsMpZe0y4/2tS343s5g0msmPOw1AFHCdg2Hi X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="825732622" Received: from alln-l-core-05.cisco.com ([173.36.16.142]) by alln-iport-5.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-05.cisco.com (Postfix) with ESMTPS id 9B0A618000481; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id D91F8CD02BC; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 5/9] python3-aiohttp: fix CVE-2026-22815 Date: Sun, 30 Aug 2026 21:57:40 -0700 Message-Id: <20260831045744.3321483-6-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-05.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:50 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129609 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1], [2], and [3]. The advisory identifying the fix is referenced in [4]. The generated aiohttp/_http_parser.c changes are omitted. The recipe-time Cython regeneration introduced with CVE-2025-69224 regenerates that file from the patched _http_parser.pyx before the accelerated parser is compiled. [1] https://github.com/aio-libs/aiohttp/commit/0c2e9da51126238a421568eb7c5b53e5b5d17b36 [2] https://github.com/aio-libs/aiohttp/commit/ed6440ca49ef4907ab9d99ba7e329aab702b7173 [3] https://github.com/aio-libs/aiohttp/commit/30ec25f8a58c5dc3f8fdb3eec31f555eeaabd30a [4] https://nvd.nist.gov/vuln/detail/CVE-2026-22815 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-22815.patch | 1020 +++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 1021 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-22815.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-22815.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-22815.patch new file mode 100644 index 0000000000..16c2bf9c76 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-22815.patch @@ -0,0 +1,1020 @@ +From 397155d08683c97a24d7dfc245d45d13941aca1e Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Mon, 12 Jan 2026 18:49:19 +0000 +Subject: [PATCH] Add max_headers parameter (#11955) (#11959) (#11960) + +CVE: CVE-2026-22815 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/0c2e9da51126238a421568eb7c5b53e5b5d17b36] + +Backport Changes: +- Retained the Scarthgap bytearray and CIMultiDict parser model. +- Added header-count enforcement to the existing header processor. +- Reset header-name size while retaining Scarthgap buffer handling. +- Added max_headers to the legacy client request path. +- Preserved Scarthgap connection and exception-handling logic. +- Omitted newer middleware, TypedDict, retry, and SSL API context. +- Adapted documentation and tests for the Scarthgap API. +- Omitted the generated aiohttp/_http_parser.c changes. The Scarthgap + recipe regenerates this file from the patched _http_parser.pyx using + python3-cython-native before compilation. + +(cherry picked from commit ed6440ca49ef4907ab9d99ba7e329aab702b7173) +(cherry picked from commit 30ec25f8a58c5dc3f8fdb3eec31f555eeaabd30a) +(cherry picked from commit 0c2e9da51126238a421568eb7c5b53e5b5d17b36) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/11955.feature.rst | 1 + + aiohttp/_http_parser.pyx | 31 ++++---- + aiohttp/client.py | 9 ++- + aiohttp/client_proto.py | 2 + + aiohttp/http_exceptions.py | 9 +-- + aiohttp/http_parser.py | 83 +++++++++++++--------- + aiohttp/web_protocol.py | 2 +- + docs/client_reference.rst | 26 ++++++- + docs/web_reference.rst | 5 +- + tests/test_client_functional.py | 117 +++++++++++++++++++++--------- + tests/test_http_exceptions.py | 18 ++--- + tests/test_http_parser.py | 121 +++++++++++++++++++++++++------- + 12 files changed, 302 insertions(+), 122 deletions(-) + create mode 100644 CHANGES/11955.feature.rst + +diff --git a/CHANGES/11955.feature.rst b/CHANGES/11955.feature.rst +new file mode 100644 +index 000000000..eaea1016e +--- /dev/null ++++ b/CHANGES/11955.feature.rst +@@ -0,0 +1 @@ ++Added ``max_headers`` parameter to limit the number of headers that should be read from a response -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index 4dca08f35..72454dfd3 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -278,6 +278,7 @@ cdef class HttpParser: + bytearray _raw_name + bytearray _raw_value + bint _has_value ++ int _header_name_size + + object _protocol + object _loop +@@ -328,7 +329,7 @@ cdef class HttpParser: + self, cparser.llhttp_type mode, + object protocol, object loop, int limit, + object timer=None, +- size_t max_line_size=8190, size_t max_headers=32768, ++ size_t max_line_size=8190, size_t max_headers=128, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, +@@ -351,6 +352,7 @@ cdef class HttpParser: + self._raw_name = bytearray() + self._raw_value = bytearray() + self._has_value = False ++ self._header_name_size = 0 + + self._max_line_size = max_line_size + self._max_headers = max_headers +@@ -384,6 +386,8 @@ cdef class HttpParser: + value = raw_value.decode('utf-8', 'surrogateescape') + + self._headers.add(name, value) ++ if len(self._headers) > self._max_headers: ++ raise BadHttpMessage("Too many headers received") + + if name is CONTENT_ENCODING: + self._content_encoding = value +@@ -391,6 +395,7 @@ cdef class HttpParser: + PyByteArray_Resize(self._raw_name, 0) + PyByteArray_Resize(self._raw_value, 0) + self._has_value = False ++ self._header_name_size = 0 + self._raw_headers.append((raw_name, raw_value)) + + cdef _on_header_field(self, char* at, size_t length): +@@ -582,7 +587,7 @@ cdef class HttpRequestParser(HttpParser): + + def __init__( + self, protocol, loop, int limit, timer=None, +- size_t max_line_size=8190, size_t max_headers=32768, ++ size_t max_line_size=8190, size_t max_headers=128, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, +@@ -646,7 +651,7 @@ cdef class HttpResponseParser(HttpParser): + + def __init__( + self, protocol, loop, int limit, timer=None, +- size_t max_line_size=8190, size_t max_headers=32768, ++ size_t max_line_size=8190, size_t max_headers=128, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True +@@ -685,8 +690,8 @@ cdef int cb_on_url(cparser.llhttp_t* parser, + cdef HttpParser pyparser = parser.data + try: + if length > pyparser._max_line_size: +- raise LineTooLong( +- 'Status line is too long', pyparser._max_line_size, length) ++ status = pyparser._buf + at[:length] ++ raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) + extend(pyparser._buf, at, length) + except BaseException as ex: + pyparser._last_error = ex +@@ -698,11 +703,10 @@ cdef int cb_on_url(cparser.llhttp_t* parser, + cdef int cb_on_status(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data +- cdef str reason + try: + if length > pyparser._max_line_size: +- raise LineTooLong( +- 'Status line is too long', pyparser._max_line_size, length) ++ reason = pyparser._buf + at[:length] ++ raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) + extend(pyparser._buf, at, length) + except BaseException as ex: + pyparser._last_error = ex +@@ -719,8 +723,9 @@ cdef int cb_on_header_field(cparser.llhttp_t* parser, + pyparser._on_status_complete() + size = len(pyparser._raw_name) + length + if size > pyparser._max_field_size: +- raise LineTooLong( +- 'Header name is too long', pyparser._max_field_size, size) ++ name = pyparser._raw_name + at[:length] ++ raise LineTooLong(name[:100] + b"...", pyparser._max_field_size) ++ pyparser._header_name_size = size + pyparser._on_header_field(at, length) + except BaseException as ex: + pyparser._last_error = ex +@@ -735,9 +740,9 @@ cdef int cb_on_header_value(cparser.llhttp_t* parser, + cdef Py_ssize_t size + try: + size = len(pyparser._raw_value) + length +- if size > pyparser._max_field_size: +- raise LineTooLong( +- 'Header value is too long', pyparser._max_field_size, size) ++ if pyparser._header_name_size + size > pyparser._max_field_size: ++ value = pyparser._raw_value + at[:length] ++ raise LineTooLong(value[:100] + b"...", pyparser._max_field_size) + pyparser._on_header_value(at, length) + except BaseException as ex: + pyparser._last_error = ex +diff --git a/aiohttp/client.py b/aiohttp/client.py +index 32d2c3b71..1d10fc84c 100644 +--- a/aiohttp/client.py ++++ b/aiohttp/client.py +@@ -136,7 +136,6 @@ if TYPE_CHECKING: + else: + SSLContext = None + +- + @attr.s(auto_attribs=True, frozen=True, slots=True) + class ClientTimeout: + total: Optional[float] = None +@@ -195,6 +194,7 @@ class ClientSession: + "_read_bufsize", + "_max_line_size", + "_max_field_size", ++ "_max_headers", + "_resolve_charset", + ] + ) +@@ -232,6 +232,7 @@ class ClientSession: + read_bufsize: int = 2**16, + max_line_size: int = 8190, + max_field_size: int = 8190, ++ max_headers: int = 128, + fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8", + ) -> None: + # We initialise _connector to None immediately, as it's referenced in __del__() +@@ -316,6 +317,7 @@ class ClientSession: + self._read_bufsize = read_bufsize + self._max_line_size = max_line_size + self._max_field_size = max_field_size ++ self._max_headers = max_headers + + # Convert to list of tuples + if headers: +@@ -418,6 +420,7 @@ class ClientSession: + auto_decompress: Optional[bool] = None, + max_line_size: Optional[int] = None, + max_field_size: Optional[int] = None, ++ max_headers: Optional[int] = None, + ) -> ClientResponse: + + # NOTE: timeout clamps existing connect and read timeouts. We cannot +@@ -490,6 +493,9 @@ class ClientSession: + if max_field_size is None: + max_field_size = self._max_field_size + ++ if max_headers is None: ++ max_headers = self._max_headers ++ + traces = [ + Trace( + self, +@@ -599,6 +605,7 @@ class ClientSession: + timeout_ceil_threshold=self._connector._timeout_ceil_threshold, + max_line_size=max_line_size, + max_field_size=max_field_size, ++ max_headers=max_headers, + ) + + try: +diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py +index 723f5aae5..10852efc0 100644 +--- a/aiohttp/client_proto.py ++++ b/aiohttp/client_proto.py +@@ -180,6 +180,7 @@ class ResponseHandler(BaseProtocol, DataQueue[Tuple[RawResponseMessage, StreamRe + timeout_ceil_threshold: float = 5, + max_line_size: int = 8190, + max_field_size: int = 8190, ++ max_headers: int = 128, + ) -> None: + self._skip_payload = skip_payload + +@@ -198,6 +199,7 @@ class ResponseHandler(BaseProtocol, DataQueue[Tuple[RawResponseMessage, StreamRe + auto_decompress=auto_decompress, + max_line_size=max_line_size, + max_field_size=max_field_size, ++ max_headers=max_headers, + ) + + if self._tail: +diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py +index 877b07d4c..201fe0bcc 100644 +--- a/aiohttp/http_exceptions.py ++++ b/aiohttp/http_exceptions.py +@@ -81,11 +81,12 @@ class DecompressSizeError(PayloadEncodingError): + + class LineTooLong(BadHttpMessage): + def __init__( +- self, line: str, limit: str = "Unknown", actual_size: str = "Unknown" ++ self, ++ line: Union[str, bytes], ++ limit: Union[str, int] = "Unknown", ++ actual_size: str = "Unknown", + ) -> None: +- super().__init__( +- f"Got more than {limit} bytes ({actual_size}) when reading {line}." +- ) ++ super().__init__(f"Got more than {limit} bytes when reading: {line!r}.") + self.args = (line, limit, actual_size) + + +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index cdf3fc89a..2c1e4b17a 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -167,20 +167,10 @@ class HeadersParser: + raise InvalidHeader(line) + + bvalue = bvalue.lstrip(b" \t") +- if len(bname) > self.max_field_size: +- raise LineTooLong( +- "request header name {}".format( +- bname.decode("utf8", "backslashreplace") +- ), +- str(self.max_field_size), +- str(len(bname)), +- ) + name = bname.decode("utf-8", "surrogateescape") + if not TOKENRE.fullmatch(name): + raise InvalidHeader(bname) + +- header_length = len(bvalue) +- + # next line + lines_idx += 1 + line = lines[lines_idx] +@@ -190,16 +180,14 @@ class HeadersParser: + + # Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding + if continuation: ++ header_length = len(bvalue) + bvalue_lst = [bvalue] + while continuation: + header_length += len(line) + if header_length > self.max_field_size: ++ header_line = bname + b": " + b"".join(bvalue_lst) + raise LineTooLong( +- "request header field {}".format( +- bname.decode("utf8", "backslashreplace") +- ), +- str(self.max_field_size), +- str(header_length), ++ header_line[:100] + b"...", self.max_field_size + ) + bvalue_lst.append(line) + +@@ -213,15 +201,6 @@ class HeadersParser: + line = b"" + break + bvalue = b"".join(bvalue_lst) +- else: +- if header_length > self.max_field_size: +- raise LineTooLong( +- "request header field {}".format( +- bname.decode("utf8", "backslashreplace") +- ), +- str(self.max_field_size), +- str(header_length), +- ) + + bvalue = bvalue.strip(b" \t") + value = bvalue.decode("utf-8", "surrogateescape") +@@ -252,7 +231,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + loop: Optional[asyncio.AbstractEventLoop] = None, + limit: int = 2**16, + max_line_size: int = 8190, +- max_headers: int = 32768, ++ max_headers: int = 128, + max_field_size: int = 8190, + timer: Optional[BaseTimerContext] = None, + code: Optional[int] = None, +@@ -325,6 +304,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + data_len = len(data) + start_pos = 0 + loop = self.loop ++ max_line_length = self.max_line_size + + while start_pos < data_len: + +@@ -342,11 +322,21 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + line = data[start_pos:pos] + if SEP == b"\n": # For lax response parsing + line = line.rstrip(b"\r") ++ if len(line) > max_line_length: ++ raise LineTooLong(line[:100] + b"...", max_line_length) ++ + self._lines.append(line) ++ # After processing the status/request line, everything is a header. ++ max_line_length = self.max_field_size ++ ++ if len(self._lines) > self.max_headers: ++ raise BadHttpMessage("Too many headers received") ++ + start_pos = pos + len(SEP) + + # \r\n\r\n found + if self._lines[-1] == EMPTY: ++ max_trailers = self.max_headers - len(self._lines) + try: + msg: _MsgT = self.parse_message(self._lines) + finally: +@@ -406,6 +396,9 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + auto_decompress=self._auto_decompress, + lax=self.lax, + headers_parser=self._headers_parser, ++ max_line_size=self.max_line_size, ++ max_field_size=self.max_field_size, ++ max_trailers=max_trailers, + ) + if not payload_parser.done: + self._payload_parser = payload_parser +@@ -426,6 +419,9 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + auto_decompress=self._auto_decompress, + lax=self.lax, + headers_parser=self._headers_parser, ++ max_line_size=self.max_line_size, ++ max_field_size=self.max_field_size, ++ max_trailers=max_trailers, + ) + elif not empty_body and length is None and self.read_until_eof: + payload = StreamReader( +@@ -446,6 +442,9 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + auto_decompress=self._auto_decompress, + lax=self.lax, + headers_parser=self._headers_parser, ++ max_line_size=self.max_line_size, ++ max_field_size=self.max_field_size, ++ max_trailers=max_trailers, + ) + if not payload_parser.done: + self._payload_parser = payload_parser +@@ -455,6 +454,8 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + messages.append((msg, payload)) + else: + self._tail = data[start_pos:] ++ if len(self._tail) > self.max_line_size: ++ raise LineTooLong(self._tail[:100] + b"...", self.max_line_size) + data = EMPTY + break + +@@ -594,11 +595,6 @@ class HttpRequestParser(HttpParser[RawRequestMessage]): + except ValueError: + raise BadStatusLine(line) from None + +- if len(path) > self.max_line_size: +- raise LineTooLong( +- "Status line is too long", str(self.max_line_size), str(len(path)) +- ) +- + # method + if not TOKENRE.fullmatch(method): + raise BadStatusLine(method) +@@ -706,11 +702,6 @@ class HttpResponseParser(HttpParser[RawResponseMessage]): + status = status.strip() + reason = "" + +- if len(reason) > self.max_line_size: +- raise LineTooLong( +- "Status line is too long", str(self.max_line_size), str(len(reason)) +- ) +- + # version + match = VERSRE.fullmatch(version) + if match is None: +@@ -772,6 +763,9 @@ class HttpPayloadParser: + lax: bool = False, + *, + headers_parser: HeadersParser, ++ max_line_size: int = 8190, ++ max_field_size: int = 8190, ++ max_trailers: int = 128, + ) -> None: + self._length = 0 + self._type = ParseState.PARSE_NONE +@@ -781,6 +775,9 @@ class HttpPayloadParser: + self._auto_decompress = auto_decompress + self._lax = lax + self._headers_parser = headers_parser ++ self._max_line_size = max_line_size ++ self._max_field_size = max_field_size ++ self._max_trailers = max_trailers + self._trailer_lines: list[bytes] = [] + self.done = False + +@@ -855,6 +852,15 @@ class HttpPayloadParser: + # Chunked transfer encoding parser + elif self._type == ParseState.PARSE_CHUNKED: + if self._chunk_tail: ++ # We should never have a tail if we're inside the payload body. ++ assert self._chunk != ChunkState.PARSE_CHUNKED_CHUNK ++ # We should check the length is sane. ++ max_line_length = self._max_line_size ++ if self._chunk == ChunkState.PARSE_TRAILERS: ++ max_line_length = self._max_field_size ++ if len(self._chunk_tail) > max_line_length: ++ raise LineTooLong(self._chunk_tail[:100] + b"...", max_line_length) ++ + chunk = self._chunk_tail + chunk + self._chunk_tail = b"" + +@@ -938,8 +944,15 @@ class HttpPayloadParser: + chunk = chunk[pos + len(SEP) :] + if SEP == b"\n": # For lax response parsing + line = line.rstrip(b"\r") ++ ++ if len(line) > self._max_field_size: ++ raise LineTooLong(line[:100] + b"...", self._max_field_size) ++ + self._trailer_lines.append(line) + ++ if len(self._trailer_lines) > self._max_trailers: ++ raise BadHttpMessage("Too many trailers received") ++ + # \r\n\r\n found, end of stream + if self._trailer_lines[-1] == b"": + # Headers and trailers are defined the same way, +diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py +index f083b13eb..a06503e0c 100644 +--- a/aiohttp/web_protocol.py ++++ b/aiohttp/web_protocol.py +@@ -177,7 +177,7 @@ class RequestHandler(BaseProtocol): + access_log_format: str = AccessLogger.LOG_FORMAT, + debug: bool = False, + max_line_size: int = 8190, +- max_headers: int = 32768, ++ max_headers: int = 128, + max_field_size: int = 8190, + lingering_time: float = 10.0, + read_bufsize: int = 2**16, +diff --git a/docs/client_reference.rst b/docs/client_reference.rst +index fdf66e1be..49a2dfe58 100644 +--- a/docs/client_reference.rst ++++ b/docs/client_reference.rst +@@ -52,6 +52,9 @@ The client session supports the context manager protocol for self closing. + requote_redirect_url=True, \ + trust_env=False, \ + trace_configs=None, \ ++ max_line_size=8190, \ ++ max_field_size=8190, \ ++ max_headers=128, \ + fallback_charset_resolver=lambda r, b: "utf-8") + + The class for creating client sessions and making requests. +@@ -227,6 +230,17 @@ The client session supports the context manager protocol for self closing. + disabling. See :ref:`aiohttp-client-tracing-reference` for + more information. + ++ :param int read_bufsize: Size of the read buffer (:attr:`ClientResponse.content`). ++ 64 KiB by default. ++ ++ .. versionadded:: 3.7 ++ ++ :param int max_line_size: Maximum allowed size of lines in responses. ++ ++ :param int max_field_size: Maximum allowed size of header name and value combined in responses. ++ ++ :param int max_headers: Maximum number of headers and trailers combined in responses. ++ + :param Callable[[ClientResponse,bytes],str] fallback_charset_resolver: + A :term:`callable` that accepts a :class:`ClientResponse` and the + :class:`bytes` contents, and returns a :class:`str` which will be used as +@@ -376,7 +390,11 @@ The client session supports the context manager protocol for self closing. + timeout=sentinel, ssl=None, \ + verify_ssl=None, fingerprint=None, \ + ssl_context=None, proxy_headers=None, \ +- server_hostname=None, auto_decompress=None) ++ server_hostname=None, \ ++ auto_decompress=None, \ ++ max_line_size=None, \ ++ max_field_size=None, \ ++ max_headers=None) + :async: + :noindexentry: + +@@ -561,6 +579,12 @@ The client session supports the context manager protocol for self closing. + Overrides :attr:`ClientSession.auto_decompress`. + May be used to enable/disable auto decompression on a per-request basis. + ++ :param int max_line_size: Maximum allowed size of lines in responses. ++ ++ :param int max_field_size: Maximum allowed size of header name and value combined in responses. ++ ++ :param int max_headers: Maximum number of headers and trailers combined in responses. ++ + :return ClientResponse: a :class:`client response ` + object. + +diff --git a/docs/web_reference.rst b/docs/web_reference.rst +index aedac0e54..cc6201b42 100644 +--- a/docs/web_reference.rst ++++ b/docs/web_reference.rst +@@ -2709,9 +2709,10 @@ application on specific TCP or Unix socket, e.g.:: + :attr:`helpers.AccessLogger.LOG_FORMAT`. + :param int max_line_size: Optional maximum header line size. Default: + ``8190``. +- :param int max_headers: Optional maximum header size. Default: ``32768``. +- :param int max_field_size: Optional maximum header field size. Default: ++ :param int max_field_size: Optional maximum header combined name and value size. Default: + ``8190``. ++ :param int max_headers: Optional maximum number of headers and trailers combined. Default: ++ ``128``. + + :param float lingering_time: Maximum time during which the server + reads and ignores additional data coming from the client when +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index 7d126d185..55115fd6f 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -3361,17 +3361,17 @@ async def test_http_empty_data_text(aiohttp_client) -> None: + assert resp.headers["Content-Type"] == "text/plain; charset=utf-8" + + +-async def test_max_field_size_session_default(aiohttp_client) -> None: +- async def handler(request): +- return web.Response(headers={"Custom": "x" * 8190}) ++async def test_max_field_size_session_default(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(headers={"Custom": "x" * 8182}) + + app = web.Application() + app.add_routes([web.get("/", handler)]) + + client = await aiohttp_client(app) + +- async with await client.get("/") as resp: +- assert resp.headers["Custom"] == "x" * 8190 ++ async with client.get("/") as resp: ++ assert resp.headers["Custom"] == "x" * 8182 + + + async def test_max_field_size_session_default_fail(aiohttp_client) -> None: +@@ -3386,43 +3386,96 @@ async def test_max_field_size_session_default_fail(aiohttp_client) -> None: + await client.get("/") + + +-async def test_max_field_size_session_explicit(aiohttp_client) -> None: +- async def handler(request): +- return web.Response(headers={"Custom": "x" * 8191}) ++async def test_max_field_size_session_explicit(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(headers={"Custom": "x" * 8192}) + + app = web.Application() + app.add_routes([web.get("/", handler)]) + +- client = await aiohttp_client(app, max_field_size=8191) ++ client = await aiohttp_client(app, max_field_size=8200) + +- async with await client.get("/") as resp: +- assert resp.headers["Custom"] == "x" * 8191 ++ async with client.get("/") as resp: ++ assert resp.headers["Custom"] == "x" * 8192 + + +-async def test_max_field_size_request_explicit(aiohttp_client) -> None: +- async def handler(request): +- return web.Response(headers={"Custom": "x" * 8191}) ++async def test_max_headers_session_default(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(headers={f"Custom-{i}": "x" for i in range(120)}) + + app = web.Application() + app.add_routes([web.get("/", handler)]) + + client = await aiohttp_client(app) + +- async with await client.get("/", max_field_size=8191) as resp: +- assert resp.headers["Custom"] == "x" * 8191 ++ async with client.get("/") as resp: ++ assert resp.headers["Custom-119"] == "x" + + +-async def test_max_line_size_session_default(aiohttp_client) -> None: +- async def handler(request): +- return web.Response(status=200, reason="x" * 8190) ++async def test_max_headers_session_default_fail( ++ aiohttp_client: AiohttpClient, ++) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(headers={f"Custom-{i}": "x" for i in range(129)}) + + app = web.Application() + app.add_routes([web.get("/", handler)]) + + client = await aiohttp_client(app) ++ with pytest.raises(aiohttp.ClientResponseError): ++ await client.get("/") + +- async with await client.get("/") as resp: +- assert resp.reason == "x" * 8190 ++ ++async def test_max_headers_session_explicit(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(headers={f"Custom-{i}": "x" for i in range(130)}) ++ ++ app = web.Application() ++ app.add_routes([web.get("/", handler)]) ++ ++ client = await aiohttp_client(app, max_headers=140) ++ ++ async with client.get("/") as resp: ++ assert resp.headers["Custom-129"] == "x" ++ ++ ++async def test_max_headers_request_explicit(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(headers={f"Custom-{i}": "x" for i in range(130)}) ++ ++ app = web.Application() ++ app.add_routes([web.get("/", handler)]) ++ ++ client = await aiohttp_client(app) ++ ++ async with client.get("/", max_headers=140) as resp: ++ assert resp.headers["Custom-129"] == "x" ++ ++ ++async def test_max_field_size_request_explicit(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(headers={"Custom": "x" * 8192}) ++ ++ app = web.Application() ++ app.add_routes([web.get("/", handler)]) ++ ++ client = await aiohttp_client(app) ++ ++ async with client.get("/", max_field_size=8200) as resp: ++ assert resp.headers["Custom"] == "x" * 8192 ++ ++ ++async def test_max_line_size_session_default(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(status=200, reason="x" * 8177) ++ ++ app = web.Application() ++ app.add_routes([web.get("/", handler)]) ++ ++ client = await aiohttp_client(app) ++ ++ async with client.get("/") as resp: ++ assert resp.reason == "x" * 8177 + + + async def test_max_line_size_session_default_fail(aiohttp_client) -> None: +@@ -3437,30 +3490,30 @@ async def test_max_line_size_session_default_fail(aiohttp_client) -> None: + await client.get("/") + + +-async def test_max_line_size_session_explicit(aiohttp_client) -> None: +- async def handler(request): +- return web.Response(status=200, reason="x" * 8191) ++async def test_max_line_size_session_explicit(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(status=200, reason="x" * 8197) + + app = web.Application() + app.add_routes([web.get("/", handler)]) + +- client = await aiohttp_client(app, max_line_size=8191) ++ client = await aiohttp_client(app, max_line_size=8210) + +- async with await client.get("/") as resp: +- assert resp.reason == "x" * 8191 ++ async with client.get("/") as resp: ++ assert resp.reason == "x" * 8197 + + +-async def test_max_line_size_request_explicit(aiohttp_client) -> None: +- async def handler(request): +- return web.Response(status=200, reason="x" * 8191) ++async def test_max_line_size_request_explicit(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(status=200, reason="x" * 8197) + + app = web.Application() + app.add_routes([web.get("/", handler)]) + + client = await aiohttp_client(app) + +- async with await client.get("/", max_line_size=8191) as resp: +- assert resp.reason == "x" * 8191 ++ async with client.get("/", max_line_size=8210) as resp: ++ assert resp.reason == "x" * 8197 + + + @pytest.mark.xfail(raises=asyncio.TimeoutError, reason="#7599") +diff --git a/tests/test_http_exceptions.py b/tests/test_http_exceptions.py +index 24944d9fc..6186a71c6 100644 +--- a/tests/test_http_exceptions.py ++++ b/tests/test_http_exceptions.py +@@ -69,32 +69,32 @@ class TestBadHttpMessage: + + class TestLineTooLong: + def test_ctor(self) -> None: +- err = http_exceptions.LineTooLong("spam", "10", "12") ++ err = http_exceptions.LineTooLong(b"spam", 10) + assert err.code == 400 +- assert err.message == "Got more than 10 bytes (12) when reading spam." ++ assert err.message == "Got more than 10 bytes when reading: b'spam'." + assert err.headers is None + + def test_pickle(self) -> None: +- err = http_exceptions.LineTooLong(line="spam", limit="10", actual_size="12") ++ err = http_exceptions.LineTooLong(line=b"spam", limit=10, actual_size="12") + err.foo = "bar" + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + pickled = pickle.dumps(err, proto) + err2 = pickle.loads(pickled) + assert err2.code == 400 +- assert err2.message == ("Got more than 10 bytes (12) " "when reading spam.") ++ assert err2.message == ("Got more than 10 bytes when reading: b'spam'.") + assert err2.headers is None + assert err2.foo == "bar" + + def test_str(self) -> None: +- err = http_exceptions.LineTooLong(line="spam", limit="10", actual_size="12") +- expected = "400, message:\n Got more than 10 bytes (12) when reading spam." ++ err = http_exceptions.LineTooLong(line=b"spam", limit=10) ++ expected = "400, message:\n Got more than 10 bytes when reading: b'spam'." + assert str(err) == expected + + def test_repr(self) -> None: +- err = http_exceptions.LineTooLong(line="spam", limit="10", actual_size="12") ++ err = http_exceptions.LineTooLong(line=b"spam", limit=10) + assert repr(err) == ( +- "" ++ '" + ) + + +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 9449c4061..ea8c338e0 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -20,7 +20,9 @@ from aiohttp.http_parser import ( + NO_EXTENSIONS, + DeflateBuffer, + HttpPayloadParser, ++ HttpRequestParser, + HttpRequestParserPy, ++ HttpResponseParser, + HttpResponseParserPy, + HttpVersion, + ) +@@ -66,7 +68,7 @@ def parser(loop: Any, protocol: Any, request: Any): + loop, + 2**16, + max_line_size=8190, +- max_headers=32768, ++ max_headers=128, + max_field_size=8190, + ) + +@@ -85,7 +87,7 @@ def response(loop: Any, protocol: Any, request: Any): + loop, + 2**16, + max_line_size=8190, +- max_headers=32768, ++ max_headers=128, + max_field_size=8190, + ) + +@@ -297,9 +299,20 @@ def test_parse_headers_longline(parser: Any) -> None: + parser.feed_data(text) + + ++@pytest.fixture ++def xfail_c_parser_status(request) -> None: ++ if isinstance(request.getfixturevalue("parser"), HttpRequestParserPy): ++ return ++ request.node.add_marker( ++ pytest.mark.xfail( ++ reason="Regression test for Py parser. May match C behaviour later.", ++ raises=http_exceptions.BadStatusLine, ++ ) ++ ) ++ ++ ++@pytest.mark.usefixtures("xfail_c_parser_status") + def test_parse_unusual_request_line(parser) -> None: +- if not isinstance(response, HttpResponseParserPy): +- pytest.xfail("Regression test for Py parser. May match C behaviour later.") + text = b"#smol //a HTTP/1.3\r\n\r\n" + messages, upgrade, tail = parser.feed_data(text) + assert len(messages) == 1 +@@ -696,13 +709,14 @@ def test_max_header_field_size(parser, size) -> None: + name = b"t" * size + text = b"GET /test HTTP/1.1\r\n" + name + b":data\r\n\r\n" + +- match = f"400, message:\n Got more than 8190 bytes \\({size}\\) when reading" ++ match = "400, message:\n Got more than 8190 bytes when reading" + with pytest.raises(http_exceptions.LineTooLong, match=match): +- parser.feed_data(text) ++ for i in range(0, len(text), 5000): # pragma: no branch ++ parser.feed_data(text[i : i + 5000]) + + +-def test_max_header_field_size_under_limit(parser) -> None: +- name = b"t" * 8190 ++def test_max_header_size_under_limit(parser: HttpRequestParser) -> None: ++ name = b"t" * 8185 + text = b"GET /test HTTP/1.1\r\n" + name + b":data\r\n\r\n" + + messages, upgrade, tail = parser.feed_data(text) +@@ -724,14 +738,68 @@ def test_max_header_value_size(parser, size) -> None: + name = b"t" * size + text = b"GET /test HTTP/1.1\r\n" b"data:" + name + b"\r\n\r\n" + +- match = f"400, message:\n Got more than 8190 bytes \\({size}\\) when reading" ++ match = "400, message:\n Got more than 8190 bytes when reading" ++ with pytest.raises(http_exceptions.LineTooLong, match=match): ++ for i in range(0, len(text), 4000): # pragma: no branch ++ parser.feed_data(text[i : i + 4000]) ++ ++ ++def test_max_header_combined_size(parser: HttpRequestParser) -> None: ++ k = b"t" * 4100 ++ text = b"GET /test HTTP/1.1\r\n" + k + b":" + k + b"\r\n\r\n" ++ ++ match = "400, message:\n Got more than 8190 bytes when reading" + with pytest.raises(http_exceptions.LineTooLong, match=match): + parser.feed_data(text) + + +-def test_max_header_value_size_under_limit(parser) -> None: +- value = b"A" * 8190 +- text = b"GET /test HTTP/1.1\r\n" b"data:" + value + b"\r\n\r\n" ++@pytest.mark.parametrize("size", [40960, 8191]) ++async def test_max_trailer_size(parser: HttpRequestParser, size: int) -> None: ++ value = b"t" * size ++ text = ( ++ b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n" ++ + hex(4000)[2:].encode() ++ + b"\r\n" ++ + b"b" * 4000 ++ + b"\r\n0\r\ntest: " ++ + value ++ + b"\r\n\r\n" ++ ) ++ ++ match = "400, message:\n Got more than 8190 bytes when reading" ++ with pytest.raises(http_exceptions.LineTooLong, match=match): ++ payload = None ++ for i in range(0, len(text), 3000): # pragma: no branch ++ messages, upgrade, tail = parser.feed_data(text[i : i + 3000]) ++ if messages: ++ payload = messages[0][-1] ++ # Trailers are not seen until payload is read. ++ assert payload is not None ++ await payload.read() ++ ++ ++@pytest.mark.parametrize("headers,trailers", ((129, 0), (0, 129), (64, 65))) ++async def test_max_headers( ++ parser: HttpRequestParser, headers: int, trailers: int ++) -> None: ++ text = ( ++ b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked" ++ + b"".join(b"\r\nHeader-%d: Value" % i for i in range(headers)) ++ + b"\r\n\r\n4\r\ntest\r\n0" ++ + b"".join(b"\r\nTrailer-%d: Value" % i for i in range(trailers)) ++ + b"\r\n\r\n" ++ ) ++ ++ match = "Too many (headers|trailers) received" ++ with pytest.raises(http_exceptions.BadHttpMessage, match=match): ++ messages, upgrade, tail = parser.feed_data(text) ++ # Trailers are not seen until payload is read. ++ await messages[0][-1].read() ++ ++ ++def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None: ++ value = b"A" * 8185 ++ text = b"GET /test HTTP/1.1\r\ndata:" + value + b"\r\n\r\n" + + messages, upgrade, tail = parser.feed_data(text) + msg = messages[0][0] +@@ -752,13 +820,16 @@ def test_max_header_value_size_continuation(response, size) -> None: + name = b"T" * (size - 5) + text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + name + b"\r\n\r\n" + +- match = f"400, message:\n Got more than 8190 bytes \\({size}\\) when reading" ++ match = "400, message:\n Got more than 8190 bytes when reading" + with pytest.raises(http_exceptions.LineTooLong, match=match): +- response.feed_data(text) ++ for i in range(0, len(text), 9000): # pragma: no branch ++ response.feed_data(text[i : i + 9000]) + + +-def test_max_header_value_size_continuation_under_limit(response) -> None: +- value = b"A" * 8185 ++def test_max_header_value_size_continuation_under_limit( ++ response: HttpResponseParser, ++) -> None: ++ value = b"A" * 8179 + text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + value + b"\r\n\r\n" + + messages, upgrade, tail = response.feed_data(text) +@@ -956,13 +1027,13 @@ def test_http_request_parser_bad_nonascii_uri(parser: Any) -> None: + @pytest.mark.parametrize("size", [40965, 8191]) + def test_http_request_max_status_line(parser, size) -> None: + path = b"t" * (size - 5) +- match = f"400, message:\n Got more than 8190 bytes \\({size}\\) when reading" ++ match = "400, message:\n Got more than 8190 bytes when reading" + with pytest.raises(http_exceptions.LineTooLong, match=match): + parser.feed_data(b"GET /path" + path + b" HTTP/1.1\r\n\r\n") + + +-def test_http_request_max_status_line_under_limit(parser) -> None: +- path = b"t" * (8190 - 5) ++def test_http_request_max_status_line_under_limit(parser: HttpRequestParser) -> None: ++ path = b"t" * 8172 + messages, upgraded, tail = parser.feed_data( + b"GET /path" + path + b" HTTP/1.1\r\n\r\n" + ) +@@ -1039,13 +1110,15 @@ def test_http_response_parser_strict_obs_line_folding(response: Any) -> None: + @pytest.mark.parametrize("size", [40962, 8191]) + def test_http_response_parser_bad_status_line_too_long(response, size) -> None: + reason = b"t" * (size - 2) +- match = f"400, message:\n Got more than 8190 bytes \\({size}\\) when reading" ++ match = "400, message:\n Got more than 8190 bytes when reading" + with pytest.raises(http_exceptions.LineTooLong, match=match): + response.feed_data(b"HTTP/1.1 200 Ok" + reason + b"\r\n\r\n") + + +-def test_http_response_parser_status_line_under_limit(response) -> None: +- reason = b"O" * 8190 ++def test_http_response_parser_status_line_under_limit( ++ response: HttpResponseParser, ++) -> None: ++ reason = b"O" * 8177 + messages, upgraded, tail = response.feed_data( + b"HTTP/1.1 200 " + reason + b"\r\n\r\n" + ) +@@ -1552,7 +1625,7 @@ def test_parse_bad_method_for_c_parser_raises(loop, protocol): + loop, + 2**16, + max_line_size=8190, +- max_headers=32768, ++ max_headers=128, + max_field_size=8190, + ) + +@@ -1867,7 +1940,7 @@ class TestDeflateBuffer: + dbuf = DeflateBuffer(buf, "deflate") + + # Feed compressed data in chunks (simulating network streaming) +- for i in range(0, len(compressed), chunk_size): ++ for i in range(0, len(compressed), chunk_size): # pragma: no branch + chunk = compressed[i : i + chunk_size] + dbuf.feed_data(chunk, len(chunk)) + +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index feb9039ae1..8b835341f3 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -16,6 +16,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2025-69229_p2.patch \ file://CVE-2025-69227.patch \ file://CVE-2025-69223.patch \ + file://CVE-2026-22815.patch \ " PYPI_PACKAGE = "aiohttp" From patchwork Mon Aug 31 04:57:41 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96878 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id AF675C624C6 for ; Mon, 31 Aug 2026 04:57:51 +0000 (UTC) Received: from alln-iport-7.cisco.com (alln-iport-7.cisco.com [173.37.142.94]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.23002.1788152269150094967 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=N2tD/y2/; spf=pass (domain: cisco.com, ip: 173.37.142.94, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=4007; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=/5FOf2rsRU7tV5uIJO7eYFcF0MllSiVyaLqlYbbHaIY=; b=N2tD/y2/KshfLIM/K6UhP2KbM8OkYpkRT9/MWEvsjmZ44OHYCgA+OqZQ NEIXW2I8sDMDpR9m+w7ebMwSWqMGzcmPNiKAKxJWXN6D2lkPL9BTVZl14 kakEzuik1ePKvefb1k8bjTX1z34v46ARrBAzYo/NqwcPATKOQsF3dnEzg x68gmzdHC3SkmkwMcZIj9taCfpIlPIncyIzt/YSeSq6a3H2NmgkrdmluU Ci71bwBgLbU9PLj5XBespibPgsOAtHqbF+qpSvv/Ej5WdszD0GtcV0qa1 2hlv8iRpKW5lCmfHoaiHT8SVo7zsLXlL2hX3m9OcW1BqNVOWGybvPW+z8 g==; X-CSE-ConnectionGUID: JtHfncpXQ3CIBJGu7Rq9qA== X-CSE-MsgGUID: EY0YaMCbQNygWWZZZ1CJbA== X-IPAS-Result: A0BJAgCvCJVq/5EQJK1aglmCV3ReQ0mWSgOLZJI3gX4PAQEBD0QNBAEBhD9GAo1zAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQsBAQUBAQECAQcFgQ4Thk8NhloBAgEDJwsBGAEbEhAcAwECLyALIwgZgwIBgjoDNwMRwgOBeTOBAYMoAT8CQ1DYSw2CWAELFAGBOIU/gn+FI10YAYJJgjMnGxuBcoR+gQWBGkIBAYglBIIigQyBWpIfSIEeA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+F4ENGwYFgR2BKIQPIxk2eoEJXoErKWABEheBCYIIAoJaggUCAUlDDgdHUwkECxgNSBEsNxUZBD5uB456H4JLUzsBCiEEghCmDqAecQoog3aMIo8+hXwaM4VbpRELmH2OCoQJkkeEaYFoPIFHCwdwFYMiCUoZD444g2uBf4NlxlUnMgIJMgEBBwIHDgMLgWiRfgEB IronPort-Data: A9a23:48gV0Klhlv+i+2b6oqp2/Ufo5gzQJ0RdPkR7XQ2eYbSJt1+Wr1Gzt xJNXTrUO6mKMWGnL4wlbIq2p0pQ7MTXmt9iTVA9rCAxEFtH+JHPbTi7wugcHM8zwunrFh8PA xA2M4GYRCwMZiaC4E/raf658SUUOZigHtLUEPTDNj16WThqQSIgjQMLs+Mii+aEu/Dha++2k Y20+ZC31GONgWYubDpEsPrb8XuDgdyr0N8mlg1mDRx0lAe2e0k9VPo3Oay3Jn3kdYhYdsbSb /rD1ryw4lTC9B4rDN6/+p6jGqHdauePVeQmoiM+t5mK2nCulARrukoIHKZ0hXNsttm8t4sZJ OOhGnCHYVxB0qXkwIzxWvTDes10FfUuFLTveRBTvSEPpqHLWyOE/hlgMK05Fa8E2uhxCnl+z KczCRovQQ3cvfvn2ZvuH4GAhux7RCXqFIobvnclyXTSCuwrBMiTBa7L/tRfmjw3g6iiH96HO JFfMmQpNUqGOkESUrsUIMpWcOOAinrydzRZuVu9rqss6G+Vxwt0uFToGIqPIYPSFZgOwC50o ErrzjzICzQxPuW52DW6zUCetLbkvynSDdd6+LqQs6QCbEeo7msLBRsbUFG2rfW0hguyVsxSL 2QQ+zEytu417EGtQ9z3UhG0rXLCuQQTM+e8CMUz7AWLj66R6AGDCy1cHnhKaccts4k9QjlCO kK1ou4FzAdH6NW9IU9xPJ/Pxd9uEUD59VM/WBI= IronPort-HdrOrdr: A9a23:JpsROqnmjpJ7BXyl0QeY0phGe2HpDfIO3DAbv31ZSRFFG/FwWf rAoB19726StN9/YhAdcLy7VZVoBEmsl6KdgrNhWYtKIjOHhILAFugLhuHfKn/bakjDH4Vmu5 uIHZITNDSJNykYse/KpC+lDt0n3N6LtIqshevY0jNRaDsCUdAH0++8YTzranGfg2J9dOMEKK Y= X-Talos-CUID: 9a23:8VH2YG8hh9qopchgNiuVv2wIOs4jeWXt9Sz7AmWaJns2cqTEUHbFrQ== X-Talos-MUID: 9a23:XsgCXAr4LgWbOq78TKEezxxTbPZhxo+kMWYcnqo5ifu2bAdSMR7I2Q== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="823708536" Received: from alln-l-core-08.cisco.com ([173.36.16.145]) by alln-iport-7.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-08.cisco.com (Postfix) with ESMTPS id 9DA641800047F; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id DC8F2CD02BD; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 6/9] python3-aiohttp: fix CVE-2026-34514 Date: Sun, 30 Aug 2026 21:57:41 -0700 Message-Id: <20260831045744.3321483-7-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-08.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129604 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1] and [2]. The advisory identifying the fix is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/9a6ada97e2c6cf1ce31727c6c9fcea17c21f6f06 [2] https://github.com/aio-libs/aiohttp/commit/dab9e879be5606682a39b9dd378900eba0afd1a4 [3] https://nvd.nist.gov/vuln/detail/CVE-2026-34514 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34514.patch | 65 +++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 66 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34514.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34514.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34514.patch new file mode 100644 index 0000000000..baba4c9b91 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34514.patch @@ -0,0 +1,65 @@ +From b3aca47a956826116930a59e482953cacf78830a Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sat, 21 Feb 2026 00:17:11 +0000 +Subject: [PATCH] Fix multipart injection (#12104) (#12110) + +CVE: CVE-2026-34514 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/9a6ada97e2c6cf1ce31727c6c9fcea17c21f6f06] + +(cherry picked from commit dab9e879be5606682a39b9dd378900eba0afd1a4) + +Co-authored-by: mingi jung +(cherry picked from commit 9a6ada97e2c6cf1ce31727c6c9fcea17c21f6f06) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/formdata.py | 5 +++++ + tests/test_formdata.py | 16 +++++++++++----- + 2 files changed, 16 insertions(+), 5 deletions(-) + +diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py +index 2b75b3de7..c14ada176 100644 +--- a/aiohttp/formdata.py ++++ b/aiohttp/formdata.py +@@ -79,6 +79,11 @@ class FormData: + raise TypeError( + "content_type must be an instance of str. " "Got: %s" % content_type + ) ++ if "\r" in content_type or "\n" in content_type: ++ raise ValueError( ++ "Newline or carriage return detected in headers. " ++ "Potential header injection attack." ++ ) + headers[hdrs.CONTENT_TYPE] = content_type + self._is_multipart = True + if content_transfer_encoding is not None: +diff --git a/tests/test_formdata.py b/tests/test_formdata.py +index 4bb8aa075..9e331515c 100644 +--- a/tests/test_formdata.py ++++ b/tests/test_formdata.py +@@ -46,12 +46,18 @@ def test_invalid_formdata_params2() -> None: + FormData("as") # 2-char str is not allowed + + +-def test_invalid_formdata_content_type() -> None: ++@pytest.mark.parametrize("val", (0, 0.1, {}, [], b"foo")) ++def test_invalid_type_formdata_content_type(val: object) -> None: + form = FormData() +- invalid_vals = [0, 0.1, {}, [], b"foo"] +- for invalid_val in invalid_vals: +- with pytest.raises(TypeError): +- form.add_field("foo", "bar", content_type=invalid_val) ++ with pytest.raises(TypeError): ++ form.add_field("foo", "bar", content_type=val) # type: ignore[arg-type] ++ ++ ++@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r")) ++def test_invalid_value_formdata_content_type(val: str) -> None: ++ form = FormData() ++ with pytest.raises(ValueError): ++ form.add_field("foo", "bar", content_type=val) + + + def test_invalid_formdata_filename() -> None: +-- +2.44.4 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index 8b835341f3..f6731a64b3 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -17,6 +17,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2025-69227.patch \ file://CVE-2025-69223.patch \ file://CVE-2026-22815.patch \ + file://CVE-2026-34514.patch \ " PYPI_PACKAGE = "aiohttp" From patchwork Mon Aug 31 04:57:42 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96877 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 06999C624A5 for ; Mon, 31 Aug 2026 04:57:51 +0000 (UTC) Received: from alln-iport-2.cisco.com (alln-iport-2.cisco.com [173.37.142.89]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.23004.1788152269674346202 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=k6nJes+4; spf=pass (domain: cisco.com, ip: 173.37.142.89, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=8230; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=P69w7RImzcVOT3BHlti4noxahk6QvE0gPm0pfOQNTJo=; b=k6nJes+4dInNOE5lj3Ws6qN21M+ufa87xvSD81bCCsDWQ6P/oCwDFU/E XMrE6aTSIywALYdP+opkIfGniDOZPj8op/34CnQbrXGR1cXxlWEhslANG lNPMdW0XgKQZgYSmjaETiyHKVt0PNT10C/hZfaScdodZeir8jnhJY13Sb 8/QhyekcE6imdmPeGxGdKze2cOp4DTxhCkzIzipKd3fsxM3wdECk+ZrWP 65SGt/txj1p8p4/1NxOOpOL6Beumq/cFx/PsV1YSzo8omUdSsb7SujzZ7 b+p+d2Juhh8Ta17FSHvTEYzrPiu5uxPfPx25D7DMC7WcnEqEUD/RFWAfK Q==; X-CSE-ConnectionGUID: mGSFDeO4Szuiyd0negFgeQ== X-CSE-MsgGUID: j5L+sAxiTd6u1U3T+xT2JQ== X-IPAS-Result: A0BIAgCvCJVq/44QJK1aglmCV3ReQ0mWSgOeGxSBag8BAQEPRA0EAQGEP0YCjXMCJjQJDgECBAMCAwEBAQEBAQEBAQEBCwEBBQEBAQIBBwWBDhOGTw2GWgECAQMnCwEYAS0QHAMBAi8rIwgZgwIBgnQDEcIDgXkzgQGDKAE/AkNQ2zABCxQBgTiFP4giXRgBhHwnGxuBcoJQgi6BBYFcAQGBOA6GXwSCIoEMgVqBF5EISIEeA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+F4ENGwYFgR2BKIQPIxk2eoEJXoErKWABEheBCYIIAoJaggUCAUlDDgdHUwkECxgNSBEsNxUZBD5uB456H4JLgQ4BKgGBfRc0kxCSSoE1n1oKKIN2jCKVOhozhVulEQuYfYs3glOVWnaEaYFoPIFHCwdwFTuCZwlKGQ+OKg4Lg2CBf4NlxlUnMgIJMgEBBwIHDgMLgWiQAYEdYAEB IronPort-Data: A9a23:ItFTjaJvtmTNg5cyFE+RgJQlxSXFcZb7ZxGr2PjKsXjdYENShWMHy WoaXGCDPvaMN2fxe40iPtzg9xtSsMWEy4BhT1Md+CA2RRqmiyZq6fd1j6vUF3nPRiEWZBs/t 63yUvGZcoZsCCSa/kvxWlTYhSEU/bmSQbbhA/LzNCl0RAt1IA8skhsLd9QR2uaEuvDnRVnQ0 T/Oi5eHYgH9i2Usajt8B5+r8XuDgtyj4Fv0gXRmDRx7lAe2v2UYCpsZOZawIxPQKqFIHvS3T vr017qw+GXU5X8FUrtJRZ6iLyXm6paLVeS/oiI+t5qK23CulQRuukoPD8fwXG8M49m/c3+d/ /0W3XC4YV9B0qQhA43xWTEAe811FfUuFLMqvRFTvOTLp3AqfUcAzN1rI3oYEYA/pt1lHGBf+ aQeJT8jYR260rfeLLKTEoGAh+wqKM3teYdasXZ6wHSBULAtQIvIROPB4towMDUY358VW62AI ZNHL2MzNnwsYDUXUrsTIJ49keOhh2j2WzZZs1mS46Ew5gA/ySQhiuGzaYSOKoHiqcN9nBu85 Xnv72XFDk8HbeHE9XmVziiOv7qa9c/8cMdIfFGizdZtmFCVy2kZBREaWFf+rfSnh0qWX9NEN 1dS/TIjq6U3/kGnQtTxGRqirxa5UgU0Ut5UFag+rQqK0KeRu1vfDWkfRTkHY9sj3CMreQEXO payt4uBLVRSXHe9EBpxKp/8QeuOBBUo IronPort-HdrOrdr: A9a23:8Eq7I6/cPo0uStqTSv5uk+D6I+orL9Y04lQ7vn2ZhyY7TiX+rb HIoB11737JYVoqNU3I3OrwWpVoIkmskaKdn7NwAV7KZmCP0wGVxcNZnO7fKlbbdREWmNQw6U 4ZSdkcNPTASX5nkM39/A60V/wkwNWB7eSUoN229QYKcemvAJsQlzuQzW2gYzRLeDU= X-Talos-CUID: 9a23:2JF1/m7SUodxBEY01Nss8WEvNOwoVSHm3mrRMkPgD2tmZrKYcArF X-Talos-MUID: 9a23:N9S9oAWRd7nvfz/q/D2xoHJ5Hddp36jwFRkwyqlFq/XeFxUlbg== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="821616385" Received: from alln-l-core-05.cisco.com ([173.36.16.142]) by alln-iport-2.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-05.cisco.com (Postfix) with ESMTPS id A1FB118000220; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id E017FCD02BE; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 7/9] python3-aiohttp: fix CVE-2026-34513 Date: Sun, 30 Aug 2026 21:57:42 -0700 Message-Id: <20260831045744.3321483-8-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-05.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129606 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1] and [2]. The advisory identifying the fix is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/c4d77c3533122be353b8afca8e8675e3b4cbda98 [2] https://github.com/aio-libs/aiohttp/commit/8ab84c52fe58ef34794fa9b12f00b06e626adcc0 [3] https://nvd.nist.gov/vuln/detail/CVE-2026-34513 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34513.patch | 211 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 212 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34513.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34513.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34513.patch new file mode 100644 index 0000000000..be8c3e4d0f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34513.patch @@ -0,0 +1,211 @@ +From d094b4d3ad66d824ebfc8900234dff7aaf4acec0 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sun, 22 Feb 2026 12:58:22 +0000 +Subject: [PATCH] Bound DNS cache (#12106) (#12117) + +--------- + +CVE: CVE-2026-34513 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/c4d77c3533122be353b8afca8e8675e3b4cbda98] + +Backport Changes: +- Imported OrderedDict for the aiohttp 3.9.5 collections layout. +- Kept Dict[str, Any] because 3.9.5 lacks ResolveResult. +- Removed ResolveResult annotations from the backported tests. +- Kept family: int = 0 from the 3.9.5 connector API. +- Kept _throttle_dns_events from the 3.9.5 DNS flow. + +(cherry picked from commit 8ab84c52fe58ef34794fa9b12f00b06e626adcc0) +Co-authored-by: gonas +(cherry picked from commit c4d77c3533122be353b8afca8e8675e3b4cbda98) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12106.feature.rst | 1 + + aiohttp/connector.py | 26 ++++++++++---- + tests/test_connector.py | 76 +++++++++++++++++++++++++++++++++++++++ + 3 files changed, 96 insertions(+), 7 deletions(-) + create mode 100644 CHANGES/12106.feature.rst + +diff --git a/CHANGES/12106.feature.rst b/CHANGES/12106.feature.rst +new file mode 100644 +index 000000000..daa9088ee +--- /dev/null ++++ b/CHANGES/12106.feature.rst +@@ -0,0 +1 @@ ++Added a ``dns_cache_max_size`` parameter to ``TCPConnector`` to limit the size of the cache -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/connector.py b/aiohttp/connector.py +index f95ebe84c..7267d9a5c 100644 +--- a/aiohttp/connector.py ++++ b/aiohttp/connector.py +@@ -4,7 +4,7 @@ import random + import sys + import traceback + import warnings +-from collections import defaultdict, deque ++from collections import OrderedDict, defaultdict, deque + from contextlib import suppress + from http import HTTPStatus + from http.cookies import SimpleCookie +@@ -690,25 +690,33 @@ class BaseConnector: + + + class _DNSCacheTable: +- def __init__(self, ttl: Optional[float] = None) -> None: +- self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[Dict[str, Any]], int]] = {} ++ def __init__(self, ttl: Optional[float] = None, max_size: int = 1000) -> None: ++ self._addrs_rr: OrderedDict[ ++ Tuple[str, int], Tuple[Iterator[Dict[str, Any]], int] ++ ] = OrderedDict() + self._timestamps: Dict[Tuple[str, int], float] = {} + self._ttl = ttl ++ self._max_size = max_size + + def __contains__(self, host: object) -> bool: + return host in self._addrs_rr + + def add(self, key: Tuple[str, int], addrs: List[Dict[str, Any]]) -> None: ++ if key in self._addrs_rr: ++ self._addrs_rr.move_to_end(key) ++ + self._addrs_rr[key] = (cycle(addrs), len(addrs)) + + if self._ttl is not None: + self._timestamps[key] = monotonic() + ++ if len(self._addrs_rr) > self._max_size: ++ oldest_key, _ = self._addrs_rr.popitem(last=False) ++ self._timestamps.pop(oldest_key, None) ++ + def remove(self, key: Tuple[str, int]) -> None: + self._addrs_rr.pop(key, None) +- +- if self._ttl is not None: +- self._timestamps.pop(key, None) ++ self._timestamps.pop(key, None) + + def clear(self) -> None: + self._addrs_rr.clear() +@@ -719,6 +727,7 @@ class _DNSCacheTable: + addrs = list(islice(loop, length)) + # Consume one more element to shift internal state of `cycle` + next(loop) ++ self._addrs_rr.move_to_end(key) + return addrs + + def expired(self, key: Tuple[str, int]) -> bool: +@@ -760,6 +769,7 @@ class TCPConnector(BaseConnector): + fingerprint: Optional[bytes] = None, + use_dns_cache: bool = True, + ttl_dns_cache: Optional[int] = 10, ++ dns_cache_max_size: int = 1000, + family: int = 0, + ssl_context: Optional[SSLContext] = None, + ssl: Union[bool, Fingerprint, SSLContext] = True, +@@ -789,7 +799,9 @@ class TCPConnector(BaseConnector): + self._resolver = resolver + + self._use_dns_cache = use_dns_cache +- self._cached_hosts = _DNSCacheTable(ttl=ttl_dns_cache) ++ self._cached_hosts = _DNSCacheTable( ++ ttl=ttl_dns_cache, max_size=dns_cache_max_size ++ ) + self._throttle_dns_events: Dict[Tuple[str, int], EventResultOrError] = {} + self._family = family + self._local_addr = local_addr +diff --git a/tests/test_connector.py b/tests/test_connector.py +index 02e48bc10..dd5e2c9e1 100644 +--- a/tests/test_connector.py ++++ b/tests/test_connector.py +@@ -2197,6 +2197,25 @@ async def test_named_pipe_connector( + + + class TestDNSCacheTable: ++ host1 = ("localhost", 80) ++ host2 = ("foo", 80) ++ result1 = { ++ "hostname": "localhost", ++ "host": "127.0.0.1", ++ "port": 80, ++ "family": socket.AF_INET, ++ "proto": 0, ++ "flags": socket.AI_NUMERICHOST, ++ } ++ result2 = { ++ "hostname": "foo", ++ "host": "127.0.0.2", ++ "port": 80, ++ "family": socket.AF_INET, ++ "proto": 0, ++ "flags": socket.AI_NUMERICHOST, ++ } ++ + @pytest.fixture + def dns_cache_table(self): + return _DNSCacheTable() +@@ -2282,6 +2301,63 @@ class TestDNSCacheTable: + addrs = dns_cache_table.next_addrs("foo") + assert addrs == ["127.0.0.1"] + ++ def test_max_size_eviction(self) -> None: ++ table = _DNSCacheTable(max_size=2) ++ ++ table.add(self.host1, [self.result1]) ++ table.add(self.host2, [self.result2]) ++ ++ host3 = ("example.com", 80) ++ result3 = { ++ **self.result1, ++ "hostname": "example.com", ++ "host": "1.2.3.4", ++ } ++ table.add(host3, [result3]) ++ ++ assert len(table._addrs_rr) == 2 ++ assert self.host1 not in table._addrs_rr ++ assert host3 in table._addrs_rr ++ ++ def test_lru_eviction(self) -> None: ++ table = _DNSCacheTable(max_size=2) ++ ++ table.add(self.host1, [self.result1]) ++ table.add(self.host2, [self.result2]) ++ ++ table.next_addrs(self.host1) ++ ++ host3 = ("example.com", 80) ++ result3 = { ++ **self.result1, ++ "hostname": "example.com", ++ "host": "1.2.3.4", ++ } ++ table.add(host3, [result3]) ++ ++ assert self.host1 in table._addrs_rr ++ assert self.host2 not in table._addrs_rr ++ ++ def test_lru_eviction_add(self) -> None: ++ table = _DNSCacheTable(max_size=2) ++ ++ table.add(self.host1, [self.result1]) ++ table.add(self.host2, [self.result2]) ++ ++ # Re-add, thus making host1 the most recently used. ++ table.add(self.host1, [self.result1]) ++ ++ host3 = ("example.com", 80) ++ result3 = { ++ **self.result1, ++ "hostname": "example.com", ++ "host": "1.2.3.4", ++ } ++ table.add(host3, [result3]) ++ ++ assert self.host1 in table._addrs_rr ++ assert self.host2 not in table._addrs_rr ++ + + async def test_connector_cache_trace_race(): + class DummyTracer: +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index f6731a64b3..e104b62e1e 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -18,6 +18,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2025-69223.patch \ file://CVE-2026-22815.patch \ file://CVE-2026-34514.patch \ + file://CVE-2026-34513.patch \ " PYPI_PACKAGE = "aiohttp" From patchwork Mon Aug 31 04:57:43 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96879 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 5878BC624C2 for ; Mon, 31 Aug 2026 04:57:51 +0000 (UTC) Received: from alln-iport-6.cisco.com (alln-iport-6.cisco.com [173.37.142.93]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.22957.1788152269275233289 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=gr6JB0Td; spf=pass (domain: cisco.com, ip: 173.37.142.93, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=13960; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=4Daz4g8CztPUK0y4ZTgiMEedIhFH65z629dJd2LrK0U=; b=gr6JB0Tdf4onH314DoZgmePoG1c+TyBK8g4uSvAEb7/gaLY3/sgD84Yi LcoQ2/Zi2YnegAcCd5v7piXK1k/CIVSfroYPa+FtCTdDQW45zY9Fd9Lj1 R4wyA3FRhKTcEMUkrcKXpVHRsEdg1dDDHV70ZrVMP2jIs33lo3FTHgf6t u/HN13VsOd7lQz8gOqhhLUVVcsJcur2ivN75wwHD8svh3jYMBaeLAsClS DPchJ1vkp3n0xfb1+LzVgoLoZFCU3Wav0Jl4uPmDHTGcF/nm+smGyIK74 krApLS0B6QRjd4rHM8FdsFoaOQVGBitgoI37pEz8bIL90DSGgET4Z1NaI w==; X-CSE-ConnectionGUID: 8x0Pq1vtTSyzIrwdsQgdlg== X-CSE-MsgGUID: zIV9ZJotS4+qww8/9DJm+g== X-IPAS-Result: A0BIAgAsCZVq/5IQJK1aglmCV3ReQ0mWSgOeG4F+DwEBAQ9EDQQBAYQ/RgKNcwImNAkOAQIEAwIDAQEBAQEBAQEBAQELAQEFAQEBAgEHBYEOE4ZPDYZaAQIBAycLARgBLRAcAwECLysjCBmDAgGCdAMRBsF7gXkzgQGDKAE/AgJAAVDbMAELFAGBOIU/iCJdGAGEfCcbG4FyhH6BBYFcAQECgUiGWwSCIoEMgVqBF5EISIEeA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+F4ENGwYFgR2BKIQPIxk2eoEJXoErKWABEheBCYIIAoJaggUCAUlDDgdHUwkECxgNSBEsNxUZBD5uB456H4JLYS0BKQEBBHsSegIHEwUOAw0cFJJpOY9lgiGBNZ9aCiiDdowilToaM4VbpRELmH2JAYI2glOWUIRpgWg8gUcLB3AVO4JnCUoZD44LLYNrgX+DFFHGVScyAgkyAQEHAgcOAwuBaJABASYHgU8BAQ IronPort-Data: A9a23:UrM5Kq1pe4pB6WLQofbD5YJwkn2cJEfYwER7XKvMYLTBsI5bpzNWy GZMWGGPb6mLZmT2eY10bY+xph8Bv5XTxtFjGgo43Hw8FHgiRegpqji6wuYcGwvIc6UvmWo+t 512huHodZ5yFjmH4E/xbtANlFEkvYmQXL3wFeXYDS54QA5gWU8JhAlq8wIDqtYAbeORXUXX5 Lsen+WFYAX7g24vajpNg06+gEoHUMra6WtwUmMWPZinjHeG/1EJAZQWI72GLneQauF8Au6gS u/f+6qy92Xf8g1FIovNfmHTKxBirhb6ZGBiu1IOM0SQqkEqSh8ajs7XAMEhhXJ/0F1lqTzeJ OJl7vRcQS9xVkHFdX90vxNwS0mSNoUekFPLzOTWXcG7lyX7n3XQL/pGXFFuBZRB5PpLOnBc1 OYHJCs0YRyHiLfjqF67YrEEasULJc3vOsYb/3pn1zycVadgSpHYSKKM7thdtNsyrpkRRrCFO YxAN3w2MEqojx5nYj/7DLoyn+qsj3juehVTqUmeouw85G27IAlZgOK2bIaJJILaLSlTtlS5m DP03WPlOU1ZLcOAmX3U3lCpxeCayEsXX6pXTtVU7MVCh0WewGEWAhAaWVa35PW0lEO6c9ZeM FAPvC02oK4/8UamQtXwU1u/unHsg/IHc9NUF+t/7ESGzbDZpl7IQGMFVTVGLtchsafaWAAX6 7NApPuxbRQHjVFfYSv1Gmu8xd9qBRUoEA== IronPort-HdrOrdr: A9a23:5bumxqwPOwB0ziteGB9yKrPwDr1zdoMgy1knxilNoNJuHfBw8P re+8jzuiWUtN98YhwdcJW7Scu9qBDnhPpICPcqXYtKNTOO0ADDEGgh1/qG/9SKIUPDH4BmuZ uIC5IOa+HYPBxdkdvw5hW+HpILxdmK973tuMLlpk0dKz2Dr8pbnn9E4sHxKDwOeDV7 X-Talos-CUID: 9a23:IVMax2pqLnyMdO7JBHn8N8DmUZA1KkXElWnMH3SpG0dzV7GOcFCc/Kwxxg== X-Talos-MUID: 9a23:xWGJngQ7REQIDSVBRXT+pQ8zBuVv/5/yS1kSjsVB6uWmMG9/bmI= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="823444668" Received: from alln-l-core-09.cisco.com ([173.36.16.146]) by alln-iport-6.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-09.cisco.com (Postfix) with ESMTPS id AA31918000446; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id E78F9CD02BF; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 8/9] python3-aiohttp: fix CVE-2026-34993 Date: Sun, 30 Aug 2026 21:57:43 -0700 Message-Id: <20260831045744.3321483-9-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-09.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:51 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129605 From: Darsh Kelaiya This patch applies the reviewed upstream fix shown in [1]. The advisory identifying the fix is referenced in [2]. [1] https://github.com/aio-libs/aiohttp/commit/dcf40f30637e8752c76781cf6703b5a236749a00 [2] https://nvd.nist.gov/vuln/detail/CVE-2026-34993 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34993.patch | 364 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 365 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34993.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34993.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34993.patch new file mode 100644 index 0000000000..76756759c8 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34993.patch @@ -0,0 +1,364 @@ +From d4f415a2a236f92c23b66f7e025419f1c3e9ac77 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Sun, 22 Feb 2026 15:53:43 +0000 +Subject: [PATCH] Restrict pickle deserialization in CookieJar.load() (#12105) + +**This is a backport of PR #12091 as merged into master +(8a631e74c1d266499dbc6bcdbc83c60f4ea3ee3c).** + +--------- + +CVE: CVE-2026-34993 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/dcf40f30637e8752c76781cf6703b5a236749a00] + +Backport Changes: +- Omitted tests/conftest.py Blockbuster allowances because + aiohttp 3.9.5 does not use the upstream Blockbuster fixture. +- Omitted the partitioned-cookie JSON roundtrip test because + aiohttp 3.9.5 lacks partitioned-cookie support. +- Adapted the secure-cookie JSON roundtrip test to construct a + SimpleCookie and call CookieJar.update_cookies(), since + aiohttp 3.9.5 lacks update_cookies_from_headers(). +- Omitted CHANGES/12091.bugfix.rst, docs/client_reference.rst, + and docs/spelling_wordlist.txt as release documentation unrelated + to the runtime security fix; the upstream 3.14 version directives + do not apply to this aiohttp 3.9.5 backport. + +Co-authored-by: Yuval Elbar <41901908+YuvalElbar6@users.noreply.github.com> +Co-authored-by: Sam Bull +(cherry picked from commit dcf40f30637e8752c76781cf6703b5a236749a00) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/cookiejar.py | 114 ++++++++++++++++++++++++++- + tests/test_cookiejar.py | 168 ++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 278 insertions(+), 4 deletions(-) + +diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py +index a348f112c..376f7597a 100644 +--- a/aiohttp/cookiejar.py ++++ b/aiohttp/cookiejar.py +@@ -2,6 +2,7 @@ import asyncio + import calendar + import contextlib + import datetime ++import json + import os # noqa + import pathlib + import pickle +@@ -36,6 +37,41 @@ __all__ = ("CookieJar", "DummyCookieJar") + CookieItem = Union[str, "Morsel[str]"] + + ++class _RestrictedCookieUnpickler(pickle.Unpickler): ++ """A restricted unpickler that only allows cookie-related types. ++ ++ This prevents arbitrary code execution when loading pickled cookie data ++ from untrusted sources. Only types that are expected in a serialized ++ CookieJar are permitted. ++ ++ See: https://docs.python.org/3/library/pickle.html#restricting-globals ++ """ ++ ++ _ALLOWED_CLASSES: frozenset[tuple[str, str]] = frozenset( ++ { ++ # Core cookie types ++ ("http.cookies", "SimpleCookie"), ++ ("http.cookies", "Morsel"), ++ # Container types used by CookieJar._cookies ++ ("collections", "defaultdict"), ++ # builtins that pickle uses for reconstruction ++ ("builtins", "tuple"), ++ ("builtins", "set"), ++ ("builtins", "frozenset"), ++ ("builtins", "dict"), ++ } ++ ) ++ ++ def find_class(self, module: str, name: str) -> type: ++ if (module, name) not in self._ALLOWED_CLASSES: ++ raise pickle.UnpicklingError( ++ f"Forbidden class: {module}.{name}. " ++ "CookieJar.load() only allows cookie-related types for security. " ++ "See https://docs.python.org/3/library/pickle.html#restricting-globals" ++ ) ++ return super().find_class(module, name) # type: ignore[no-any-return] ++ ++ + class CookieJar(AbstractCookieJar): + """Implements cookie storage adhering to RFC 6265.""" + +@@ -104,14 +140,84 @@ class CookieJar(AbstractCookieJar): + self._expirations: Dict[Tuple[str, str, str], float] = {} + + def save(self, file_path: PathLike) -> None: ++ """Save cookies to a file using JSON format. ++ ++ :param file_path: Path to file where cookies will be serialized, ++ :class:`str` or :class:`pathlib.Path` instance. ++ """ + file_path = pathlib.Path(file_path) +- with file_path.open(mode="wb") as f: +- pickle.dump(self._cookies, f, pickle.HIGHEST_PROTOCOL) ++ data: dict[str, dict[str, dict[str, str | bool]]] = {} ++ for (domain, path), cookie in self._cookies.items(): ++ key = f"{domain}|{path}" ++ data[key] = {} ++ for name, morsel in cookie.items(): ++ morsel_data: dict[str, str | bool] = { ++ "key": morsel.key, ++ "value": morsel.value, ++ "coded_value": morsel.coded_value, ++ } ++ # Save all morsel attributes that have values ++ for attr in morsel._reserved: # type: ignore[attr-defined] ++ attr_val = morsel[attr] ++ if attr_val: ++ morsel_data[attr] = attr_val ++ data[key][name] = morsel_data ++ with file_path.open(mode="w", encoding="utf-8") as f: ++ json.dump(data, f, indent=2) + + def load(self, file_path: PathLike) -> None: ++ """Load cookies from a file. ++ ++ Tries to load JSON format first. Falls back to loading legacy ++ pickle format (using a restricted unpickler) for backward ++ compatibility with existing cookie files. ++ ++ :param file_path: Path to file from where cookies will be ++ imported, :class:`str` or :class:`pathlib.Path` instance. ++ """ + file_path = pathlib.Path(file_path) +- with file_path.open(mode="rb") as f: +- self._cookies = pickle.load(f) ++ # Try JSON format first ++ try: ++ with file_path.open(mode="r", encoding="utf-8") as f: ++ data = json.load(f) ++ self._cookies = self._load_json_data(data) ++ except (json.JSONDecodeError, UnicodeDecodeError, ValueError): ++ # Fall back to legacy pickle format with restricted unpickler ++ with file_path.open(mode="rb") as f: ++ self._cookies = _RestrictedCookieUnpickler(f).load() ++ ++ def _load_json_data( ++ self, data: dict[str, dict[str, dict[str, str | bool]]] ++ ) -> defaultdict[tuple[str, str], SimpleCookie]: ++ """Load cookies from parsed JSON data.""" ++ cookies: defaultdict[tuple[str, str], SimpleCookie] = defaultdict(SimpleCookie) ++ for compound_key, cookie_data in data.items(): ++ domain, path = compound_key.split("|", 1) ++ key = (domain, path) ++ for name, morsel_data in cookie_data.items(): ++ morsel: Morsel[str] = Morsel() ++ morsel_key = morsel_data["key"] ++ morsel_value = morsel_data["value"] ++ morsel_coded_value = morsel_data["coded_value"] ++ # Use __setstate__ to bypass validation, same pattern ++ # used in _build_morsel and _cookie_helpers. ++ morsel.__setstate__( # type: ignore[attr-defined] ++ { ++ "key": morsel_key, ++ "value": morsel_value, ++ "coded_value": morsel_coded_value, ++ } ++ ) ++ # Restore morsel attributes ++ for attr in morsel._reserved: # type: ignore[attr-defined] ++ if attr in morsel_data and attr not in ( ++ "key", ++ "value", ++ "coded_value", ++ ): ++ morsel[attr] = morsel_data[attr] ++ cookies[key][name] = morsel ++ return cookies + + def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None: + if predicate is None: +diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py +index 9c608959c..bdc5cfd72 100644 +--- a/tests/test_cookiejar.py ++++ b/tests/test_cookiejar.py +@@ -5,6 +5,7 @@ import pathlib + import pickle + import unittest + from http.cookies import BaseCookie, Morsel, SimpleCookie ++from pathlib import Path + from unittest import mock + + import pytest +@@ -864,3 +865,170 @@ async def test_treat_as_secure_origin() -> None: + assert len(jar) == 1 + filtered_cookies = jar.filter_cookies(request_url=endpoint) + assert len(filtered_cookies) == 1 ++ ++ ++# === Security tests for restricted unpickler and JSON save/load === ++ ++ ++async def test_load_rejects_malicious_pickle(tmp_path: Path) -> None: ++ """Verify CookieJar.load() blocks arbitrary code execution via pickle. ++ ++ A crafted pickle payload using os.system (or any non-cookie class) ++ must be rejected by the restricted unpickler. ++ """ ++ import os ++ ++ file_path = tmp_path / "malicious.pkl" ++ ++ class RCEPayload: ++ def __reduce__(self) -> tuple[object, ...]: ++ return (os.system, ("echo PWNED",)) ++ ++ with open(file_path, "wb") as f: ++ pickle.dump(RCEPayload(), f, pickle.HIGHEST_PROTOCOL) ++ ++ jar = CookieJar() ++ with pytest.raises(pickle.UnpicklingError, match="Forbidden class"): ++ jar.load(file_path) ++ ++ ++async def test_load_rejects_eval_payload(tmp_path: Path) -> None: ++ """Verify CookieJar.load() blocks eval-based pickle payloads.""" ++ file_path = tmp_path / "eval_payload.pkl" ++ ++ class EvalPayload: ++ def __reduce__(self) -> tuple[object, ...]: ++ return (eval, ("__import__('os').system('echo PWNED')",)) ++ ++ with open(file_path, "wb") as f: ++ pickle.dump(EvalPayload(), f, pickle.HIGHEST_PROTOCOL) ++ ++ jar = CookieJar() ++ with pytest.raises(pickle.UnpicklingError, match="Forbidden class"): ++ jar.load(file_path) ++ ++ ++async def test_load_rejects_subprocess_payload(tmp_path: Path) -> None: ++ """Verify CookieJar.load() blocks subprocess-based pickle payloads.""" ++ import subprocess ++ ++ file_path = tmp_path / "subprocess_payload.pkl" ++ ++ class SubprocessPayload: ++ def __reduce__(self) -> tuple[object, ...]: ++ return (subprocess.call, (["echo", "PWNED"],)) ++ ++ with open(file_path, "wb") as f: ++ pickle.dump(SubprocessPayload(), f, pickle.HIGHEST_PROTOCOL) ++ ++ jar = CookieJar() ++ with pytest.raises(pickle.UnpicklingError, match="Forbidden class"): ++ jar.load(file_path) ++ ++ ++async def test_load_falls_back_to_pickle( ++ tmp_path: Path, ++ cookies_to_receive: SimpleCookie, ++) -> None: ++ """Verify load() falls back to restricted pickle for legacy cookie files. ++ ++ Existing cookie files saved with older versions of aiohttp used pickle. ++ load() should detect that the file is not JSON and fall back to the ++ restricted pickle unpickler for backward compatibility. ++ """ ++ file_path = tmp_path / "legit.pkl" ++ ++ # Write a legacy pickle file directly (as old aiohttp save() would) ++ jar_save = CookieJar() ++ jar_save.update_cookies(cookies_to_receive) ++ with file_path.open(mode="wb") as f: ++ pickle.dump(jar_save._cookies, f, pickle.HIGHEST_PROTOCOL) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ jar_test = SimpleCookie() ++ for cookie in jar_load: ++ jar_test[cookie.key] = cookie ++ ++ assert jar_test == cookies_to_receive ++ ++ ++async def test_save_load_json_roundtrip( ++ tmp_path: Path, ++ cookies_to_receive: SimpleCookie, ++) -> None: ++ """Verify save/load roundtrip preserves cookies via JSON format.""" ++ file_path = tmp_path / "cookies.json" ++ ++ jar_save = CookieJar() ++ jar_save.update_cookies(cookies_to_receive) ++ jar_save.save(file_path=file_path) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ saved_cookies = SimpleCookie() ++ for cookie in jar_save: ++ saved_cookies[cookie.key] = cookie ++ ++ loaded_cookies = SimpleCookie() ++ for cookie in jar_load: ++ loaded_cookies[cookie.key] = cookie ++ ++ assert saved_cookies == loaded_cookies ++ ++ ++async def test_json_format_is_safe(tmp_path: Path) -> None: ++ """Verify the JSON file format cannot execute code on load.""" ++ import json ++ ++ file_path = tmp_path / "safe.json" ++ ++ # Write something that might look dangerous but is just data ++ malicious_data = { ++ "evil.com|/": { ++ "session": { ++ "key": "session", ++ "value": "__import__('os').system('echo PWNED')", ++ "coded_value": "__import__('os').system('echo PWNED')", ++ } ++ } ++ } ++ with open(file_path, "w") as f: ++ json.dump(malicious_data, f) ++ ++ jar = CookieJar() ++ jar.load(file_path=file_path) ++ ++ # The "malicious" string is just a cookie value, not executed code ++ cookies = list(jar) ++ assert len(cookies) == 1 ++ assert cookies[0].value == "__import__('os').system('echo PWNED')" ++ ++ ++async def test_save_load_json_secure_cookies(tmp_path: Path) -> None: ++ """Verify save/load preserves Secure and HttpOnly flags.""" ++ file_path = tmp_path / "secure.json" ++ ++ jar_save = CookieJar() ++ cookies = SimpleCookie( ++ "token=abc123; Secure; HttpOnly; Path=/; Domain=example.com" ++ ) ++ jar_save.update_cookies( ++ cookies, ++ URL("https://example.com/"), ++ ) ++ jar_save.save(file_path=file_path) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ loaded_cookies = list(jar_load) ++ assert len(loaded_cookies) == 1 ++ cookie = loaded_cookies[0] ++ assert cookie.key == "token" ++ assert cookie.value == "abc123" ++ assert cookie["secure"] is True ++ assert cookie["httponly"] is True ++ assert cookie["domain"] == "example.com" +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index e104b62e1e..245abe4b89 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -19,6 +19,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-22815.patch \ file://CVE-2026-34514.patch \ file://CVE-2026-34513.patch \ + file://CVE-2026-34993.patch \ " PYPI_PACKAGE = "aiohttp" From patchwork Mon Aug 31 04:57:44 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 96876 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id CD941C61DF0 for ; Mon, 31 Aug 2026 04:57:50 +0000 (UTC) Received: from alln-iport-3.cisco.com (alln-iport-3.cisco.com [173.37.142.90]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.22958.1788152269429499428 for ; Sun, 30 Aug 2026 21:57:49 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=dXH6CwhS; spf=pass (domain: cisco.com, ip: 173.37.142.90, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=3860; q=dns/txt; s=iport01; t=1788152269; x=1789361869; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=QrMr3RSojK9Y9MIyoQ+2Nsyu4puB+6Rczbnzw1RvvOA=; b=dXH6CwhSRwvsuy4oDxQk9X5uOaf/tgk0nymPN0Uccz2pmUu4FgsJEiWL CNQxXhPUCN7ot0NZiSghY07wr0I33C+9sSmEEg2pxISlQ9vhj1vKXn6m/ ngusKBRJHomnRjvE9Qtv9TXMzeelqXCXoq6jERph/S4UfBav2uGaXTK2s qCeZSF7WkI0q8sKckY4A61C5DvqEvoZ5Ev042w/RoFXWgz8QhwpgEPQCr jC+7WdJpKCOXIYUfWcBhtKT8FgRMoTwH0CxoOo70RDjgRWyz6Ir2c9h+l zwntKbhSypPGgNGBpsP5kmatDZ5pfRzobwjdExZcBe47RTnMu9NLc+4Qx A==; X-CSE-ConnectionGUID: bdKbC57sSeOthcBInwNWuQ== X-CSE-MsgGUID: 5HsgDoA0S5S0uY7oieHaYQ== X-IPAS-Result: A0BJAgCvCJVq/4wQJK1agRWBRIJXdF5DSZZKA54bgX4PAQEBD0QNBAEBhD9GAk8VjQ8CJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NhloBAgEDMgEYAS0QHAMBAi8rIwgZgwIBgnQDEcIDgiyBAYMoAT8CQ1DbMAELFAGBOIU/iCJdGAGEfCcbG4FyhH6BBUsBgRABAYEnhn4EgiKBDIFakh9IgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQ0bBgWBHYEohA8jGTZ6gQlegSspYAESF4EJgggCglqCBQIBSUMOB0dTCQQLGA1IESw3FRkEPm4HjnofgVhzgQ4BK4EgdDSlWqEPCiiDdowilToaM4VbpRELmH2OCoVVkCtQhGmBaDyBKBwDCwdwFYMiCUoZD444g2uBfzeDLsZVJzICCTIBAQcCBw4DC4FokACBfgEB IronPort-Data: A9a23:6plfOa4Oe12nVeFcZ7OQpgxRtGnGchMFZxGqfqrLsTDasY5as4F+v jNJXWyCOqyNa2ejKYpwbY+1pENQsZTcx4NjGwNqryw8Zn8b8sCt6fZ1gavT04J+CuWZESqLO u1HMoGowPgcFyGa+1H1dOex9RGQ7InQLpLkEunIJyttcgFtTSYlmHpLlvUw6mJSqYDR7zil5 5Wo/qUzBHf/g2QqajJPs/rawP9SlK2aVA0w7wRWic9j5Dcyp1FNZLoDKKe4KWfPQ4U8NoaSW +bZwbilyXjS9hErB8nNuu6TnpoiG+O60aCm0xK6aoD66vRwjnVaPpUTaJLwXXxqZwChxLid/ jniWauYEm/FNoWU8AgUvoIx/ytWZcWq85efSZSzXFD6I0DuKxPRL/tS4E4eN4E78M0qXnp1+ NsEKDsnTxacod2r+efuIgVsrpxLwMjDNYcbvDRkiDreF/tjGcuFSKTR7tge1zA17ixMNa+BP IxCN3w2MlKZP0Un1lQ/UPrSmM+khXT7ejxJoXqepLE85C7YywkZPL3FYIaMIYzVGZQF9qqej j/m5GvVBThCD4GkkSe0rXWti8/GrBquDer+E5X9rJaGmma7wXQeDhATX1a3rfS1z0KzRd9bA 0gV4TY1668q+UqmS9PwUxG1rDiDpBF0ZjZLO+Q+7AfIzu/f5ByUQzBfCDVAc9ch8sQxQFTGy 2O0oj8gPhQ32JX9dJ5X3u78Qe+aUcTNEVI/WA== IronPort-HdrOrdr: A9a23:9FK/gq+vsqvuDjAqk21uk+D6I+orL9Y04lQ7vn2ZhyY7TiX+rb HIoB11737JYVoqNU3I3OrwWpVoIkmskaKdn7NwAV7KZmCP0wGVxcNZnO7fKlbbdREWmNQw6U 4ZSdkcNPTASX5nkM39/A60V/wkwNWB7eSUoN229QYKcemvAJsQlzuQzW2gYzRLeDU= X-Talos-CUID: 9a23:HuCpy2uIkhGxHQSbytMWygg66Isvfl7X5yqPOHS1LnppQ5e0T0C2x75dxp8= X-Talos-MUID: 9a23:NjhSCQ0Z+2gHmirbbNJfKaFK+jUjvoS1B2wKuM095vLcKB0vPDaQrD2Ge9py X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.25,252,1779148800"; d="scan'208";a="841387498" Received: from alln-l-core-03.cisco.com ([173.36.16.140]) by alln-iport-3.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 31 Aug 2026 04:57:48 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by alln-l-core-03.cisco.com (Postfix) with ESMTPS id AE9491800042F; Mon, 31 Aug 2026 04:57:48 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id EBCC8CD03C3; Sun, 30 Aug 2026 21:57:47 -0700 (PDT) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][scarthgap][PATCH 9/9] python3-aiohttp: fix CVE-2026-34518 Date: Sun, 30 Aug 2026 21:57:44 -0700 Message-Id: <20260831045744.3321483-10-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260831045744.3321483-1-dkelaiya@cisco.com> References: <20260831045744.3321483-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: alln-l-core-03.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Mon, 31 Aug 2026 04:57:50 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/129607 From: Darsh Kelaiya This patch applies the reviewed upstream fix commits shown in [1] and [2]. The advisory identifying the fix is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/5351c980dcec7ad385730efdf4e1f4338b24fdb6 [2] https://github.com/aio-libs/aiohttp/commit/6e8f393330f9bd6d7b24a146124ebc42eaa727b9 [3] https://nvd.nist.gov/vuln/detail/CVE-2026-34518 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34518.patch | 64 +++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 65 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34518.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34518.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34518.patch new file mode 100644 index 0000000000..98417f13e9 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34518.patch @@ -0,0 +1,64 @@ +From 528611956c8ea77e8dddc312b64baa3549697bdf Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Fri, 27 Feb 2026 01:31:07 +0000 +Subject: [PATCH] Drop additional headers on redirect (#12146) (#12150) + +CVE: CVE-2026-34518 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/5351c980dcec7ad385730efdf4e1f4338b24fdb6] + +(cherry picked from commit 6e8f393330f9bd6d7b24a146124ebc42eaa727b9) + +(cherry picked from commit 5351c980dcec7ad385730efdf4e1f4338b24fdb6) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/client.py | 2 ++ + tests/test_client_functional.py | 9 ++++++++- + 2 files changed, 10 insertions(+), 1 deletion(-) + +diff --git a/aiohttp/client.py b/aiohttp/client.py +index e30b880e9..0b87d7eec 100644 +--- a/aiohttp/client.py ++++ b/aiohttp/client.py +@@ -684,6 +684,8 @@ class ClientSession: + if url.origin() != parsed_url.origin(): + auth = None + headers.pop(hdrs.AUTHORIZATION, None) ++ headers.pop(hdrs.COOKIE, None) ++ headers.pop(hdrs.PROXY_AUTHORIZATION, None) + + url = parsed_url + params = {} +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index c8d4e0aab..3ca1716ba 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -2604,6 +2604,8 @@ async def test_drop_auth_on_redirect_to_other_host( + async def srv_to(request): + assert request.host == url_to.host + assert "Authorization" not in request.headers, "Header wasn't dropped" ++ assert "Proxy-Authorization" not in request.headers ++ assert "Cookie" not in request.headers + return web.Response() + + server_from = await create_server_for_url_and_handler(url_from, srv_from) +@@ -2646,11 +2648,16 @@ async def test_drop_auth_on_redirect_to_other_host( + resp = await client.get( + url_from, + auth=aiohttp.BasicAuth("user", "pass"), ++ headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz", "Cookie": "a=b"}, + ) + assert resp.status == 200 + resp = await client.get( + url_from, +- headers={"Authorization": "Basic dXNlcjpwYXNz"}, ++ headers={ ++ "Authorization": "Basic dXNlcjpwYXNz", ++ "Proxy-Authorization": "Basic dXNlcjpwYXNz", ++ "Cookie": "a=b", ++ }, + ) + assert resp.status == 200 + +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb index 245abe4b89..3e4513b811 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.9.5.bb @@ -20,6 +20,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34514.patch \ file://CVE-2026-34513.patch \ file://CVE-2026-34993.patch \ + file://CVE-2026-34518.patch \ " PYPI_PACKAGE = "aiohttp"