From patchwork Mon Sep 28 17:43:11 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: 99496 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 5BA9ACA5FA9 for ; Mon, 28 Sep 2026 17:43:41 +0000 (UTC) Received: from rcdn-iport-2.cisco.com (rcdn-iport-2.cisco.com [173.37.86.73]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63547.1790617412808715790 for ; Mon, 28 Sep 2026 10:43:33 -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=iikwWiDt; spf=pass (domain: cisco.com, ip: 173.37.86.73, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=27722; q=dns/txt; s=iport01; t=1790617412; x=1791827012; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=MTwyhpBoJ1jMxdxbaQ00Qpnflg2ZdyqOZZF27rnf8w8=; b=iikwWiDt7eS3wJMtygk0aUBD0nENXGnb1UFmvSj8JOIhxsmquW83cl21 Ip1uwn7bOQQ2Qx59lZLnRMNjXhJsinKi2slSf4ZEgie728Ey6ol29iEIt IEdyIWYV+sVM2Z3cCVbGD9PpgOcRU415+Igsx//WPkNoMQzOq33AAXH2z OT37ZYq+rXr2txkTa7ut+b997LJCg014DnH7cLo5YhlSCi3QHgAdgWbZF fgpJh6AUz4tJmdUqehfBsB0gUqc6SauEyUPqVen+7UficbMjiRyFnHaUp bl+U6mxKR7OFvl9AGcMFMWcyVTrGlNniNhEiGpgbys0gb0Dr4EiTnXtGI Q==; X-CSE-ConnectionGUID: Ke73kV/QTYuHOL0WiV3YNA== X-CSE-MsgGUID: NhSnme1QRMiDnn9XscOR9Q== X-IPAS-Result: A0AaAABOprpq/5X/Ja1aHAEBAQEBAQcBARIBAQQEAQGBfAcBAQsBglZ1YENJjHKJWAOeGoF+DwEBAQ9EDQQBAYQ/RgKOCQImNAkOAQIEAwIDAQEBAQEBAQEBAQEBCgEBBQEBAQIBBwWBDhOGTw2QEgECAQMaDQsBGAEbEhAcAwECLysjCBmDAgGCdAMRwleBeTOBAYMpAT8CQ1DbMgELFAGBOAGFP4gjXRgBRIQ4JxsbgXKBFYNpgQWBXAEBiCUEgiKBDIFak0NIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQcbBgWBHYEgghkjGTZ6gQlegSspYAEQF4EHggcCglSCAQIBSUMOB0VTCSVBChJHJiIIEgkBExowC4EhOEEJKBEYDUgRLDcVGQQ+bgeQLh6CZQGBDQEqAX8ZQnCTGgeQFoIhoQ8KKIN2jCKVOhozqm8LmH2OCpZQhGmBaDyBRwsHcBWDIglKGQ+OKw4Lg2CBf8o6JzICCTIBAQcCBw4DC4FokAItgU8BAQ IronPort-Data: A9a23:k+xXoakzgiLbkMCj2438jHDo5gzRJ0RdPkR7XQ2eYbSJt1+Wr1Gzt xIdWj/VPPzcNGH2e9Fzb9i38UsCuZfXxoJiTgpqqSs8HltH+JHPbTi7wugcHM8zwunrFh8PA xA2M4GYRCwMZiaC4Errav6+/SEUOZigHtLUEPTDNj16WThqQSIgjQMLs+Mii+aEu/Dha++2k Y20+ZK31GONgWYubDpFsfLb8XuDgdyr0N8mlg1mDRx0lAe2e0k9VPo3Oay3Jn3kdYhYdsbSb /rD1ryw4lTC9B4rDN6/+p6jGqHdauePVeQmoiM+t5mK2nCulARrukoIHKZ0hXNsttm8t4sZJ OOhGnCHYVxB0qXkwIzxWvTDes10FfUuFLTveRBTvSEPpqHLWyOE/hlgMK05Fa4Vpb10PyZ3y c0FCBcpNi+CubmH5pvuH4GAhux7RCXqFJkUtnclyXTSCuwrBMmZBa7L/tRfmjw3g6iiH96HO JFfMmUpNkmdJUQTaz/7C7pm9AusrnX8fjlRqUOcjaE2+GPUigd21dABNfKII4DRHJQKwBnwS mTu2WTCBBQXbeWl7Dup9HT9tvTEuhmqYddHfFG/3rsw6LGJ/UQUEBAQWF6xrPW1h0L7V99BJ kg8/is1sbN081SmSNT4VRC0rHOI+BkGVLJt//YS8gqBzO/Qpg2eHGVBF2QHY909v8hwTjsvv rOUo+7U6fVUmOX9YRqgGn289Fte5QB9wbc+WBI5 IronPort-HdrOrdr: A9a23:qQviRa5X0ZnDXgoyLwPXwOrXdLJyesId70hD6qm+c3Nom6uj5q eTdZsgtCMc5Ax9ZJhko6HjBEDiewK5yXcK2+ks1N6ZNWGM0ldAbrsSiLcKqAePJ8SRzIJgPN 9bAstDIey1K0RmhsDn5wT9OdMhzN6btJ2Mv47lvhFQpcUAUdAZ0++/YTzra3FLeA== X-Talos-CUID: 9a23:HtHCTmM7SmHuyu5DeyQ4r00qXfkZQ2D961OIHE6GUldPYejA X-Talos-MUID: 9a23:tlTbsAYz1j9jC+BTkT/XgjRvbf5SxqWCDkZcoY4NnPPVDHkl X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="514319767" Received: from rcdn-l-core-12.cisco.com ([173.37.255.149]) by rcdn-iport-2.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:31 +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 rcdn-l-core-12.cisco.com (Postfix) with ESMTPS id 7E87C180001D4; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 23F3ACBF201; Mon, 28 Sep 2026 10:43:31 -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 01/13] python3-aiohttp: fix CVE-2026-34519 Date: Mon, 28 Sep 2026 10:43:11 -0700 Message-Id: <20260928174323.1810308-2-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:41 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130448 From: Darsh Kelaiya This patch applies the upstream stable-branch fix in [1], which backports the original upstream commit in [2]. The advisory identifying the fix is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/53b35a2f8869c37a133e60bf1a82a1c01642ba2b [2] https://github.com/aio-libs/aiohttp/commit/18510482de080bf741c560bf2a190fdc54b4eed4 [3] https://nvd.nist.gov/vuln/detail/CVE-2026-34519 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34519.patch | 638 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 639 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34519.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34519.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34519.patch new file mode 100644 index 0000000000..b4d2a39352 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34519.patch @@ -0,0 +1,638 @@ +From 0eb0a867fe5a9071b885c60ada894dc11da5f858 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sat, 7 Mar 2026 19:00:02 +0000 +Subject: [PATCH] Restrict reason (#12209) (#12212) + +CVE: CVE-2026-34519 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/53b35a2f8869c37a133e60bf1a82a1c01642ba2b] + +Backport Changes: +- Replaced the upstream _write_str_raise_on_nlcr() call with the + existing _safe_header(status_line) implementation because + aiohttp 3.9.5 does not provide _write_str_raise_on_nlcr(). +- Added CR/LF validation in web_response.py because aiohttp 3.9.5 + lacks the upstream LF-only reason check, while retaining its existing + HTTPStatus phrase lookup and set_status implementation. +- Adapted the upstream regression tests to the aiohttp 3.9.5 imports + and test signatures, and added coverage for status-line + serialization. +- Regenerated _http_writer.c with Cython 3.0.5 because the accelerated + build uses the checked-in generated source. + +(cherry picked from commit 18510482de080bf741c560bf2a190fdc54b4eed4) + +--------- + +Co-authored-by: Dhiral Vyas +(cherry picked from commit 53b35a2f8869c37a133e60bf1a82a1c01642ba2b) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/_http_writer.c | 147 ++++++++++++++++++++++++++----------------------- + aiohttp/_http_writer.pyx | 1 + + aiohttp/http_writer.py | 1 + + aiohttp/web_exceptions.py | 2 ++ + aiohttp/web_response.py | 2 ++ + tests/test_web_exceptions.py | 15 +++++++++++++++ + tests/test_web_response.py | 26 ++++++++++++++++++++++++++ + 7 files changed, 130 insertions(+), 70 deletions(-) + +diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx +index eff852195..24e5bb752 100644 +--- a/aiohttp/_http_writer.pyx ++++ b/aiohttp/_http_writer.pyx +@@ -131,6 +131,7 @@ def _serialize_headers(str status_line, headers): + _safe_header(to_str(key)) + _safe_header(to_str(val)) + ++ _safe_header(status_line) + try: + if _write_str(&writer, status_line) < 0: + raise +diff --git a/aiohttp/_http_writer.c b/aiohttp/_http_writer.c +index 74bc210ea..7eecc2940 100644 +--- a/aiohttp/_http_writer.c ++++ b/aiohttp/_http_writer.c +@@ -3852,7 +3852,7 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + * _safe_header(to_str(key)) + * _safe_header(to_str(val)) # <<<<<<<<<<<<<< + * +- * try: ++ * _safe_header(status_line) + */ + __pyx_t_6 = __pyx_f_7aiohttp_12_http_writer_to_str(__pyx_v_val); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 132, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); +@@ -3864,34 +3864,43 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + /* "aiohttp/_http_writer.pyx":134 + * _safe_header(to_str(val)) + * ++ * _safe_header(status_line) # <<<<<<<<<<<<<< ++ * try: ++ * if _write_str(&writer, status_line) < 0: ++ */ ++ __pyx_f_7aiohttp_12_http_writer__safe_header(__pyx_v_status_line); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 134, __pyx_L1_error) ++ ++ /* "aiohttp/_http_writer.pyx":135 ++ * ++ * _safe_header(status_line) + * try: # <<<<<<<<<<<<<< + * if _write_str(&writer, status_line) < 0: + * raise + */ + /*try:*/ { + +- /* "aiohttp/_http_writer.pyx":135 +- * ++ /* "aiohttp/_http_writer.pyx":136 ++ * _safe_header(status_line) + * try: + * if _write_str(&writer, status_line) < 0: # <<<<<<<<<<<<<< + * raise + * if _write_byte(&writer, b'\r') < 0: + */ +- __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_str((&__pyx_v_writer), __pyx_v_status_line); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 135, __pyx_L6_error) ++ __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_str((&__pyx_v_writer), __pyx_v_status_line); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 136, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_4 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":136 ++ /* "aiohttp/_http_writer.pyx":137 + * try: + * if _write_str(&writer, status_line) < 0: + * raise # <<<<<<<<<<<<<< + * if _write_byte(&writer, b'\r') < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 136, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 137, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":135 +- * ++ /* "aiohttp/_http_writer.pyx":136 ++ * _safe_header(status_line) + * try: + * if _write_str(&writer, status_line) < 0: # <<<<<<<<<<<<<< + * raise +@@ -3899,27 +3908,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":137 ++ /* "aiohttp/_http_writer.pyx":138 + * if _write_str(&writer, status_line) < 0: + * raise + * if _write_byte(&writer, b'\r') < 0: # <<<<<<<<<<<<<< + * raise + * if _write_byte(&writer, b'\n') < 0: + */ +- __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\r'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 137, __pyx_L6_error) ++ __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\r'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 138, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_4 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":138 ++ /* "aiohttp/_http_writer.pyx":139 + * raise + * if _write_byte(&writer, b'\r') < 0: + * raise # <<<<<<<<<<<<<< + * if _write_byte(&writer, b'\n') < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 138, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 139, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":137 ++ /* "aiohttp/_http_writer.pyx":138 + * if _write_str(&writer, status_line) < 0: + * raise + * if _write_byte(&writer, b'\r') < 0: # <<<<<<<<<<<<<< +@@ -3928,27 +3937,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":139 ++ /* "aiohttp/_http_writer.pyx":140 + * if _write_byte(&writer, b'\r') < 0: + * raise + * if _write_byte(&writer, b'\n') < 0: # <<<<<<<<<<<<<< + * raise + * + */ +- __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\n'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 139, __pyx_L6_error) ++ __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\n'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 140, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_4 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":140 ++ /* "aiohttp/_http_writer.pyx":141 + * raise + * if _write_byte(&writer, b'\n') < 0: + * raise # <<<<<<<<<<<<<< + * + * for key, val in headers.items(): + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 140, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 141, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":139 ++ /* "aiohttp/_http_writer.pyx":140 + * if _write_byte(&writer, b'\r') < 0: + * raise + * if _write_byte(&writer, b'\n') < 0: # <<<<<<<<<<<<<< +@@ -3957,7 +3966,7 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":142 ++ /* "aiohttp/_http_writer.pyx":143 + * raise + * + * for key, val in headers.items(): # <<<<<<<<<<<<<< +@@ -3967,9 +3976,9 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + __pyx_t_3 = 0; + if (unlikely(__pyx_v_headers == Py_None)) { + PyErr_Format(PyExc_AttributeError, "'NoneType' object has no attribute '%.30s'", "items"); +- __PYX_ERR(0, 142, __pyx_L6_error) ++ __PYX_ERR(0, 143, __pyx_L6_error) + } +- __pyx_t_6 = __Pyx_dict_iterator(__pyx_v_headers, 0, __pyx_n_s_items, (&__pyx_t_2), (&__pyx_t_4)); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 142, __pyx_L6_error) ++ __pyx_t_6 = __Pyx_dict_iterator(__pyx_v_headers, 0, __pyx_n_s_items, (&__pyx_t_2), (&__pyx_t_4)); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 143, __pyx_L6_error) + __Pyx_GOTREF(__pyx_t_6); + __Pyx_XDECREF(__pyx_t_1); + __pyx_t_1 = __pyx_t_6; +@@ -3977,7 +3986,7 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + while (1) { + __pyx_t_7 = __Pyx_dict_iter_next(__pyx_t_1, __pyx_t_2, &__pyx_t_3, &__pyx_t_6, &__pyx_t_5, NULL, __pyx_t_4); + if (unlikely(__pyx_t_7 == 0)) break; +- if (unlikely(__pyx_t_7 == -1)) __PYX_ERR(0, 142, __pyx_L6_error) ++ if (unlikely(__pyx_t_7 == -1)) __PYX_ERR(0, 143, __pyx_L6_error) + __Pyx_GOTREF(__pyx_t_6); + __Pyx_GOTREF(__pyx_t_5); + __Pyx_XDECREF_SET(__pyx_v_key, __pyx_t_6); +@@ -3985,30 +3994,30 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + __Pyx_XDECREF_SET(__pyx_v_val, __pyx_t_5); + __pyx_t_5 = 0; + +- /* "aiohttp/_http_writer.pyx":143 ++ /* "aiohttp/_http_writer.pyx":144 + * + * for key, val in headers.items(): + * if _write_str(&writer, to_str(key)) < 0: # <<<<<<<<<<<<<< + * raise + * if _write_byte(&writer, b':') < 0: + */ +- __pyx_t_5 = __pyx_f_7aiohttp_12_http_writer_to_str(__pyx_v_key); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 143, __pyx_L6_error) ++ __pyx_t_5 = __pyx_f_7aiohttp_12_http_writer_to_str(__pyx_v_key); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 144, __pyx_L6_error) + __Pyx_GOTREF(__pyx_t_5); +- __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_str((&__pyx_v_writer), ((PyObject*)__pyx_t_5)); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 143, __pyx_L6_error) ++ __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_str((&__pyx_v_writer), ((PyObject*)__pyx_t_5)); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 144, __pyx_L6_error) + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + __pyx_t_8 = (__pyx_t_7 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":144 ++ /* "aiohttp/_http_writer.pyx":145 + * for key, val in headers.items(): + * if _write_str(&writer, to_str(key)) < 0: + * raise # <<<<<<<<<<<<<< + * if _write_byte(&writer, b':') < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 144, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 145, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":143 ++ /* "aiohttp/_http_writer.pyx":144 + * + * for key, val in headers.items(): + * if _write_str(&writer, to_str(key)) < 0: # <<<<<<<<<<<<<< +@@ -4017,27 +4026,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":145 ++ /* "aiohttp/_http_writer.pyx":146 + * if _write_str(&writer, to_str(key)) < 0: + * raise + * if _write_byte(&writer, b':') < 0: # <<<<<<<<<<<<<< + * raise + * if _write_byte(&writer, b' ') < 0: + */ +- __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), ':'); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 145, __pyx_L6_error) ++ __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), ':'); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 146, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_7 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":146 ++ /* "aiohttp/_http_writer.pyx":147 + * raise + * if _write_byte(&writer, b':') < 0: + * raise # <<<<<<<<<<<<<< + * if _write_byte(&writer, b' ') < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 146, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 147, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":145 ++ /* "aiohttp/_http_writer.pyx":146 + * if _write_str(&writer, to_str(key)) < 0: + * raise + * if _write_byte(&writer, b':') < 0: # <<<<<<<<<<<<<< +@@ -4046,27 +4055,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":147 ++ /* "aiohttp/_http_writer.pyx":148 + * if _write_byte(&writer, b':') < 0: + * raise + * if _write_byte(&writer, b' ') < 0: # <<<<<<<<<<<<<< + * raise + * if _write_str(&writer, to_str(val)) < 0: + */ +- __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), ' '); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 147, __pyx_L6_error) ++ __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), ' '); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 148, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_7 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":148 ++ /* "aiohttp/_http_writer.pyx":149 + * raise + * if _write_byte(&writer, b' ') < 0: + * raise # <<<<<<<<<<<<<< + * if _write_str(&writer, to_str(val)) < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 148, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 149, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":147 ++ /* "aiohttp/_http_writer.pyx":148 + * if _write_byte(&writer, b':') < 0: + * raise + * if _write_byte(&writer, b' ') < 0: # <<<<<<<<<<<<<< +@@ -4075,30 +4084,30 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":149 ++ /* "aiohttp/_http_writer.pyx":150 + * if _write_byte(&writer, b' ') < 0: + * raise + * if _write_str(&writer, to_str(val)) < 0: # <<<<<<<<<<<<<< + * raise + * if _write_byte(&writer, b'\r') < 0: + */ +- __pyx_t_5 = __pyx_f_7aiohttp_12_http_writer_to_str(__pyx_v_val); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 149, __pyx_L6_error) ++ __pyx_t_5 = __pyx_f_7aiohttp_12_http_writer_to_str(__pyx_v_val); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 150, __pyx_L6_error) + __Pyx_GOTREF(__pyx_t_5); +- __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_str((&__pyx_v_writer), ((PyObject*)__pyx_t_5)); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 149, __pyx_L6_error) ++ __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_str((&__pyx_v_writer), ((PyObject*)__pyx_t_5)); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 150, __pyx_L6_error) + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + __pyx_t_8 = (__pyx_t_7 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":150 ++ /* "aiohttp/_http_writer.pyx":151 + * raise + * if _write_str(&writer, to_str(val)) < 0: + * raise # <<<<<<<<<<<<<< + * if _write_byte(&writer, b'\r') < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 150, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 151, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":149 ++ /* "aiohttp/_http_writer.pyx":150 + * if _write_byte(&writer, b' ') < 0: + * raise + * if _write_str(&writer, to_str(val)) < 0: # <<<<<<<<<<<<<< +@@ -4107,27 +4116,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":151 ++ /* "aiohttp/_http_writer.pyx":152 + * if _write_str(&writer, to_str(val)) < 0: + * raise + * if _write_byte(&writer, b'\r') < 0: # <<<<<<<<<<<<<< + * raise + * if _write_byte(&writer, b'\n') < 0: + */ +- __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\r'); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 151, __pyx_L6_error) ++ __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\r'); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 152, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_7 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":152 ++ /* "aiohttp/_http_writer.pyx":153 + * raise + * if _write_byte(&writer, b'\r') < 0: + * raise # <<<<<<<<<<<<<< + * if _write_byte(&writer, b'\n') < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 152, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 153, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":151 ++ /* "aiohttp/_http_writer.pyx":152 + * if _write_str(&writer, to_str(val)) < 0: + * raise + * if _write_byte(&writer, b'\r') < 0: # <<<<<<<<<<<<<< +@@ -4136,27 +4145,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":153 ++ /* "aiohttp/_http_writer.pyx":154 + * if _write_byte(&writer, b'\r') < 0: + * raise + * if _write_byte(&writer, b'\n') < 0: # <<<<<<<<<<<<<< + * raise + * + */ +- __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\n'); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 153, __pyx_L6_error) ++ __pyx_t_7 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\n'); if (unlikely(__pyx_t_7 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 154, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_7 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":154 ++ /* "aiohttp/_http_writer.pyx":155 + * raise + * if _write_byte(&writer, b'\n') < 0: + * raise # <<<<<<<<<<<<<< + * + * if _write_byte(&writer, b'\r') < 0: + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 154, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 155, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":153 ++ /* "aiohttp/_http_writer.pyx":154 + * if _write_byte(&writer, b'\r') < 0: + * raise + * if _write_byte(&writer, b'\n') < 0: # <<<<<<<<<<<<<< +@@ -4167,27 +4176,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + } + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + +- /* "aiohttp/_http_writer.pyx":156 ++ /* "aiohttp/_http_writer.pyx":157 + * raise + * + * if _write_byte(&writer, b'\r') < 0: # <<<<<<<<<<<<<< + * raise + * if _write_byte(&writer, b'\n') < 0: + */ +- __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\r'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 156, __pyx_L6_error) ++ __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\r'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 157, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_4 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":157 ++ /* "aiohttp/_http_writer.pyx":158 + * + * if _write_byte(&writer, b'\r') < 0: + * raise # <<<<<<<<<<<<<< + * if _write_byte(&writer, b'\n') < 0: + * raise + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 157, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 158, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":156 ++ /* "aiohttp/_http_writer.pyx":157 + * raise + * + * if _write_byte(&writer, b'\r') < 0: # <<<<<<<<<<<<<< +@@ -4196,27 +4205,27 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":158 ++ /* "aiohttp/_http_writer.pyx":159 + * if _write_byte(&writer, b'\r') < 0: + * raise + * if _write_byte(&writer, b'\n') < 0: # <<<<<<<<<<<<<< + * raise + * + */ +- __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\n'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 158, __pyx_L6_error) ++ __pyx_t_4 = __pyx_f_7aiohttp_12_http_writer__write_byte((&__pyx_v_writer), '\n'); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 159, __pyx_L6_error) + __pyx_t_8 = (__pyx_t_4 < 0); + if (unlikely(__pyx_t_8)) { + +- /* "aiohttp/_http_writer.pyx":159 ++ /* "aiohttp/_http_writer.pyx":160 + * raise + * if _write_byte(&writer, b'\n') < 0: + * raise # <<<<<<<<<<<<<< + * + * return PyBytes_FromStringAndSize(writer.buf, writer.pos) + */ +- __Pyx_ReraiseException(); __PYX_ERR(0, 159, __pyx_L6_error) ++ __Pyx_ReraiseException(); __PYX_ERR(0, 160, __pyx_L6_error) + +- /* "aiohttp/_http_writer.pyx":158 ++ /* "aiohttp/_http_writer.pyx":159 + * if _write_byte(&writer, b'\r') < 0: + * raise + * if _write_byte(&writer, b'\n') < 0: # <<<<<<<<<<<<<< +@@ -4225,7 +4234,7 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + */ + } + +- /* "aiohttp/_http_writer.pyx":161 ++ /* "aiohttp/_http_writer.pyx":162 + * raise + * + * return PyBytes_FromStringAndSize(writer.buf, writer.pos) # <<<<<<<<<<<<<< +@@ -4233,14 +4242,14 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + * _release_writer(&writer) + */ + __Pyx_XDECREF(__pyx_r); +- __pyx_t_1 = PyBytes_FromStringAndSize(__pyx_v_writer.buf, __pyx_v_writer.pos); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 161, __pyx_L6_error) ++ __pyx_t_1 = PyBytes_FromStringAndSize(__pyx_v_writer.buf, __pyx_v_writer.pos); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 162, __pyx_L6_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_r = __pyx_t_1; + __pyx_t_1 = 0; + goto __pyx_L5_return; + } + +- /* "aiohttp/_http_writer.pyx":163 ++ /* "aiohttp/_http_writer.pyx":164 + * return PyBytes_FromStringAndSize(writer.buf, writer.pos) + * finally: + * _release_writer(&writer) # <<<<<<<<<<<<<< +@@ -4264,7 +4273,7 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + __Pyx_XGOTREF(__pyx_t_15); + __pyx_t_4 = __pyx_lineno; __pyx_t_7 = __pyx_clineno; __pyx_t_9 = __pyx_filename; + { +- __pyx_f_7aiohttp_12_http_writer__release_writer((&__pyx_v_writer)); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 163, __pyx_L22_error) ++ __pyx_f_7aiohttp_12_http_writer__release_writer((&__pyx_v_writer)); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 164, __pyx_L22_error) + } + if (PY_MAJOR_VERSION >= 3) { + __Pyx_XGIVEREF(__pyx_t_13); +@@ -4295,7 +4304,7 @@ static PyObject *__pyx_pf_7aiohttp_12_http_writer__serialize_headers(CYTHON_UNUS + __pyx_L5_return: { + __pyx_t_15 = __pyx_r; + __pyx_r = 0; +- __pyx_f_7aiohttp_12_http_writer__release_writer((&__pyx_v_writer)); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 163, __pyx_L1_error) ++ __pyx_f_7aiohttp_12_http_writer__release_writer((&__pyx_v_writer)); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 164, __pyx_L1_error) + __pyx_r = __pyx_t_15; + __pyx_t_15 = 0; + goto __pyx_L0; +diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py +index d6b02e6f5..37cdc06e6 100644 +--- a/aiohttp/http_writer.py ++++ b/aiohttp/http_writer.py +@@ -181,6 +181,7 @@ def _safe_header(string: str) -> str: + + + def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes: ++ _safe_header(status_line) + headers_gen = (_safe_header(k) + ": " + _safe_header(v) for k, v in headers.items()) + line = status_line + "\r\n" + "\r\n".join(headers_gen) + "\r\n\r\n" + return line.encode("utf-8") +diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py +index ee2c1e72d..30792c281 100644 +--- a/aiohttp/web_exceptions.py ++++ b/aiohttp/web_exceptions.py +@@ -101,6 +101,8 @@ class HTTPException(Response, Exception): + "body argument is deprecated for http web exceptions", + DeprecationWarning, + ) ++ if reason is not None and ("\r" in reason or "\n" in reason): ++ raise ValueError("Reason cannot contain \\r or \\n") + Response.__init__( + self, + status=self.status_code, +diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py +index 40d6f01ec..3bd9d45b5 100644 +--- a/aiohttp/web_response.py ++++ b/aiohttp/web_response.py +@@ -140,6 +140,8 @@ class StreamResponse(BaseClass, HeadersMixin): + reason = HTTPStatus(self._status).phrase + except ValueError: + reason = "" ++ elif "\r" in reason or "\n" in reason: ++ raise ValueError("Reason cannot contain \\r or \\n") + self._reason = reason + + @property +diff --git a/tests/test_web_exceptions.py b/tests/test_web_exceptions.py +index 69deb27a0..5a98d0f94 100644 +--- a/tests/test_web_exceptions.py ++++ b/tests/test_web_exceptions.py +@@ -270,3 +270,18 @@ def test_unicode_text_body_unauthorized() -> None: + ): + resp = web.HTTPUnauthorized(body="text") + assert resp.status == 401 ++ ++ ++def test_multiline_reason() -> None: ++ with pytest.raises(ValueError, match=r"Reason cannot contain"): ++ web.HTTPOk(reason="Bad\r\nInjected-header: foo") ++ ++ ++def test_reason_with_cr() -> None: ++ with pytest.raises(ValueError, match=r"Reason cannot contain"): ++ web.HTTPOk(reason="OK\rSet-Cookie: evil=1") ++ ++ ++def test_reason_with_lf() -> None: ++ with pytest.raises(ValueError, match=r"Reason cannot contain"): ++ web.HTTPOk(reason="OK\nSet-Cookie: evil=1") +diff --git a/tests/test_web_response.py b/tests/test_web_response.py +index d1b407c09..84018bbfb 100644 +--- a/tests/test_web_response.py ++++ b/tests/test_web_response.py +@@ -916,6 +916,27 @@ def test_set_status_with_reason() -> None: + assert "Everything is fine!" == resp.reason + + ++def test_set_status_reason_with_cr() -> None: ++ resp = StreamResponse() ++ ++ with pytest.raises(ValueError, match="Reason cannot contain"): ++ resp.set_status(200, "OK\rSet-Cookie: evil=1") ++ ++ ++def test_set_status_reason_with_lf() -> None: ++ resp = StreamResponse() ++ ++ with pytest.raises(ValueError, match="Reason cannot contain"): ++ resp.set_status(200, "OK\nSet-Cookie: evil=1") ++ ++ ++def test_set_status_reason_with_crlf() -> None: ++ resp = StreamResponse() ++ ++ with pytest.raises(ValueError, match="Reason cannot contain"): ++ resp.set_status(200, "OK\r\nSet-Cookie: evil=1") ++ ++ + async def test_start_force_close() -> None: + req = make_request("GET", "/") + resp = StreamResponse() +@@ -1168,6 +1189,16 @@ async def test_render_with_body(buf, writer) -> None: + ) + + ++async def test_multiline_reason(buf, writer) -> None: ++ with pytest.raises(ValueError, match=r"Reason cannot contain \\r or \\n"): ++ Response(reason="Bad\r\nInjected-header: foo") ++ ++ ++def test_serialize_headers_rejects_crlf_status_line() -> None: ++ with pytest.raises(ValueError, match="Newline or carriage return"): ++ _serialize_headers("HTTP/1.1 200 OK\r\nInjected: yes", CIMultiDict()) ++ ++ + async def test_send_set_cookie_header(buf, writer) -> None: + resp = Response() + resp.cookies["name"] = "value" +-- +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 7b77b19dcc..400a4a838b 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 @@ -21,6 +21,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34513.patch \ file://CVE-2026-34993.patch \ file://CVE-2026-34518.patch \ + file://CVE-2026-34519.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:12 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: 99493 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 3B234CA5FA3 for ; Mon, 28 Sep 2026 17:43:41 +0000 (UTC) Received: from rcdn-iport-3.cisco.com (rcdn-iport-3.cisco.com [173.37.86.74]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.64574.1790617412700567676 for ; Mon, 28 Sep 2026 10:43:33 -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=lmGkHVJW; spf=pass (domain: cisco.com, ip: 173.37.86.74, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=13857; q=dns/txt; s=iport01; t=1790617412; x=1791827012; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=HYhHWTAiX5nDEPjWj75rUN2L1JalIYzw5gkle5o9wXk=; b=lmGkHVJWwAVKTZH4Yw+L7qEEbwFNVPp+DhmIH41tIw8Ft3JhGjJJDNsL nt6232QLcB7xp9qxIU54heIouuh+eDG076TBsoxS+9qhZCCFatWM5w/QG usUjbK9DaECSnofJwbp+EKtHGZgOaS8y3MbiiQOFY+GXHCHUFRdfplUsa XqNlPNUEq13DCdrd8XtobbzqIWQ4mhoFfCUtvI7z7OJ5hFbM2O634QIXV reVA4rOJGBSeVq8JoSJkg7RheC05/XiW4+HvZMPk9OKBduoXixwkTNC1b /sYejYEVIzmBog0BxwPRz+yl2Tt0MQDzR5C6OzmLcmTijC/N1rutPvEGe A==; X-CSE-ConnectionGUID: l/B61G9uRc+EaIppuiauRA== X-CSE-MsgGUID: KOKq/MW8RN2zstKQY5dmCA== X-IPAS-Result: A0BIAgBOprpq/5X/Ja1aHgEBCxIMggULgld1YENJlkoDnhqBfg8BAQEPRA0EAQGEP0YCjgkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDJwsBGAEtEBwDAQIvKyMIGYMCAYJ0AxHCV4F5M4EBgykBPwJDUNsyAQsUAYE4hUCII10YAYR8JxsbgXKBFYNpgQWBXAEBiCUEgiKBDIFagQOSQEiBHgNZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPheBBxsGBYEdgSCCGSMZNnqBCV6BKylgARAXgQeCBwKCVIIBAgFJQw4HRVMJJUEKEkcmIggSCQETGjALgSE4QQkoERgNSBEsNxUZBD5uB5AuHoJkAYEOAQogAYEYCBQZpDSCIaEPCiiDdowilToaM4QEgVeSQJJUC5h9glmLMZVzXYRpgWg8gUcLB3AVO4JnCUoZD44rDguDYIF/g2XGVScyAgkyAQEHAgcOAwuBaJF+AQE IronPort-Data: A9a23:stAeGqqtbCmW1E/ERaJLtZRWI5leBmJIZBIvgKrLsJaIsI4StFCzt garIBmAMvqOMWP3KI8gPYmx8B4A6MWGztM2QVA+rXtgQilB9+PIVI+TRqvS04x+DSFioGZPt Zh2hgzodZhsJpPkjk7zdOWn9T8jhfngqoPUUIbsIjp2SRJvVBAvgBdin/9RqoNziLBVOSvV0 T/Ji5OZYgDNNwJcaDpOtfrS8kM35ZwehRtB1rAATaET1LPhvyF94KI3fcmZM3b+S49IKe+2L 86r5K255G7Q4yA2AdqjlLvhGmVSKlIFFVHT4pb+c/HKbilq/kTe4I5iXBYvQRs/ZwGyojxE4 I4lWapc5useFvakdOw1C3G0GszlVEFM0OevzXOX6aR/w6BaGpfh660GMa04AWEX0uIwKz8X2 q0FEjtOQxmin/qrwbumcMA506zPLOGzVG8ekmtrwTecCbMtRorOBvyQo9RZxzw3wMtJGJ4yZ eJANmEpN0uGOUASfA5LWfrSn8/w7pX7WzFbpVacpLs+y2PS1wd2lrPqNbI5f/TXH5QNxxrJ+ j6uE2LRITY5LZu69mq/1Ci+nfPspBKmVIc5PejtnhJtqBjJroAJMzURTVa9rPyzh0KyVt4aJ 0EK9y4Gqakp6FftScHwWRC9qnOIshMQHd1KHIUHBBql0KHY5UOdQ2MDVDMEMYZgv84tTjts3 ViM9z/0OQFSXHSuYSr13t+pQfmaYED58Udqifc4cDY4 IronPort-HdrOrdr: A9a23:XZ36cqHLAxxtiL/ZpLqEyseALOsnbusQ8zAXPo5KJiC9Ffbo8P xG88576faZslsssTQb6LK90cq7MBfhHOBOgbX5VI3KNGKNhILrFvAG0WKI+VPd8kPFmtK1rZ 0QEJRWOZnXEUVwi9r87U2TFtYtx8TCzYWT7N2uqUuEiWpRGtldB8ATMHfjLnFL X-Talos-CUID: 9a23:g/u34GEFt38Ag1SOqmJjs2RKQ+90b0fWj1rZAlegFGdgVYe8HAo= X-Talos-MUID: 9a23:NAgaiA1fNINs0ZgBXIwSIaI86jUjwIONEQcorsU8nZOWLjBTAgbAgDGca9py X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="529014944" Received: from rcdn-l-core-12.cisco.com ([173.37.255.149]) by rcdn-iport-3.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:31 +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 rcdn-l-core-12.cisco.com (Postfix) with ESMTPS id 805E618000248; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 28EEBCBF202; Mon, 28 Sep 2026 10:43:31 -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 02/13] python3-aiohttp: fix CVE-2026-34516 Date: Mon, 28 Sep 2026 10:43:12 -0700 Message-Id: <20260928174323.1810308-3-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:41 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130446 From: Darsh Kelaiya This patch applies the upstream stable-branch fix in [1], which backports the original upstream commit in [2]. The advisory identifying the fix is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/8a74257b3804c9aac0bf644af93070f68f6c5a6f [2] https://github.com/aio-libs/aiohttp/commit/5fe9dfb64400a574f5ba3f2d3b49b7db47567c29 [3] https://nvd.nist.gov/vuln/detail/CVE-2026-34516 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34516.patch | 356 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 357 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch new file mode 100644 index 0000000000..47956fe975 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch @@ -0,0 +1,356 @@ +From b119720e4e9c19b23f8589dae05b9a39b26995e1 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Tue, 10 Mar 2026 20:36:38 +0000 +Subject: [PATCH] Restrict multipart header sizes (#12208) (#12228) + +CVE: CVE-2026-34516 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/8a74257b3804c9aac0bf644af93070f68f6c5a6f] + +Backport Changes: +- Retained aiohttp 3.9.5's Optional/Union annotations in + MultipartReader because this version supports Python 3.8 and cannot + use the upstream PEP 604 union syntax. +- Omitted the test_read_boundary_across_chunks() readline() signature + update because that test is not present in aiohttp 3.9.5; retained + the applicable Stream test-helper update. +- Applied the multipart header trimming change using rstrip(b"\r\n") + for compatibility with the aiohttp 3.9.5 codebase. + +(cherry picked from commit 5fe9dfb64400a574f5ba3f2d3b49b7db47567c29) +(cherry picked from commit 8a74257b3804c9aac0bf644af93070f68f6c5a6f) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/multipart.py | 27 +++++++++++++++++++--- + aiohttp/streams.py | 16 +++++++----- + aiohttp/test_utils.py | 3 +++ + aiohttp/web_protocol.py | 7 ++++++ + aiohttp/web_request.py | 7 +++++- + tests/test_multipart.py | 3 ++- + tests/test_streams.py | 9 ++++--- + tests/test_web_request.py | 53 ++++++++++++++++++++++++++++++++++++++- + 8 files changed, 106 insertions(+), 17 deletions(-) + +diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py +index 9e5ff9b41..d7b993218 100644 +--- a/aiohttp/multipart.py ++++ b/aiohttp/multipart.py +@@ -37,6 +37,7 @@ from .hdrs import ( + ) + from .helpers import CHAR, TOKEN, parse_mimetype, reify + from .http import HeadersParser ++from .http_exceptions import BadHttpMessage + from .payload import ( + JsonPayload, + LookupError, +@@ -547,7 +548,14 @@ class MultipartReader: + #: Body part reader class for non multipart/* content types. + part_reader_cls = BodyPartReader + +- def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None: ++ def __init__( ++ self, ++ headers: Mapping[str, str], ++ content: StreamReader, ++ *, ++ max_field_size: int = 8190, ++ max_headers: int = 128, ++ ) -> None: + self._mimetype = parse_mimetype(headers[CONTENT_TYPE]) + assert self._mimetype.type == "multipart", "multipart/* content type expected" + if "boundary" not in self._mimetype.parameters: +@@ -560,6 +568,8 @@ class MultipartReader: + self._content = content + self._default_charset: Optional[str] = None + self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None ++ self._max_field_size = max_field_size ++ self._max_headers = max_headers + self._at_eof = False + self._at_bof = True + self._unread: List[bytes] = [] +@@ -661,7 +671,12 @@ class MultipartReader: + if mimetype.type == "multipart": + if self.multipart_reader_cls is None: + return type(self)(headers, self._content) +- return self.multipart_reader_cls(headers, self._content) ++ return self.multipart_reader_cls( ++ headers, ++ self._content, ++ max_field_size=self._max_field_size, ++ max_headers=self._max_headers, ++ ) + else: + return self.part_reader_cls( + self._boundary, +@@ -723,12 +738,14 @@ class MultipartReader: + async def _read_headers(self) -> "CIMultiDictProxy[str]": + lines = [] + while True: +- chunk = await self._content.readline() ++ chunk = await self._content.readline(max_line_length=self._max_field_size) +- chunk = chunk.strip() ++ chunk = chunk.rstrip(b"\r\n") + lines.append(chunk) + if not chunk: + break +- parser = HeadersParser() ++ if len(lines) > self._max_headers: ++ raise BadHttpMessage("Too many headers received") ++ parser = HeadersParser(max_field_size=self._max_field_size) + headers, raw_headers = parser.parse_headers(lines) + return headers + +diff --git a/aiohttp/streams.py b/aiohttp/streams.py +index 121528842..dffaf374b 100644 +--- a/aiohttp/streams.py ++++ b/aiohttp/streams.py +@@ -21,6 +21,7 @@ from .helpers import ( + set_exception, + set_result, + ) ++from .http_exceptions import LineTooLong + from .log import internal_logger + + __all__ = ( +@@ -327,10 +328,12 @@ class StreamReader(AsyncStreamReaderMixin): + finally: + self._waiter = None + +- async def readline(self) -> bytes: +- return await self.readuntil() ++ async def readline(self, *, max_line_length: Optional[int] = None) -> bytes: ++ return await self.readuntil(max_size=max_line_length) + +- async def readuntil(self, separator: bytes = b"\n") -> bytes: ++ async def readuntil( ++ self, separator: bytes = b"\n", *, max_size: Optional[int] = None ++ ) -> bytes: + seplen = len(separator) + if seplen == 0: + raise ValueError("Separator should be at least one-byte string") +@@ -341,6 +344,7 @@ class StreamReader(AsyncStreamReaderMixin): + chunk = b"" + chunk_size = 0 + not_enough = True ++ max_size = max_size or self._high_water + + while not_enough: + while self._buffer and not_enough: +@@ -355,8 +359,8 @@ class StreamReader(AsyncStreamReaderMixin): + if ichar: + not_enough = False + +- if chunk_size > self._high_water: +- raise ValueError("Chunk too big") ++ if chunk_size > max_size: ++ raise LineTooLong(chunk[:100] + b"...", max_size) + + if self._eof: + break +@@ -572,7 +576,7 @@ class EmptyStreamReader(StreamReader): # lgtm [py/missing-call-to-init] + def feed_data(self, data: bytes, n: int = 0) -> None: + pass + +- async def readline(self) -> bytes: ++ async def readline(self, *, max_line_length: Optional[int] = None) -> bytes: + return b"" + + async def read(self, n: int = -1) -> bytes: +diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py +index a36e85996..a12bb0505 100644 +--- a/aiohttp/test_utils.py ++++ b/aiohttp/test_utils.py +@@ -638,6 +638,9 @@ def make_mocked_request( + + if protocol is sentinel: + protocol = mock.Mock() ++ protocol.max_field_size = 8190 ++ protocol.max_line_length = 8190 ++ protocol.max_headers = 128 + protocol.transport = transport + + if writer is sentinel: +diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py +index a06503e0c..a73bb4364 100644 +--- a/aiohttp/web_protocol.py ++++ b/aiohttp/web_protocol.py +@@ -136,6 +136,9 @@ class RequestHandler(BaseProtocol): + KEEPALIVE_RESCHEDULE_DELAY = 1 + + __slots__ = ( ++ "max_field_size", ++ "max_headers", ++ "max_line_size", + "_request_count", + "_keepalive", + "_manager", +@@ -193,6 +196,10 @@ class RequestHandler(BaseProtocol): + self._request_handler: Optional[_RequestHandler] = manager.request_handler + self._request_factory: Optional[_RequestFactory] = manager.request_factory + ++ self.max_line_size = max_line_size ++ self.max_headers = max_headers ++ self.max_field_size = max_field_size ++ + self._tcp_keepalive = tcp_keepalive + # placeholder to be replaced on keepalive timeout setup + self._keepalive_time = 0.0 +diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py +index cd77b7bde..2ec09565c 100644 +--- a/aiohttp/web_request.py ++++ b/aiohttp/web_request.py +@@ -687,7 +687,12 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + + async def multipart(self) -> MultipartReader: + """Return async iterator to process BODY as multipart.""" +- return MultipartReader(self._headers, self._payload) ++ return MultipartReader( ++ self._headers, ++ self._payload, ++ max_field_size=self._protocol.max_field_size, ++ max_headers=self._protocol.max_headers, ++ ) + + async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]": + """Return POST parameters.""" +diff --git a/tests/test_multipart.py b/tests/test_multipart.py +index e4a2be1f3..3bc9efd71 100644 +--- a/tests/test_multipart.py ++++ b/tests/test_multipart.py +@@ -3,6 +3,7 @@ import io + import json + import pathlib + import zlib ++from typing import Optional + from unittest import mock + + import pytest +@@ -63,7 +64,7 @@ class Stream: + def at_eof(self): + return self.content.tell() == len(self.content.getbuffer()) + +- async def readline(self): ++ async def readline(self, *, max_line_length: Optional[int] = None) -> bytes: + return self.content.readline() + + def unread_data(self, data): +diff --git a/tests/test_streams.py b/tests/test_streams.py +index ed65b567a..2076bc9ba 100644 +--- a/tests/test_streams.py ++++ b/tests/test_streams.py +@@ -12,6 +12,7 @@ import pytest + from re_assert import Matches + + from aiohttp import streams ++from aiohttp.http_exceptions import LineTooLong + + DATA = b"line1\nline2\nline3\n" + +@@ -325,7 +326,7 @@ class TestStreamReader: + stream.feed_data(b"li") + stream.feed_data(b"ne1\nline2\n") + +- with pytest.raises(ValueError): ++ with pytest.raises(LineTooLong): + await stream.readline() + # The buffer should contain the remaining data after exception + stream.feed_eof() +@@ -346,7 +347,7 @@ class TestStreamReader: + + loop.call_soon(cb) + +- with pytest.raises(ValueError): ++ with pytest.raises(LineTooLong): + await stream.readline() + data = await stream.read() + assert b"chunk3\n" == data +@@ -436,7 +437,7 @@ class TestStreamReader: + stream.feed_data(b"li") + stream.feed_data(b"ne1" + separator + b"line2" + separator) + +- with pytest.raises(ValueError): ++ with pytest.raises(LineTooLong): + await stream.readuntil(separator) + # The buffer should contain the remaining data after exception + stream.feed_eof() +@@ -458,7 +459,7 @@ class TestStreamReader: + + loop.call_soon(cb) + +- with pytest.raises(ValueError, match="Chunk too big"): ++ with pytest.raises(LineTooLong): + await stream.readuntil(separator) + data = await stream.read() + assert b"chunk3#" == data +diff --git a/tests/test_web_request.py b/tests/test_web_request.py +index 962092999..c1d61f895 100644 +--- a/tests/test_web_request.py ++++ b/tests/test_web_request.py +@@ -11,6 +11,7 @@ from yarl import URL + + from aiohttp import HttpVersion + from aiohttp.base_protocol import BaseProtocol ++from aiohttp.http_exceptions import BadHttpMessage, LineTooLong + from aiohttp.http_parser import RawRequestMessage + from aiohttp.streams import StreamReader + from aiohttp.test_utils import make_mocked_request +@@ -676,7 +677,57 @@ async def test_multipart_formdata_file(protocol: BaseProtocol) -> None: + result["a_file"].file.close() + + +-async def test_make_too_big_request_limit_None(protocol) -> None: ++async def test_multipart_formdata_headers_too_many(protocol: BaseProtocol) -> None: ++ many = b"".join(f"X-{i}: a\r\n".encode() for i in range(130)) ++ body = ( ++ b"--b\r\n" ++ b'Content-Disposition: form-data; name="a"\r\n' + many + b"\r\n1\r\n" ++ b"--b--\r\n" ++ ) ++ content_type = "multipart/form-data; boundary=b" ++ payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) ++ payload.feed_data(body) ++ payload.feed_eof() ++ req = make_mocked_request( ++ "POST", ++ "/", ++ headers={"CONTENT-TYPE": content_type}, ++ payload=payload, ++ ) ++ ++ with pytest.raises(BadHttpMessage, match="Too many headers received"): ++ await req.post() ++ ++ ++async def test_multipart_formdata_header_too_long(protocol: BaseProtocol) -> None: ++ k = b"t" * 4100 ++ body = ( ++ b"--b\r\n" ++ b'Content-Disposition: form-data; name="a"\r\n' ++ + k ++ + b":" ++ + k ++ + b"\r\n" ++ + b"\r\n1\r\n" ++ b"--b--\r\n" ++ ) ++ content_type = "multipart/form-data; boundary=b" ++ payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) ++ payload.feed_data(body) ++ payload.feed_eof() ++ req = make_mocked_request( ++ "POST", ++ "/", ++ headers={"CONTENT-TYPE": content_type}, ++ payload=payload, ++ ) ++ ++ match = "400, message:\n Got more than 8190 bytes when reading" ++ with pytest.raises(LineTooLong, match=match): ++ await req.post() ++ ++ ++async def test_make_too_big_request_limit_None(protocol: BaseProtocol) -> None: + payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) + large_file = 1024**2 * b"x" + too_large_file = large_file + b"x" +-- +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 400a4a838b..d7c7a5014a 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 @@ -22,6 +22,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34993.patch \ file://CVE-2026-34518.patch \ file://CVE-2026-34519.patch \ + file://CVE-2026-34516.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:13 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: 99497 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 4DA6CCA5FAD for ; Mon, 28 Sep 2026 17:43:42 +0000 (UTC) Received: from rcdn-iport-7.cisco.com (rcdn-iport-7.cisco.com [173.37.86.78]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63551.1790617414307827365 for ; Mon, 28 Sep 2026 10:43:34 -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=IWhQ8/IU; spf=pass (domain: cisco.com, ip: 173.37.86.78, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=3842; q=dns/txt; s=iport01; t=1790617414; x=1791827014; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=3OtC1uqMDPlv7sfhKLheWMaTnjKivARoEd9XMAR+nqc=; b=IWhQ8/IUglaKXdBGPkCYOIcfAj8/BXnEzAiLM26uUQh8+YtqhFUBnJzh UDf+dGjH2LUYWD+APOwEwMuIpDH2Mfo0Kkt6RKi2DvXaSLwytWx7DL3uG LBAU1T+Fzyl0vqF6KMl84gRrsRBBNgiTG2BKEyjhYfvcBEqhbGNCbYir9 Hb2yAeKm6xKASF67EUm+9eSN5K8ZGW9xjSzbbnFrLmIzjftDNvPoe2Dan 91cN4g+1YCynJFtjr0jf156ijn323vaMHq6jpmWoViRXYj9SVs41nQlKQ /WyRhUDagnx+hlhM5/1VamcD1udUpaBAGCMqUPY9ZHjK7bRCu6WZ5LF62 A==; X-CSE-ConnectionGUID: K9M+lodJQr+yzM7fPPLzhg== X-CSE-MsgGUID: AIq3dWXBSQSSPjZ1IGUavA== X-IPAS-Result: A0BHAgC9prpq/47/Ja1aHgEBCxIMggULgld1YENJlkoDnhqBfg8BAQEPRA0EAQGEP0YCjgkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDMgEYAS0QHAMBAi8rIwgZgwIBgnQDEcJFgiyBAYMpAT8CQ1DbMgELFAGBOIVAiCNdGAGEfCcbG4FyhH6BBYFcAQGIJQSCIoEMgVqTQ0iBHgNZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPheBBxsGBYEdgSCCGSMZNnqBCV6BKylgARAXgQeCBwKCVIIBAgFJQw4HRVMJJUEKEkcmIggSCQETGjALgSE4QQkoERgNSBEsNxUZBD5uB5AuHoJlAYENAQohf4EVKQ2TNJADgiGhDwoog3aMIpU6GjOEBIFXkkCSVAuYfY4KllCEaYFoPIEoGwQLB3AVgyIJShkPjjmDa4F/yjonMgIJMgEBBwIHDgMLgWiRfgEB IronPort-Data: A9a23:dtbcd6CpAhxLzRVW/3niw5YqxClBgxIJ4kV8jS/XYbTApDol02QFn TZLWjuFbqrcZjCjKI1xa9u1pk8Ou5HdzIM2OVdlrnsFo1CmBibm6XV1Cm+qYkt+++WaFBoPA /02M4eGcYZsCCCM/n9BC5C5xVFkz6aEW7HgP+DNPyF1VGdMRTwo4f5Zs7ZRbrVA357jXGthh fuo+5eBYA7/i2YtWo4pw/vrRC1H7ayaVAww5jTSVdgT1HfCmn8cCo4oJK3ZBxPQXolOE+emc P3Ixbe/83mx109F5gSNy+uTnuUiG9Y+DCDW4pZkc/HKbitq+kTe5p0G2M80Mi+7vdkmc+dZk 72hvbToIesg0zaldO41C3G0GAkmVUFKFSOuzXWX6aSuI0P6n3TE4KRLT0UxMa8h4KVmE0QR+ 8QgDCINYUXW7w626OrTpuhEnM8vKozveYgYoHwllWyfBvc9SpeFSKLPjTNa9G5v3YYVQrCEO pdfMGY1BPjDS0Un1lM/BJEzmO6pl3DXeDxDo1XTrq0yi4TW5FErj+m3a4OPIbRmQ+18v1/Dq l321V/AOSoiNu7G6BW4+2yF07qncSTTHdh6+KeD3vlyjVuew2YeBBEbWR63rOe0jma6WslDM AoT4icooK04+UCnQ9W7WAe3yENopTYGUNZWVul/4waXx++MvkCSB3MPSXhKb9lOWNIKeAHGH 2Shx7vBbQGDepXEIZ5B3t94dQ+PBBU= IronPort-HdrOrdr: A9a23:TlnUQKBn1T6TKAblHemO55DYdb4zR+YMi2TDGXofdfUzSL3+qy nAppUmPHPP5Qr5HUtQ++xoW5PwJU80i6QU3WB5B97LN2PbUSmTXeRfBODZrQEIdReTygck79 YCT4FOTPvtEFN9kcH2pCO8E9om3Z271ZrAv5a585+oJjsaE52JKGxCe3+mLnE= X-Talos-CUID: 9a23:F99CL25W1sgpH05gEtss1x8xWcQifl/k1jTBMWrnB15RbK+VRgrF X-Talos-MUID: 9a23:cYni+QkGSJG2pdUybHgcdnpLFd4z75ueIns0qq4+ufvdFANNMim02WE= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="527923123" Received: from rcdn-l-core-05.cisco.com ([173.37.255.142]) by rcdn-iport-7.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:31 +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 rcdn-l-core-05.cisco.com (Postfix) with ESMTPS id 85AA318000357; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 2EAD1CBF203; Mon, 28 Sep 2026 10:43:31 -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 03/13] python3-aiohttp: fix CVE-2026-34517 Date: Mon, 28 Sep 2026 10:43:13 -0700 Message-Id: <20260928174323.1810308-4-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:42 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130454 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/cbb774f38330563422ca0c413a71021d7b944145 [2] https://nvd.nist.gov/vuln/detail/CVE-2026-34517 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34517.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-34517.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34517.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34517.patch new file mode 100644 index 0000000000..577df52b0a --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34517.patch @@ -0,0 +1,65 @@ +From 70f4c611ec466b6a33f1bce57d5776fac964919f Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Tue, 10 Mar 2026 22:14:02 +0000 +Subject: [PATCH] [PR #12216/9cc4b917 backport][3.13] Check multipart max_size + during iteration (#12229) + +**This is a backport of PR #12216 as merged into master +(9cc4b917c54833a22f65edae7963d16a6eeb1f54).** + +--------- + +CVE: CVE-2026-34517 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/cbb774f38330563422ca0c413a71021d7b944145] + +Backport Changes: +- Replaced BodyPartReader.decode_iter() with aiohttp 3.9.5's + synchronous decode() API. Form-data does not support content + compression, so this preserves upstream's single-decode behavior + after bounded chunk reads. + +Co-authored-by: Sam Bull +(cherry picked from commit cbb774f38330563422ca0c413a71021d7b944145) +Signed-off-by: Darsh Kelaiya +--- + aiohttp/web_request.py | 19 +++++++++++++------ + 1 file changed, 13 insertions(+), 6 deletions(-) + +diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py +index 2ec09565c..f9de59573 100644 +--- a/aiohttp/web_request.py ++++ b/aiohttp/web_request.py +@@ -760,17 +760,24 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + out.add(field.name, ff) + else: + # deal with ordinary data +- value = await field.read(decode=True) ++ raw_data = bytearray() ++ while chunk := await field.read_chunk(): ++ size += len(chunk) ++ if 0 < max_size < size: ++ raise HTTPRequestEntityTooLarge( ++ max_size=max_size, actual_size=size ++ ) ++ raw_data.extend(chunk) ++ ++ # aiohttp 3.9.5 has synchronous BodyPartReader.decode() ++ # and form-data does not support content compression. ++ value = field.decode(raw_data) ++ + if field_ct is None or field_ct.startswith("text/"): + charset = field.get_charset(default="utf-8") + out.add(field.name, value.decode(charset)) + else: + out.add(field.name, value) +- size += len(value) +- if 0 < max_size < size: +- raise HTTPRequestEntityTooLarge( +- max_size=max_size, actual_size=size +- ) + else: + raise ValueError( + "To decode nested multipart you need " "to use custom reader", +-- +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 d7c7a5014a..fecf871d9f 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 @@ -23,6 +23,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34518.patch \ file://CVE-2026-34519.patch \ file://CVE-2026-34516.patch \ + file://CVE-2026-34517.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:14 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: 99492 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 3B1F2CA5FA1 for ; Mon, 28 Sep 2026 17:43:41 +0000 (UTC) Received: from rcdn-iport-8.cisco.com (rcdn-iport-8.cisco.com [173.37.86.79]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63553.1790617415702148386 for ; Mon, 28 Sep 2026 10:43:36 -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=ZlRjhAGG; spf=pass (domain: cisco.com, ip: 173.37.86.79, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=6972; q=dns/txt; s=iport01; t=1790617416; x=1791827016; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=IvgBmqVbDjGS+4nZ47XCiI5773wfZMJQolizsBkXylY=; b=ZlRjhAGG+5fk6Al+XIUsEuKU9U5X+xrQekNFkXPs5RHj5D8FUQGO8LD0 LzjBa/zfSH5KsN7gJBaYYpW+bJigmjTJjZxK/EPFvalsjAJnx48CnJeKB P0Q72U5/N2f2fMtIpqg7qBJDda0Nw+z5skzeKYi+KRHNcNUpWSclnqEeN jUl1oeI3y73YX+1BRhuAy2nTcDHkZ7Eb8wR5DeqWqd6kpjBqQiVgjTcPP xyPb5Dr1oLOW93N195e9ZuSYFVKiaemrq3ZJFMBAjRZfyXDF38Ft97u1P MqWICfgPvDxoKRg+BJEii8veyhbML4hVNdjMhSJjWmKdpUIJ6EUdb2Ae3 w==; X-CSE-ConnectionGUID: oflYzhynTWq1x/rNBYBDGA== X-CSE-MsgGUID: 1qbt077jQQmR/FmzKbOYFg== X-IPAS-Result: A0AaAAC9prpq/47/Ja1aHAEBAQEBAQcBARIBAQQEAQGBfAcBAQsBglZ1YENJhFeIG4lYA4tkkjaBfg8BAQEPRA0EAQGEP0YCjgkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDIwQLARgBLRAcAwECAwImAgIgCyMIGYMCAYI6AzcDEcJFen8zgQGDKQE/AkNQ2EsNgloBCxQBgQouAYU/gn8gAYUDXRgBhHwnGxuBcoQIdoEFgRpCAQECgSWEFIJqBIIigQyBWh6TJUiBAhwDWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XL1gbBgWBHYEgghkjGTZ6gQlegSspYAEQF4EHggcCglSCAQIBSUMOB0VTCSVBChJHJiIIEgkBExowC4EhOEEJKBEYDUgRLDcVGQQ9AW4HkC4egmUBgQ0BKQEBgWwoNJNUj2WCIYE1nmlxCiiDdowijz6FfBozqm8LmH2OCoQJkXdQhGmBaDyBRwsHcBWDIglKGQ+OKw4Lg2CBf4MUxyYnMgIBCDIBAQcCBw4DC4FokAACJgeBTwEB IronPort-Data: A9a23:6iFWL61Bl/do0cxnlfbD5YBwkn2cJEfYwER7XKvMYLTBsI5bpzNRn DROWGmPPvuKZGOnft52aoSxpk1QusLQxoNqSgo53Hw8FHgiRegpqji6wuYcGwvIc6UvmWo+t 512huHodZ5yEzmE4Ej9atANlFEkvYmQXL3wFeXYDS54QA5gWU8JhAlq8wIDqtYAbeORXUXX5 rsen+WFYAX7g28uYjpNg06+gEoHUMra6WtwUmMWPZinjHeG/1EJAZQWI72GLneQauF8Au6gS u/f+6qy92Xf8g1FIovNfmHTKxBirhb6ZGBiu1IOM0SQqkEqSh8ajs7XAMEhhXJ/0F1lqTzeJ OJl7vRcQS9xVkHFdX90vxNwS0mSNoUekFPLzOTWXcG7lyX7n3XQL/pGC08yDJ8Y6+BLPngX5 bsCDho8RDysrrfjqF67YrEEasULNsLnOsYb/3pn1zycVatgSpHYSKKM7thdtNsyrpkRRrCFO IxDNGcpNUiaC/FMEg9/5JYWkOSlgnD+YjRwo1OOrq1x6G/WpOB0+OW3aIaPIILQFK25mG7Bv 2japzumACgjMebY5z+X0XiLt8HAyHaTtIU6UefQGuRRqFqLy2oeDRcbWVe2rby1h1CzX/pbK lcI4WwptaU0+UmhQ9XxUhH+p2SL1iPwQPJKGOE8rQXIwa3O7kPBWy4PTyVKb5ots8peqSEW6 2JlVujBXVRH2IB5g1rEqt94cRva1fApEFI/ IronPort-HdrOrdr: A9a23:06yLZayiICN3wVywS8tRKrPw9L1zdoMgy1knxilNoNJuHfBw8P re+sjztCWE7wr5N0tApTntAsS9qDbnhPxICOoqTNOftXfd2FdARbsKheCJ/9SjIVyaygc378 ldmsZFZOEYdWIK7vrS0U2fD8sqxsWB/eSDgOfTyGoocCRRApsQljuQzm2gYzZLrM4sP+tAKK ah X-Talos-CUID: 9a23:L1W8Qmwl2AzAdaFIs1Y4BgUTFMQUYH3MykvvOhaoTnxPYoy/ZXC5rfY= X-Talos-MUID: 9a23:PHF8LQnn/DA8Sz0qnipydno7Lc10u/vxCnk0lK9BlcyWFiZXChmS2WE= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="520422629" Received: from rcdn-l-core-05.cisco.com ([173.37.255.142]) by rcdn-iport-8.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:31 +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 rcdn-l-core-05.cisco.com (Postfix) with ESMTPS id 8815518000377; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 340F5CBF204; Mon, 28 Sep 2026 10:43:31 -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 04/13] python3-aiohttp: fix CVE-2026-34520 Date: Mon, 28 Sep 2026 10:43:14 -0700 Message-Id: <20260928174323.1810308-5-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:41 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130459 From: Darsh Kelaiya This patch applies the reviewed upstream fix shown in [1]. The CVE record is referenced in [2]. It also backports the C-parser null-byte validation and adapts its regression test from [3]. [1] https://github.com/aio-libs/aiohttp/commit/9370b9714a7a56003cacd31a9b4ae16eab109ba4 [2] https://nvd.nist.gov/vuln/detail/CVE-2026-34520 [3] https://github.com/aio-libs/aiohttp/commit/db560cfeab32aa35f3d06f7a24238bedc01ffe2b Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34520.patch | 121 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 122 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34520.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34520.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34520.patch new file mode 100644 index 0000000000..2492a23a3d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34520.patch @@ -0,0 +1,121 @@ +From 661f91935d061a3d7d554f2c808c8a14f3122991 Mon Sep 17 00:00:00 2001 +From: Rodrigo Nogueira +Date: Wed, 11 Mar 2026 20:38:17 -0300 +Subject: [PATCH] [PR #12231/7043bc56 backport][3.13] Adjust header value + character checks to RFC 9110 (#12235) + +CVE: CVE-2026-34520 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/9370b9714a7a56003cacd31a9b4ae16eab109ba4] + +Backport Changes: +- Included the C-parser null-byte validation from upstream prerequisite + commit db560cfeab32aa35f3d06f7a24238bedc01ffe2b because the Scarthgap + 3.9.5 recipe patch stack does not contain that prerequisite. +- Used the target's local `raw_value` bytes object when raising + `InvalidHeader`, since aiohttp 3.9.5 converts + `_raw_value` before checking. +- Added a response-parser regression test for the C-parser validation. +- Omitted the generated aiohttp/_http_parser.c changes because the + Scarthgap recipe regenerates this file from _http_parser.pyx with + Cython. + +Co-authored-by: rodrigo.nogueira +(cherry picked from commit 9370b9714a7a56003cacd31a9b4ae16eab109ba4) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12231.bugfix.rst | 2 ++ + aiohttp/_http_parser.pyx | 7 +++++++ + aiohttp/http_parser.py | 9 ++++++++- + tests/test_http_parser.py | 17 +++++++++++++++++ + 4 files changed, 34 insertions(+), 1 deletion(-) + create mode 100644 CHANGES/12231.bugfix.rst + +diff --git a/CHANGES/12231.bugfix.rst b/CHANGES/12231.bugfix.rst +new file mode 100644 +index 000000000..cd74bd1e7 +--- /dev/null ++++ b/CHANGES/12231.bugfix.rst +@@ -0,0 +1,2 @@ ++Adjusted pure-Python request header value validation to align with RFC 9110 control-character handling, while preserving lax response parser behavior, and added regression tests for Host/header control-character cases. ++-- by :user:`rodrigobnogueira`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index cc8b13cfb..496498d6f 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -387,5 +387,12 @@ cdef class HttpParser: + value = raw_value.decode('utf-8', 'surrogateescape') + ++ # reject null bytes in header values - matches the Python parser ++ # check at http_parser.py. llhttp in lenient mode doesn't reject ++ # these itself, so we need to catch them here. ++ # ref: RFC 9110 section 5.5 (CTL chars forbidden in field values) ++ if "\x00" in value: ++ raise InvalidHeader(raw_value) ++ + self._headers.add(name, value) + if len(self._headers) > self._max_headers: + raise BadHttpMessage("Too many headers received") +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index 95ac28252..a7fec7f01 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -73,6 +73,10 @@ ASCIISET: Final[Set[str]] = set(string.printable) + # token = 1*tchar + _TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~") + TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+") ++# https://www.rfc-editor.org/rfc/rfc9110#section-5.5-5 ++_FIELD_VALUE_FORBIDDEN_CTL_RE: Final[Pattern[str]] = re.compile( ++ r"[\x00-\x08\x0a-\x1f\x7f]" ++) + VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII) + DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII) + HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+") +@@ -200,7 +204,10 @@ class HeadersParser: + value = bvalue.decode("utf-8", "surrogateescape") + + # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 +- if "\n" in value or "\r" in value or "\x00" in value: ++ if self._lax: ++ if "\n" in value or "\r" in value or "\x00" in value: ++ raise InvalidHeader(bvalue) ++ elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value): + raise InvalidHeader(bvalue) + + headers.add(name, value) +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index cb562f598..be7446e0a 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -210,6 +210,9 @@ def test_bad_header_name(parser: Any, rfc9110_5_6_2_token_delim: str) -> None: + "Foo : bar", # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 + "Foo\t: bar", + "\xffoo: bar", ++ "Foo: abc\x01def", # CTL bytes forbidden per RFC 9110 §5.5 ++ "Foo: abc\x7fdef", # DEL is also a CTL byte ++ "Foo: abc\x1fdef", + ), + ) + def test_bad_headers(parser: Any, hdr: str) -> None: +@@ -218,6 +221,20 @@ def test_bad_headers(parser: Any, hdr: str) -> None: + parser.feed_data(text) + + ++def test_ctl_host_header_bad_characters(parser: HttpRequestParser) -> None: ++ """CTL byte in Host header must be rejected.""" ++ text = b"GET /test HTTP/1.1\r\nHost: trusted.example\x01@bad.test\r\n\r\n" ++ with pytest.raises(http_exceptions.BadHttpMessage): ++ parser.feed_data(text) ++ ++ ++def test_null_byte_in_response_header_value( ++ response: HttpResponseParser, ++) -> None: ++ with pytest.raises(http_exceptions.InvalidHeader): ++ response.feed_data(b"HTTP/1.1 200 OK\r\nFoo: abc\x00def\r\n\r\n") ++ ++ + def test_unpaired_surrogate_in_header_py(loop: Any, protocol: Any) -> None: + parser = HttpRequestParserPy( + protocol, +-- +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 fecf871d9f..b7741d5ed4 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 @@ -24,6 +24,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34519.patch \ file://CVE-2026-34516.patch \ file://CVE-2026-34517.patch \ + file://CVE-2026-34520.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:15 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: 99500 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 8FAACCA5FAE for ; Mon, 28 Sep 2026 17:43:42 +0000 (UTC) Received: from rcdn-iport-6.cisco.com (rcdn-iport-6.cisco.com [173.37.86.77]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63550.1790617413037125375 for ; Mon, 28 Sep 2026 10:43:33 -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=i/c4CJoF; spf=pass (domain: cisco.com, ip: 173.37.86.77, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=17517; q=dns/txt; s=iport01; t=1790617413; x=1791827013; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=ItbZtDmn/uCrjFyegp2dHQcKuIrnWCKNE8Qswv1JF4I=; b=i/c4CJoFbaQR77tQ+WrKPvJ38jwYmfd5Fgtrhp0cV45adNyOgURUO9GX PHtxqJgvkUhWlcZIbyyehyXy+TWyL6wvXm8qPDkIngvh+bICAXUTpA1RE BfB2a1eWS0bWIiRXADrL8lkylQe0KbqshL8WVc0cwS1a+Q0VuXTZZ490u fDuBjWsK9df02uIygrPMVEpVDu106xNEIULY96XMxKU5Q3FIXGmNa7EjO lwFWKtEbstGL9grRzb4Njw1NQzTjNZ2H6aA3xmnDHdIsm+nMhrWlu7ahu GOo3kjnN7p+V4cbihJXqVxe43sIIL1e87qwItQmuxANrNH2+RRIc8KpL3 w==; X-CSE-ConnectionGUID: S4JTaBgwS3mzBtBMPjXdag== X-CSE-MsgGUID: nqeP6IAqTxaJQ0YaueZ+gQ== X-IPAS-Result: A0AaAAC9prpq/43/Ja1aHAEBAQEBAQcBARIBAQQEAQGBfAcBAQsBglZ1YENJhFeIG4lYA4tkhWWMUYF+DwEBAQ9EDQQBAYQ/RgKOCQImNAkOAQIEAwIDAQEBAQEBAQEBAQEBCgEBBQEBAQIBBwWBDhOGTw2QEgECAQMjBAsBGAEtEBwDAQIDAiYCAiALIwgQCYMCAYI6AzcDEcJFen8zgQGDKQE/AkNQ2EsNgloBCxQBgQouAYU/gn8gAYUDXRgBhHwnGxuBcoEVg2mBBYEaQgEBAoFGg3OCagSCIoEMgVqBA5JASIECHANZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPhcvWBsGBYEdgSCCGSMZNnqBCV6BKylgARAXgQeCBwKCVIIBAgFJQw4HRVMJJUEKEkcmIggSCQETGjALgSE4QQkoERgNSBEsNxUZBD0BbgeQLh6BdGoHAYENARsOAQEXC12BFRgGFgKTLSUKj1uCIYE1nmlxCiiDdowijz6FfBozhVulFAuYfY4KhAmRKzQYUIRpgWg8OYEOCwdwFTuCZwlKGQ+BHI0PDguDYIF/gxTHJicyAgEIMgEBBwIHDgMLgWiRfgEB IronPort-Data: A9a23:uOaeDK3n7RaoBjTycfbD5YNwkn2cJEfYwER7XKvMYLTBsI5bpzYPn DMeX2vQM/fcNGOmfYxxOo6wp0tT75SGyd83Slc93Hw8FHgiRegpqji6wuYcGwvIc6UvmWo+t 512huHodZ5yEzmE4Ej9atANlFEkvYmQXL3wFeXYDS54QA5gWU8JhAlq8wIDqtYAbeORXUXX5 rsen+WFYAX7g28uYjpNg06+gEoHUMra6WtwUmMWPZinjHeG/1EJAZQWI72GLneQauF8Au6gS u/f+6qy92Xf8g1FIovNfmHTKxBirhb6ZGBiu1IOM0SQqkEqSh8ajs7XAMEhhXJ/0F1lqTzeJ OJl7vRcQS9xVkHFdX90vxNwS0mSNoUekFPLzOTWXcG7lyX7n3XQL/pGNx0UAYQy9flMWmRc3 +AyDQgPXhyKiLfjqF67YrEEasULNsLnOsYb/3pn1zycVapgSpHYSKKM7thdtNsyrpkRRrCFO IxDNGcpNUiZC/FMEg9/5JYWkOSlgnD+YjRwo1OOrq1x6G/WpOB0+OW3YIePIIDQHq25mG6ev 2/+oUXQGy0KPf6mkQuM73+exeD2yHaTtIU6UefQGuRRqFqLy2oeDRcbWVe2rby1h1CzX/pbK lcI4WwptaU0+UmhQ9XxUhH+p2SL1iPwQPJKGOE8rQXIwa3O7kPBWi4PTyVKb5ots8peqSEW6 2JlVujBXVRH2IB5g1rHnltIhVte4RQoEFI= IronPort-HdrOrdr: A9a23:f7vbZK6+ywJAKEnMoQPXwBDXdLJyesId70hD6qm+c3Nom6uj5q aTdZUgpHjJYVkqOU3I9ersBEDEewK/yXcX2/h0AV7dZmnbUQKTRekIh7cKgQeQfhEWndQy6U 4PScRD4aXLfDtHpPe/xhWkGNA9x9TC2qWpieDCi0pJd2hRGthdB8MTMHfhLqWwLzM2faYEKA == X-Talos-CUID: 9a23:obLozWm7uTWV85fvidufpGdO9/TXOUaB7kvZCUCnMj1sWpeJY3qo+pNomsU7zg== X-Talos-MUID: 9a23:w1fgzQmOQLBBiGaJ6VmadnpkLcpQvYWHEXwdmLc/4/iNKCFCeDik2WE= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="528522423" Received: from rcdn-l-core-04.cisco.com ([173.37.255.141]) by rcdn-iport-6.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-04.cisco.com (Postfix) with ESMTPS id E0AC0180005C6; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 38E20CBF21E; Mon, 28 Sep 2026 10:43:31 -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 05/13] python3-aiohttp: fix CVE-2026-34525 Date: Mon, 28 Sep 2026 10:43:15 -0700 Message-Id: <20260928174323.1810308-6-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-l-core-04.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, 28 Sep 2026 17:43:42 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130453 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/e00ca3cca92c465c7913c4beb763a72da9ed8349 [2] https://github.com/aio-libs/aiohttp/commit/53e2e6fc58b89c6185be7820bd2c9f40216b3000 [3] https://nvd.nist.gov/vuln/detail/CVE-2026-34525 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-34525_p1.patch | 128 +++++++ .../python3-aiohttp/CVE-2026-34525_p2.patch | 331 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 2 + 3 files changed, 461 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p2.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p1.patch new file mode 100644 index 0000000000..d18479e540 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p1.patch @@ -0,0 +1,128 @@ +From 4032dcab4b75e875cb81e9a8726214c38b66747a Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Sun, 15 Mar 2026 13:58:08 +0000 +Subject: [PATCH] [PR #12240/345d2537 backport][3.13] Reject duplicate + singleton headers in C extension parser (#12241) + +**This is a backport of PR #12240 as merged into master +(345d25371562dd56de099f1fcd5720e96c6e7702).** + +CVE: CVE-2026-34525 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/e00ca3cca92c465c7913c4beb763a72da9ed8349] + +Co-authored-by: Rodrigo Nogueira +(cherry picked from commit e00ca3cca92c465c7913c4beb763a72da9ed8349) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12240.bugfix.rst | 5 +++++ + aiohttp/_http_parser.pyx | 22 +++++++++++++++++++++ + tests/test_http_parser.py | 41 +++++++++++++++++++++++++++++++++++++++ + 3 files changed, 68 insertions(+) + create mode 100644 CHANGES/12240.bugfix.rst + +diff --git a/CHANGES/12240.bugfix.rst b/CHANGES/12240.bugfix.rst +new file mode 100644 +index 000000000..49508b3f5 +--- /dev/null ++++ b/CHANGES/12240.bugfix.rst +@@ -0,0 +1,5 @@ ++Rejected duplicate singleton headers (``Host``, ``Content-Type``, ++``Content-Length``, etc.) in the C extension HTTP parser to match ++the pure Python parser behavior, preventing potential host-based ++access control bypasses via parser differentials ++-- by :user:`rodrigobnogueira`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index 213ce2f0c..bb7bf673d 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -72,6 +72,20 @@ cdef object StreamReader = _StreamReader + cdef object DeflateBuffer = _DeflateBuffer + + ++# https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-6 ++cdef tuple SINGLETON_HEADERS = ( ++ hdrs.CONTENT_LENGTH, ++ hdrs.CONTENT_LOCATION, ++ hdrs.CONTENT_RANGE, ++ hdrs.CONTENT_TYPE, ++ hdrs.ETAG, ++ hdrs.HOST, ++ hdrs.MAX_FORWARDS, ++ hdrs.SERVER, ++ hdrs.TRANSFER_ENCODING, ++ hdrs.USER_AGENT, ++) ++ + cdef inline object extend(object buf, const char* at, size_t length): + cdef Py_ssize_t s + cdef char* ptr +@@ -438,6 +452,14 @@ cdef class HttpParser: + raw_headers = tuple(self._raw_headers) + headers = CIMultiDictProxy(self._headers) + ++ # https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf ++ bad_hdr = next( ++ (h for h in SINGLETON_HEADERS if len(headers.getall(h, ())) > 1), ++ None, ++ ) ++ if bad_hdr is not None: ++ raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.") ++ + 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 +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index be7446e0a..b0a282f3e 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -263,6 +263,47 @@ def test_content_length_transfer_encoding(parser: Any) -> None: + parser.feed_data(text) + + ++@pytest.mark.parametrize( ++ "hdr", ++ ( ++ "Content-Length", ++ "Content-Location", ++ "Content-Range", ++ "Content-Type", ++ "ETag", ++ "Host", ++ "Max-Forwards", ++ "Server", ++ "Transfer-Encoding", ++ "User-Agent", ++ ), ++) ++def test_duplicate_singleton_header_rejected( ++ parser: HttpRequestParser, hdr: str ++) -> None: ++ val1, val2 = ("1", "2") if hdr == "Content-Length" else ("value1", "value2") ++ text = ( ++ f"GET /test HTTP/1.1\r\n" ++ f"Host: example.com\r\n" ++ f"{hdr}: {val1}\r\n" ++ f"{hdr}: {val2}\r\n" ++ f"\r\n" ++ ).encode() ++ with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"): ++ parser.feed_data(text) ++ ++ ++def test_duplicate_host_header_rejected(parser: HttpRequestParser) -> None: ++ text = ( ++ b"GET /admin HTTP/1.1\r\n" ++ b"Host: admin.example\r\n" ++ b"Host: public.example\r\n" ++ b"\r\n" ++ ) ++ with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate.*Host"): ++ parser.feed_data(text) ++ ++ + def test_bad_chunked_py(loop: Any, protocol: Any) -> None: + """Test that invalid chunked encoding doesn't allow content-length to be used.""" + parser = HttpRequestParserPy( + +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p2.patch new file mode 100644 index 0000000000..57fa7e43a9 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34525_p2.patch @@ -0,0 +1,331 @@ +From 285d606756b2de628b957ac8792ce8f0539ec6f6 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Tue, 31 Mar 2026 10:34:37 -1000 +Subject: [PATCH] [PR #12302/2dc02ee0 backport][3.13] Skip duplicate singleton + header check in lax mode (#12303) + +Co-authored-by: J. Nick Koston +Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> +Fixes home-assistant/core#166956 +Fixes https://github.com/getmoto/moto/issues/9930 +Fixes #12301 +Fixes https://github.com/catalyst-cooperative/pudl-archiver/issues/1059 + +CVE: CVE-2026-34525 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/53e2e6fc58b89c6185be7820bd2c9f40216b3000] + +(cherry picked from commit 53e2e6fc58b89c6185be7820bd2c9f40216b3000) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12302.bugfix.rst | 3 ++ + aiohttp/_http_parser.pyx | 27 ++++++----- + aiohttp/http_parser.py | 40 ++++++++-------- + tests/test_http_parser.py | 97 ++++++++++++++++++++++++++++++++++++--- + 4 files changed, 131 insertions(+), 36 deletions(-) + create mode 100644 CHANGES/12302.bugfix.rst + +diff --git a/CHANGES/12302.bugfix.rst b/CHANGES/12302.bugfix.rst +new file mode 100644 +index 000000000..fe9e8fbd6 +--- /dev/null ++++ b/CHANGES/12302.bugfix.rst +@@ -0,0 +1,3 @@ ++Skipped the duplicate singleton header check in lax mode (the default for response ++parsing). In strict mode (request parsing, or ``-X dev``), all RFC 9110 singletons ++are still enforced -- by :user:`bdraco`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index bb7bf673d..8e9ecae69 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -72,8 +72,11 @@ cdef object StreamReader = _StreamReader + cdef object DeflateBuffer = _DeflateBuffer + + +-# https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-6 +-cdef tuple SINGLETON_HEADERS = ( ++# RFC 9110 singleton headers — duplicates are rejected in strict mode. ++# In lax mode (response parser default), the check is skipped entirely ++# since real-world servers (e.g. Google APIs, Werkzeug) commonly send ++# duplicate headers like Content-Type or Server. ++cdef frozenset SINGLETON_HEADERS = frozenset({ + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_LOCATION, + hdrs.CONTENT_RANGE, +@@ -84,7 +87,7 @@ cdef tuple SINGLETON_HEADERS = ( + hdrs.SERVER, + hdrs.TRANSFER_ENCODING, + hdrs.USER_AGENT, +-) ++}) + + cdef inline object extend(object buf, const char* at, size_t length): + cdef Py_ssize_t s +@@ -304,6 +307,7 @@ cdef class HttpParser: + size_t _max_headers + bint _response_with_body + bint _read_until_eof ++ bint _lax + + bint _started + object _url +@@ -311,6 +315,7 @@ cdef class HttpParser: + str _path + str _reason + object _headers ++ set _seen_singletons + list _raw_headers + bint _upgraded + list _messages +@@ -377,6 +382,8 @@ cdef class HttpParser: + self._upgraded = False + self._auto_decompress = auto_decompress + self._content_encoding = None ++ self._lax = False ++ self._seen_singletons = set() + + self._csettings.on_url = cb_on_url + self._csettings.on_status = cb_on_status +@@ -407,6 +414,10 @@ cdef class HttpParser: + if "\x00" in value: + raise InvalidHeader(raw_value) + ++ if not self._lax and name in SINGLETON_HEADERS: ++ if name in self._seen_singletons: ++ raise BadHttpMessage(f"Duplicate '{name}' header found.") ++ self._seen_singletons.add(name) + self._headers.add(name, value) + if len(self._headers) > self._max_headers: + raise BadHttpMessage("Too many headers received") +@@ -452,14 +463,6 @@ cdef class HttpParser: + raw_headers = tuple(self._raw_headers) + headers = CIMultiDictProxy(self._headers) + +- # https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf +- bad_hdr = next( +- (h for h in SINGLETON_HEADERS if len(headers.getall(h, ())) > 1), +- None, +- ) +- if bad_hdr is not None: +- raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.") +- + 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 +@@ -695,6 +698,7 @@ cdef class HttpResponseParser(HttpParser): + cparser.llhttp_set_lenient_headers(self._cparser, 1) + cparser.llhttp_set_lenient_optional_cr_before_lf(self._cparser, 1) + cparser.llhttp_set_lenient_spaces_after_chunk_size(self._cparser, 1) ++ self._lax = True + + cdef object _on_status_complete(self): + if self._buf: +@@ -708,6 +712,7 @@ cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1: + + pyparser._started = True + pyparser._headers = CIMultiDict() ++ pyparser._seen_singletons = set() + pyparser._raw_headers = [] + PyByteArray_Resize(pyparser._buf, 0) + pyparser._path = None +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index a7fec7f01..4d7931ef6 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -87,6 +87,26 @@ VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII) + DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII) + HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+") + ++# RFC 9110 singleton headers — duplicates are rejected in strict mode. ++# In lax mode (response parser default), the check is skipped entirely ++# since real-world servers (e.g. Google APIs, Werkzeug) commonly send ++# duplicate headers like Content-Type or Server. ++# Lowercased for case-insensitive matching against wire names. ++SINGLETON_HEADERS: Final[frozenset[str]] = frozenset( ++ { ++ "content-length", ++ "content-location", ++ "content-range", ++ "content-type", ++ "etag", ++ "host", ++ "max-forwards", ++ "server", ++ "transfer-encoding", ++ "user-agent", ++ } ++) ++ + + class RawRequestMessage(NamedTuple): + method: str +@@ -216,6 +236,8 @@ class HeadersParser: + elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value): + raise InvalidHeader(bvalue) + ++ if not self._lax and name in headers and name.lower() in SINGLETON_HEADERS: ++ raise BadHttpMessage(f"Duplicate '{name}' header found.") + headers.add(name, value) + raw_headers.append((bname, bvalue)) + +@@ -526,24 +548,6 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + upgrade = False + chunked = False + +- # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-6 +- # https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf +- singletons = ( +- hdrs.CONTENT_LENGTH, +- hdrs.CONTENT_LOCATION, +- hdrs.CONTENT_RANGE, +- hdrs.CONTENT_TYPE, +- hdrs.ETAG, +- hdrs.HOST, +- hdrs.MAX_FORWARDS, +- hdrs.SERVER, +- hdrs.TRANSFER_ENCODING, +- hdrs.USER_AGENT, +- ) +- bad_hdr = next((h for h in singletons if len(headers.getall(h, ())) > 1), None) +- if bad_hdr is not None: +- raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.") +- + # keep-alive + conn = headers.get(hdrs.CONNECTION) + if conn: +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index b0a282f3e..3e1f7dfaa 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -267,32 +267,76 @@ def test_content_length_transfer_encoding(parser: Any) -> None: + "hdr", + ( + "Content-Length", ++ "Host", ++ "Transfer-Encoding", ++ ), ++) ++def test_duplicate_singleton_header_rejected( ++ parser: HttpRequestParser, hdr: str ++) -> None: ++ val1, val2 = ("1", "2") if hdr == "Content-Length" else ("value1", "value2") ++ text = ( ++ f"GET /test HTTP/1.1\r\n" ++ f"Host: example.com\r\n" ++ f"{hdr}: {val1}\r\n" ++ f"{hdr}: {val2}\r\n" ++ "\r\n" ++ ).encode() ++ with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"): ++ parser.feed_data(text) ++ ++ ++@pytest.mark.parametrize( ++ "hdr", ++ ( + "Content-Location", + "Content-Range", + "Content-Type", + "ETag", +- "Host", + "Max-Forwards", + "Server", +- "Transfer-Encoding", + "User-Agent", + ), + ) +-def test_duplicate_singleton_header_rejected( ++def test_duplicate_non_security_singleton_header_rejected_strict( + parser: HttpRequestParser, hdr: str + ) -> None: +- val1, val2 = ("1", "2") if hdr == "Content-Length" else ("value1", "value2") ++ """Non-security singletons are rejected in strict mode (requests).""" + text = ( + f"GET /test HTTP/1.1\r\n" + f"Host: example.com\r\n" +- f"{hdr}: {val1}\r\n" +- f"{hdr}: {val2}\r\n" +- f"\r\n" ++ f"{hdr}: value1\r\n" ++ f"{hdr}: value2\r\n" ++ "\r\n" + ).encode() + with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"): + parser.feed_data(text) + + ++@pytest.mark.parametrize( ++ "hdr", ++ ( ++ # Content-Length is excluded because llhttp rejects duplicates ++ # at the C level before our singleton check runs. ++ "Content-Location", ++ "Content-Range", ++ "Content-Type", ++ "ETag", ++ "Max-Forwards", ++ "Server", ++ "Transfer-Encoding", ++ "User-Agent", ++ ), ++) ++def test_duplicate_singleton_header_accepted_in_lax_mode( ++ response: HttpResponseParser, hdr: str ++) -> None: ++ """All singleton duplicates are accepted in lax mode (response parser default).""" ++ text = (f"HTTP/1.1 200 OK\r\n{hdr}: value1\r\n{hdr}: value2\r\n\r\n").encode() ++ messages, upgrade, tail = response.feed_data(text) ++ assert len(messages) == 1 ++ ++ + def test_duplicate_host_header_rejected(parser: HttpRequestParser) -> None: + text = ( + b"GET /admin HTTP/1.1\r\n" +@@ -304,6 +348,45 @@ def test_duplicate_host_header_rejected(parser: HttpRequestParser) -> None: + parser.feed_data(text) + + ++@pytest.mark.parametrize( ++ ("hdr1", "hdr2"), ++ ( ++ ("content-length", "Content-Length"), ++ ("Content-Length", "content-length"), ++ ("transfer-encoding", "Transfer-Encoding"), ++ ("Transfer-Encoding", "transfer-encoding"), ++ ), ++) ++def test_duplicate_singleton_header_different_casing_rejected( ++ parser: HttpRequestParser, hdr1: str, hdr2: str ++) -> None: ++ """Singleton check must be case-insensitive per RFC 9110.""" ++ val1, val2 = ("1", "2") if "content-length" in hdr1.lower() else ("v1", "v2") ++ text = ( ++ f"GET /test HTTP/1.1\r\n" ++ f"Host: example.com\r\n" ++ f"{hdr1}: {val1}\r\n" ++ f"{hdr2}: {val2}\r\n" ++ "\r\n" ++ ).encode() ++ with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"): ++ parser.feed_data(text) ++ ++ ++def test_duplicate_host_header_different_casing_rejected( ++ parser: HttpRequestParser, ++) -> None: ++ """Duplicate Host with different casing must also be rejected.""" ++ text = ( ++ b"GET /test HTTP/1.1\r\n" ++ b"host: evil.example\r\n" ++ b"Host: good.example\r\n" ++ b"\r\n" ++ ) ++ with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"): ++ parser.feed_data(text) ++ ++ + def test_bad_chunked_py(loop: Any, protocol: Any) -> None: + """Test that invalid chunked encoding doesn't allow content-length to be used.""" + parser = HttpRequestParserPy( + +-- +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 b7741d5ed4..921dc01dc3 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 @@ -25,6 +25,8 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34516.patch \ file://CVE-2026-34517.patch \ file://CVE-2026-34520.patch \ + file://CVE-2026-34525_p1.patch \ + file://CVE-2026-34525_p2.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:16 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: 99498 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 59D24CA5FB0 for ; Mon, 28 Sep 2026 17:43:43 +0000 (UTC) Received: from rcdn-iport-4.cisco.com (rcdn-iport-4.cisco.com [173.37.86.75]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63548.1790617412905291384 for ; Mon, 28 Sep 2026 10:43:33 -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=ChkL7i1H; spf=pass (domain: cisco.com, ip: 173.37.86.75, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=3854; q=dns/txt; s=iport01; t=1790617412; x=1791827012; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=w4OnNCbA7CSiIvRW3K1mxAYE2npWRKCzP27DYbfxqlw=; b=ChkL7i1H9dhr3GPf/zb/r2v+3ToMXfGiunHnD08wxYQuNAc5EkiE1jAG S3PINstaJCsi5md3HskOe12g30DgVryTzxy/cwolPcxq/8WWAb+tBuON/ InW3ytjFP+PG6iZglDbwNZW1VsVg80qg2uxl5FmJI2ab99JgPqRRkD5g2 g5LaQGueBM/ipgwj0fibih+o+8fQmMPMv2rMKSB96qIQRb1OdEfOn2pfF VRbap0J6CNari9mHRq24WVNQ4rTG3OtisCNGgX6v2Ghb3D84dFrGrrH0D iw8xxBxD2q1gBu9f41ENVchYpkwptcYz2bb0sZBVf/mlJQNONgAkhKWtM A==; X-CSE-ConnectionGUID: ASKRm0ZdQwSrn8OIqadCpA== X-CSE-MsgGUID: 10Zki7MbQwGut7FtmkWE2w== X-IPAS-Result: A0BIAgBOprpq/4z/Ja1aHgEBCxIMggULgld1YENJlkoDnhqBfg8BAQEPRA0EAQGEP0YCjgkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDJwsBGAEtEBwDAQIvKyMIGYMCAYJ0AxHCV4F5M4EBgykBPwJDUNsyAQsUAYE4hUCII10YAYR8JxsbgXKCUIIugQWBXAEBiCUEgiKBDIFak0NIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQcbBgWBHYEgghkjGTZ6gQlegSspYAEQF4EHggcCglSCAQIBSUMOB0VTCSVBChJHJiIIEgkBExowC4EhOEEJKBEYDUgRLDcVGQQ+bgeQLh6CXgeBDgErgRiBMKVaoQ8KKIN2jCKVOhozhVulFAuYfY4KlgBQhGmBaDyBRwsHcBWDIglKGQ+OOYNrgX+DZcZVJzICCTIBAQcCBw4DC4FokX4BAQ IronPort-Data: A9a23:u2L3jK44jiSUkpg7nq0bIwxRtGjGchMFZxGqfqrLsTDasY5as4F+v mcbC2zUbqrbYGOmctF+PI7l9R5TvJPSnYI1SAJl+yw3Zn8b8sCt6fZ1gavT04J+CuWZESqLO u1HMoGowPgcFyGa+1H1dOex9RGQ7InQLpLkEunIJyttcgFtTSYlmHpLlvUw6mJSqYDR7zil5 5Wo/KUzBHf/g2QqajNOu/rawP9SlK2aVA0w7wRWic9j5Dcyp1FNZLoDKKe4KWfPQ4U8NoaSW +bZwbilyXjS9hErB8nNuu6TnpoiG+O60aCm0xK6aoD66vRwjnVaPpUTaJLwXXxqZwChxLid/ jniWauYEm/FNoWU8AgUvoIx/ytWZcWq85efSZSzXFD6I0DuKxPRL/tS4E4eFLY7wOMvM2136 scCLmAqTwmKi8mu6efuIgVsrpxLwMjDJogTvDRkiDreF/tjGcuFSKTR7tge1zA17ixMNa+BP IxCNnw1MUmGOkEXUrsUIMpWcOOAinrydzRZuVu9rqss6G+Vxwt0uFToGIePKoHUGJ0LxC50o Eqfp1b6A04/PuWA5ieZrVywg+zwsHnkDdd6+LqQs6QCbEeo7msLBRsbUFG2rfW0hguyVsxSL 2QQ+zEytu417EGtQ9z3UhG0rXLCuQQTM+e8CMUg4w2Lj66R6AGDCy1cFXhKaccts4k9QjlCO kK1ou4FzAdH6NW9IU9xPJ/Nxd9uEUD59VM/WBI= IronPort-HdrOrdr: A9a23:3OJCBaHnJjEB1Ut5pLqEyseALOsnbusQ8zAXPo5KJiC9Ffbo8P xG88576faZslsssTQb6LK90cq7MBfhHOBOgbX5VI3KNGKNhILrFvAG0WKI+VPd8kPFmtK1rZ 0QEJRWOZnXEUVwi9r87U2TFtYtx8TCzYWT7N2uqUuEiWpRGtldB8ATMHfjLnFL X-Talos-CUID: 9a23:5Wgbammg2ItzXJwa7+/CtRx4I/fXOVTm703Ve320MHlSUrvFbn6J8a9Pi+M7zg== X-Talos-MUID: 9a23:L7kYMw+32ZsXjDjCGU1f+W6Qf54w0aevV2UHqIopptXbGzB1OAiw1iviFw== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="528565062" Received: from rcdn-l-core-03.cisco.com ([173.37.255.140]) by rcdn-iport-4.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-03.cisco.com (Postfix) with ESMTPS id E2E33180001F5; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 3DA27CBEF8A; Mon, 28 Sep 2026 10:43:31 -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 06/13] python3-aiohttp: fix CVE-2026-47265 Date: Mon, 28 Sep 2026 10:43:16 -0700 Message-Id: <20260928174323.1810308-7-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:43 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130449 From: Darsh Kelaiya This patch applies the upstream stable-branch fix in [1], which backports the original upstream commit in [2]. The advisory identifying the vulnerability is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/f54c40851b0d6c4bbdab97ba518a223adda32478 [2] https://github.com/aio-libs/aiohttp/commit/d57efb05f5073071ceb2d3b35d72d9d0bc4512a2 [3] https://nvd.nist.gov/vuln/detail/CVE-2026-47265 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-47265.patch | 61 +++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 62 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-47265.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-47265.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-47265.patch new file mode 100644 index 0000000000..94250c6fc4 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-47265.patch @@ -0,0 +1,61 @@ +From b71a4e896630f25248223d05e995eab16e953a72 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Tue, 19 May 2026 01:23:00 +0100 +Subject: [PATCH] Drop cookies on redirect (#12550) (#12640) + +CVE: CVE-2026-47265 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/f54c40851b0d6c4bbdab97ba518a223adda32478] + +Backport Changes: +- Scarthgap 3.9.5 lacks the upstream global-auth redirect test. +- Added the per-request cookies case to the existing redirect test. + +(cherry picked from commit d57efb05f5073071ceb2d3b35d72d9d0bc4512a2) +(cherry picked from commit f54c40851b0d6c4bbdab97ba518a223adda32478) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12540.bugfix.rst | 1 + + aiohttp/client.py | 1 + + tests/test_client_functional.py | 6 ++++++ + 3 files changed, 8 insertions(+) + create mode 100644 CHANGES/12540.bugfix.rst + +diff --git a/CHANGES/12540.bugfix.rst b/CHANGES/12540.bugfix.rst +new file mode 100644 +index 000000000..dfd98129e +--- /dev/null ++++ b/CHANGES/12540.bugfix.rst +@@ -0,0 +1 @@ ++Fixed per-request ``cookies`` not being dropped on cross-origin redirects -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/client.py b/aiohttp/client.py +index 0b87d7eec..87b697f85 100644 +--- a/aiohttp/client.py ++++ b/aiohttp/client.py +@@ -683,6 +683,7 @@ class ClientSession: + + if url.origin() != parsed_url.origin(): + auth = None ++ cookies = None + headers.pop(hdrs.AUTHORIZATION, None) + headers.pop(hdrs.COOKIE, None) + headers.pop(hdrs.PROXY_AUTHORIZATION, None) +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index 3ca1716ba..2c531d7d3 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -2660,6 +2660,12 @@ async def test_drop_auth_on_redirect_to_other_host( + }, + ) + assert resp.status == 200 ++ resp = await client.get( ++ url_from, ++ headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz"}, ++ cookies={"a": "b"}, ++ ) ++ assert resp.status == 200 + + + async def test_async_with_session() -> 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 921dc01dc3..afb8cbff7d 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 @@ -27,6 +27,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34520.patch \ file://CVE-2026-34525_p1.patch \ file://CVE-2026-34525_p2.patch \ + file://CVE-2026-47265.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:17 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: 99495 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 0F607CA5FAB for ; Mon, 28 Sep 2026 17:43:42 +0000 (UTC) Received: from rcdn-iport-7.cisco.com (rcdn-iport-7.cisco.com [173.37.86.78]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63551.1790617414307827365 for ; Mon, 28 Sep 2026 10:43:34 -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=I0VuPDsO; spf=pass (domain: cisco.com, ip: 173.37.86.78, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=7683; q=dns/txt; s=iport01; t=1790617414; x=1791827014; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=v1HxL/5LW2klpu6yeaobplMzJZ7cE3mo4UNUbDY2hjw=; b=I0VuPDsODrNz5G3errX8Buwq/8NkgA+L0/3EZx1q01kXMk2/eYfXL+4H sX9nxnOXWHe5Xk7S0WrjHnX1N36Ifgn8rOEN1g0N6aN/KcDs5kMWhjKRa FWbCLGo/ivY7Wyo398rZxjte3mcYRjHEkj/1LnoIuSuhmq9+OQT+YlhbR NpDjH4tEe5dQzJC26KZgL5jdMsKcdfSVFBA0fl36T1dqRu33h4idaSr0a /LeJmWG/xdbP8CQiXriZQTropDk1thpBYemHPao9HK/QABe9h6hDa9tRv eg29yB3Ov9FGH6qTyHHL1zwsBvO6ZA6T3wqoMIZDePsn1KfZmGHVkQ1Oi Q==; X-CSE-ConnectionGUID: xQDm9izVQ1qx0PCUVjnxyw== X-CSE-MsgGUID: L75ESegWRt+l4D9MNYTkPw== X-IPAS-Result: A0BIAgC9prpq/4r/Ja1aHgEBCxIMggULgld1YENJlkoDnhqBfg8BAQEPRA0EAQGEP0YCjgkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDJwsBGAEbEhAcAwECLysjCBmDAgGCdAMRwkWBeTOBAYMpAT8CQ1DbMgELFAGBOIVAiCNdGAFEggWCMycbG4FyglCCLoEFgVwBAQKIIwSCIoEMgVqTQ0iBHgNZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPheBBxsGBYEdgSCCGSMZNnqBCV6BKylgARAXgQeCBwKCVIIBAgFJQw4HRVMJJUEKEkcmIggSCQETGjALgSE4QQkoERgNSBEsNxUZBD5uB5AuHoJlUzsBKgEXaIEFAQ80AqVYoQ8KKIN2jCKVOhozhVulFAuYfY4KllCEaYFoPIFHCwdwFYMiCUoZD44rDguDYIF/gxRRxlUnMgIJMgEBBwIHDgMLgWiQAi2BTwEB IronPort-Data: A9a23:6DGOHalBpJxRSDe582kKD2bo5gzRJ0RdPkR7XQ2eYbSJt1+Wr1Gzt xIdXD3UOPbca2X1fNwlb4608xsG6J/cnN9jSgVkqyw3H1tH+JHPbTi7wugcHM8zwunrFh8PA xA2M4GYRCwMZiaC4Errav6+/SEUOZigHtLUEPTDNj16WThqQSIgjQMLs+Mii+aEu/Dha++2k Y20+ZK31GONgWYubDpFsfLb8XuDgdyr0N8mlg1mDRx0lAe2e0k9VPo3Oay3Jn3kdYhYdsbSb /rD1ryw4lTC9B4rDN6/+p6jGqHdauePVeQmoiM+t5mK2nCulARrukoIHKZ0hXNsttm8t4sZJ OOhGnCHYVxB0qXkwIzxWvTDes10FfUuFLTveRBTvSEPpqHLWyOE/hlgMK05FZ0h1uMmAHNwz KUBOGg1RS29heSm0a3uH4GAhux7RCXqFJkUtnclyXTSCuwrBMiaBa7L/tRfmjw3g6iiH96HO JFfMmUpNkmdJUQUaj/7C7pm9AusrnX8fjlRqUOcjaE2+GPUigd21dABNfKIIIPbH54MxR7wS mTu2zukLyoXO+Ol1XmBrl79o8n+oSDDR9dHfFG/3rsw6LGJ/UQUEBAQWF6xrPW1h0L7V99BJ kg8/is1sbN081SmSNT4VRC0rHOI+BkGVLJt//YS8gqBzO/Qpg2eHGVBFmcHY909v8hwTjsvv rOUo+7U6fVUmOX9YRqgGn2891te5QB9wbc+WBI5 IronPort-HdrOrdr: A9a23:A/vDuKja8Bpm+JEJVvnqtUBArHBQXvgji2hC6mlwRA09TyVXra +TdZMgpHjJYVkqOU3I9ersBEDEewK/yXcX2/h0AV7dZmnbUQKTRekIh7cKgQeQfhEWndQy6U 4PScRD4aXLfDtHpPe/xhWkGNA9x9TC2qWpieDCi0pJd2hRGsddB8MTMHfiLqWwLzM2fKYEKA == X-Talos-CUID: 9a23:MrqKTmP+Si6w4u5DSBBL8R8VCP4cK2TNlE/7HnG6D3Z4V+jA X-Talos-MUID: 9a23:QJtH4QooG/gsc14sL8cezxU9aepT7quRMlxOlckq6uyiBBBzAyjI2Q== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="527923130" Received: from rcdn-l-core-01.cisco.com ([173.37.255.138]) by rcdn-iport-7.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-01.cisco.com (Postfix) with ESMTPS id E748C180001E2; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 43B0DCBEF98; Mon, 28 Sep 2026 10:43:31 -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 07/13] python3-aiohttp: fix CVE-2026-50269 Date: Mon, 28 Sep 2026 10:43:17 -0700 Message-Id: <20260928174323.1810308-8-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-l-core-01.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, 28 Sep 2026 17:43:42 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130455 From: Darsh Kelaiya This patch applies the reviewed upstream fix shown in [1]. For aiohttp 3.9.5, it also backports the required header validation from prerequisite commit [2]. The advisory identifying the fix is referenced in [3]. [1] https://github.com/aio-libs/aiohttp/commit/bf88077ebb14f4c29924b8e8904cba20c55c28b8 [2] https://github.com/aio-libs/aiohttp/commit/1e3ecd48ec423c1fcc729c98adff31172faae1ef [3] https://nvd.nist.gov/vuln/detail/CVE-2026-50269 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-50269.patch | 166 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 167 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-50269.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-50269.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-50269.patch new file mode 100644 index 0000000000..e931822a27 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-50269.patch @@ -0,0 +1,166 @@ +From a8ea4a13b9691357fdc7796506b0c85566ed1985 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Thu, 28 May 2026 23:57:41 +0100 +Subject: [PATCH] [PR #12719/879d48d1 backport][3.14] Reject invalid bytes in + multipart/payload headers (#12720) + +**This is a backport of PR #12719 as merged into master +(879d48d1619b9bc3662037afcb8bfa790a205e9b).** + +CVE: CVE-2026-50269 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/bf88077ebb14f4c29924b8e8904cba20c55c28b8] + +Backport Changes: +- Backported the pure-Python http_writer._safe_header() + control-character validation from prerequisite commit + 1e3ecd48ec423c1fcc729c98adff31172faae1ef because aiohttp + 3.9.5 rejects only CR/LF and the upstream CVE regression test + also requires NUL rejection. +- Adapted the prerequisite regression coverage to test _safe_header() + directly using the aiohttp 3.9.5 test layout. Omitted its C + serializer, threat-model, changelog, and broader serializer-test + changes because Payload._binary_headers invokes the pure-Python + helper directly. + +Co-authored-by: Sam Bull +(cherry picked from commit 1e3ecd48ec423c1fcc729c98adff31172faae1ef) +(cherry picked from commit bf88077ebb14f4c29924b8e8904cba20c55c28b8) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12706.bugfix.rst | 1 + + aiohttp/http_writer.py | 10 ++++++++-- + aiohttp/payload.py | 8 +++++--- + tests/test_http_writer.py | 19 +++++++++++++++++++ + tests/test_payload.py | 15 +++++++++++++++ + 5 files changed, 48 insertions(+), 5 deletions(-) + create mode 100644 CHANGES/12706.bugfix.rst + +diff --git a/CHANGES/12706.bugfix.rst b/CHANGES/12706.bugfix.rst +new file mode 100644 +index 000000000..9248585f9 +--- /dev/null ++++ b/CHANGES/12706.bugfix.rst +@@ -0,0 +1 @@ ++Fixed invalid bytes being allowed in multipart/payload headers -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py +index 37cdc06e6..9573768f6 100644 +--- a/aiohttp/http_writer.py ++++ b/aiohttp/http_writer.py +@@ -1,6 +1,7 @@ + """Http related parsers and protocol.""" + + import asyncio ++import re + import zlib + from typing import Any, Awaitable, Callable, NamedTuple, Optional, Union # noqa + +@@ -171,10 +172,15 @@ class StreamWriter(AbstractStreamWriter): + await self._protocol._drain_helper() + + ++# https://www.rfc-editor.org/info/rfc9110/#section-5.5-5 ++# https://www.rfc-editor.org/info/rfc9112/#section-4-3 ++_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]") ++ ++ + def _safe_header(string: str) -> str: +- if "\r" in string or "\n" in string: ++ if _FORBIDDEN_HEADER_CHARS_RE.search(string) is not None: + raise ValueError( +- "Newline or carriage return detected in headers. " ++ "Forbidden control character detected in headers. " + "Potential header injection attack." + ) + return string +diff --git a/aiohttp/payload.py b/aiohttp/payload.py +index 6593b05c6..77caf520c 100644 +--- a/aiohttp/payload.py ++++ b/aiohttp/payload.py +@@ -33,6 +33,7 @@ from .helpers import ( + parse_mimetype, + sentinel, + ) ++from .http_writer import _safe_header + from .streams import StreamReader + from .typedefs import JSONEncoder, _CIMultiDict + +@@ -180,9 +181,10 @@ class Payload(ABC): + @property + def _binary_headers(self) -> bytes: + return ( +- "".join([k + ": " + v + "\r\n" for k, v in self.headers.items()]).encode( +- "utf-8" +- ) ++ "".join( ++ _safe_header(k) + ": " + _safe_header(v) + "\r\n" ++ for k, v in self.headers.items() ++ ).encode("utf-8") + + b"\r\n" + ) + +diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py +index 5649f32f7..62d112cad 100644 +--- a/tests/test_http_writer.py ++++ b/tests/test_http_writer.py +@@ -6,6 +6,7 @@ import pytest + from multidict import CIMultiDict + + from aiohttp import http ++from aiohttp.http_writer import _safe_header + from aiohttp.test_utils import make_mocked_coro + + +@@ -264,6 +265,24 @@ async def test_drain_no_transport(protocol, transport, loop) -> None: + assert not protocol._drain_helper.called + + ++@pytest.mark.parametrize( ++ "char", ++ [chr(c) for c in (*range(0x00, 0x09), *range(0x0A, 0x20), 0x7F)], ++) ++def test_safe_header_rejects_forbidden_control_chars(char: str) -> None: ++ with pytest.raises( ++ ValueError, ++ match="Forbidden control character detected in headers", ++ ): ++ _safe_header(f"value{char}") ++ ++ ++def test_safe_header_allows_htab() -> None: ++ value = "text/plain\tcharset=utf-8" ++ ++ assert _safe_header(value) == value ++ ++ + async def test_write_headers_prevents_injection(protocol, transport, loop) -> None: + msg = http.StreamWriter(protocol, loop) + status_line = "HTTP/1.1 200 OK" +diff --git a/tests/test_payload.py b/tests/test_payload.py +index c8681cb5e..52ace65fd 100644 +--- a/tests/test_payload.py ++++ b/tests/test_payload.py +@@ -54,6 +54,21 @@ def test_payload_content_type() -> None: + assert p.content_type == "application/json" + + ++@pytest.mark.parametrize("bad_byte", ("\r", "\n", "\x00")) ++def test_binary_headers_reject_injection_in_value(bad_byte: str) -> None: ++ p = Payload("test", headers={"X-Custom": f"value{bad_byte}Injected: bad"}) ++ with pytest.raises(ValueError, match="header injection"): ++ p._binary_headers ++ ++ ++@pytest.mark.parametrize("bad_byte", ("\r", "\n", "\x00")) ++def test_binary_headers_reject_injection_in_name(bad_byte: str) -> None: ++ p = Payload("test") ++ p.headers[f"X-Custom{bad_byte}Injected"] = "value" ++ with pytest.raises(ValueError, match="header injection"): ++ p._binary_headers ++ ++ + def test_bytes_payload_default_content_type() -> None: + p = payload.BytesPayload(b"data") + assert p.content_type == "application/octet-stream" +-- +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 afb8cbff7d..58ae583423 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 @@ -28,6 +28,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34525_p1.patch \ file://CVE-2026-34525_p2.patch \ file://CVE-2026-47265.patch \ + file://CVE-2026-50269.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:18 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: 99501 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 DA233CA5FB1 for ; Mon, 28 Sep 2026 17:43:43 +0000 (UTC) Received: from rcdn-iport-8.cisco.com (rcdn-iport-8.cisco.com [173.37.86.79]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63553.1790617415702148386 for ; Mon, 28 Sep 2026 10:43:35 -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=XylsrbhU; spf=pass (domain: cisco.com, ip: 173.37.86.79, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=8668; q=dns/txt; s=iport01; t=1790617415; x=1791827015; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=Oc4e3YtOWdxsX0H7/VJgwBGUc2oqy3A+tD59HM/nEEg=; b=XylsrbhUxgpE9cOnJfj9L0d5Y+P5lyCz9pzlyVpxnb0KP0570HSvc9oS dAQr9jUyZGzSVrZ1tbRQOww5nhO0BodxVv3o8uzuulZfxXq6bhrh4VS0M yQDomOkHijwiWGQ2pN0rrY9grmHWuimCn53tddcIjC0Daz/dZZKbkYfiJ 6ke0OKAcmKteH/NkzPIDnbhRQNxU2Slh4LOnuZkf6O5Os3/HgojW26lUu uFaUftwOZgR9jsCU/WY5sNUn6OnQCJb7z/aSJVid8ooCI5DGgW8Lf4tPs 4vKfwftTRKW2Gl6WPYbpajEMMfB+r3r0wQsf3mEZxIfPcUMSNVav6BXrM Q==; X-CSE-ConnectionGUID: 1MusgNiGQo2DLMlVXOeMJg== X-CSE-MsgGUID: fTHH9N5zQIGHi2ZPt8TySw== X-IPAS-Result: A0BIAgC9prpq/5P/Ja1aglmCV3VgQ0mWSgOeGoF+DwEBAQ9EDQQBAYQ/RgKOCQImNAkOAQIEAwIDAQEBAQEBAQEBAQEBCgEBBQEBAQIBBwWBDhOGTw2QEgECAQMnCwEYAS0QHAMBAi8rIwgZgwIBNoI+AxHCRYF5M4EBgykBPwJDUNsyAQsUAYE4hUCII10YAYR8JxsbgXKBFYE7gi6BBYFcAQGBJxGGbQSCIoEMgVoekyVIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQcbBgWBHYEgghkjGTZ6gQlegSspYAEQF4EHggcCglSCAQIBSUMOB0VTCSVBChJHJiIIEgkBExowC4EhOEEJKBEYDUgRLDcVGQQ+bgeQLh6BcnMBgQ0BKxdoEjwrHDQCpVihDwoog3aMIpU6GjOEBIFXkkCSVAuYfY4KllCEaYFoPIFHCwdwFYMiCUoZD44rDguDYIF/yjonMgIJMgEBBwIHDgMLgWiQAAEngVYBAQ IronPort-Data: A9a23:ETgZUKM0UAt+9kLvrR32lsFynXyQoLVcMsEvi/4bfWQNrUor1mcEm zBMUDyHaPuCamOhLd5yaIm2oExQvMeAmodqS3M5pCpnJ55oRWUpJjg4wmPYZX76whjrFRo/h ykmQoCeap1yFjmD9k/F3oHJ9RFUzbuPSqf3FNnKMyVwQR4MYCo6gHqPocZh6mJTqYb/WVzlV e/a+ZWFZgf0gW4sawr41orawP9RlKWq0N8nlgRWicBj5Df2i3QTBZQDEqC9R1OQapVUBOOzW 9HYx7i/+G7Dlz91Yj9yuu+mGqGiaue60Tmm0hK6aYD76vRxjnBaPpIACRYpQRw/ZwNlMDxG4 I4lWZSYEW/FN0BX8QgXe0Ew/ypWZcWq9FJbSJSymZT78qHIT5fj66xLEVo5G4w3w+EtLUVr8 cIoDz0oTB/W0opawJrjIgVtrt4oIM+uOMYUvWttiGiBS/0nWpvEBa7N4Le03h9p2ZsIRqmYP ZdEL2MzM3wsYDUXUrsTIJ49keOhh2j2WzZZs1mS46Ew5gA/ySQsiuCxaoCLIYDiqcN9kHq4h jPgonbAIz46NYat8SbVzmOXmbqa9c/8cMdIfFGizdZtmFCVy2kZBREaWFf+rfSnh0qWX9NEN 1dS/TIjq6U3/kGnQtTxGRqirxa5UgU0QdFcFag+rQqK0KeRul7fDWkfRTkHY9sj3CMreQEXO payt4uBLVRSXHe9EC/1Gmu8xd9qBRUoEA== IronPort-HdrOrdr: A9a23:NkNJJqrOrhbeHKLNtXl2yrsaV5oJeYIsimQD101hICG9vPb2qy nIpoV96faaslcssR0b9OxofZPwI080lqQFhbX5Q43DYOCOggLBR+tfBMnZsljd8kbFmNK1u5 0NT0EHMqySMXFKyeDn/QK/D9EshPOD8KyumKPi6k0Fd3ANV0mlhD0Jcjpy1SZNNXB7OaY= X-Talos-CUID: 9a23:wWv7428ulfrOd3QxG/yVvxcxB9sqVHzy9XCKD2udDVhZb7Cwa1DFrQ== X-Talos-MUID: 9a23:HcZnoQVYJdUfbI/q/DHQpBEzEpp62oWBKEFXo60a6/WVDjMlbg== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="520422634" Received: from rcdn-l-core-10.cisco.com ([173.37.255.147]) by rcdn-iport-8.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-10.cisco.com (Postfix) with ESMTPS id E72BB18000276; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 48A5DCBEFAB; Mon, 28 Sep 2026 10:43:31 -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 08/13] python3-aiohttp: fix CVE-2026-54274 Date: Mon, 28 Sep 2026 10:43:18 -0700 Message-Id: <20260928174323.1810308-9-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:43 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130456 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/14b6ee851fb16ec199acb950de0c82d476799e7d [2] https://nvd.nist.gov/vuln/detail/CVE-2026-54274 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-54274.patch | 195 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 196 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54274.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54274.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54274.patch new file mode 100644 index 0000000000..c5566f75d6 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54274.patch @@ -0,0 +1,195 @@ +From 23b4506906b28c91439bce6d5cbce2b9989ffe61 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Sat, 6 Jun 2026 00:05:19 +0100 +Subject: [PATCH] [PR #12817/69344c6e backport][3.14] Improve websocket checks + (#12818) + +**This is a backport of PR #12817 as merged into master +(69344c6efa3e5dd80b1c88079fa06d4e902a3b83).** + +CVE: CVE-2026-54274 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/14b6ee851fb16ec199acb950de0c82d476799e7d] + +Backport Changes: +- Adapt the fix from aiohttp/_websocket/reader_py.py in aiohttp 3.14 + to aiohttp/http_websocket.py in 3.9.5, mapping OP_CODE_* to + WSMsgType.* and _payload_bytes_to_read to _payload_length. +- Preserve the older parser's two partial-message size-check removals, + accounting for the additional deletion relative to upstream. +- Omit WebSocketDataQueue annotations because that type is not imported + by the 3.9.5 websocket parser test module. + +Co-authored-by: Sam Bull +(cherry picked from commit 14b6ee851fb16ec199acb950de0c82d476799e7d) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12817.bugfix.rst | 1 + + aiohttp/http_websocket.py | 43 ++++++++++++++------- + tests/test_websocket_parser.py | 68 ++++++++++++++++++++++++++++++++++ + 3 files changed, 98 insertions(+), 14 deletions(-) + create mode 100644 CHANGES/12817.bugfix.rst + +diff --git a/CHANGES/12817.bugfix.rst b/CHANGES/12817.bugfix.rst +new file mode 100644 +index 000000000..c8a35e309 +--- /dev/null ++++ b/CHANGES/12817.bugfix.rst +@@ -0,0 +1 @@ ++Tightened up some websocket parser checks -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/http_websocket.py b/aiohttp/http_websocket.py +index 39f2e4a5c..6d95f386d 100644 +--- a/aiohttp/http_websocket.py ++++ b/aiohttp/http_websocket.py +@@ -370,13 +370,6 @@ class WebSocketReader: + if opcode != WSMsgType.CONTINUATION: + self._opcode = opcode + self._partial.extend(payload) +- if self._max_msg_size and len(self._partial) >= self._max_msg_size: +- raise WebSocketError( +- WSCloseCode.MESSAGE_TOO_BIG, +- "Message size {} exceeds limit {}".format( +- len(self._partial), self._max_msg_size +- ), +- ) + else: + # previous frame was non finished + # we should get continuation opcode +@@ -394,13 +387,6 @@ class WebSocketReader: + self._opcode = None + + self._partial.extend(payload) +- if self._max_msg_size and len(self._partial) >= self._max_msg_size: +- raise WebSocketError( +- WSCloseCode.MESSAGE_TOO_BIG, +- "Message size {} exceeds limit {}".format( +- len(self._partial), self._max_msg_size +- ), +- ) + + # Decompress process must to be done after all packets + # received. +@@ -482,6 +468,19 @@ class WebSocketReader: + "Received frame with non-zero reserved bits", + ) + ++ if opcode not in { ++ WSMsgType.CONTINUATION, ++ WSMsgType.TEXT, ++ WSMsgType.BINARY, ++ WSMsgType.CLOSE, ++ WSMsgType.PING, ++ WSMsgType.PONG, ++ }: ++ raise WebSocketError( ++ WSCloseCode.PROTOCOL_ERROR, ++ f"Unexpected opcode={opcode!r}", ++ ) ++ + if opcode > 0x7 and fin == 0: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, +@@ -555,6 +554,22 @@ class WebSocketReader: + else WSParserState.READ_PAYLOAD + ) + ++ # Reject oversized data frames before buffering any payload ++ # bytes. Control frames are capped at 125 bytes (checked in ++ # READ_HEADER) so only text/binary/continuation need this. ++ if self._max_msg_size and self._frame_opcode in { ++ WSMsgType.TEXT, ++ WSMsgType.BINARY, ++ WSMsgType.CONTINUATION, ++ }: ++ projected_size = self._payload_length + len(self._partial) ++ if projected_size >= self._max_msg_size: ++ raise WebSocketError( ++ WSCloseCode.MESSAGE_TOO_BIG, ++ f"Message size {projected_size} " ++ f"exceeds limit {self._max_msg_size}", ++ ) ++ + # read payload mask + if self._state == WSParserState.READ_PAYLOAD_MASK: + if buf_length - start_pos >= 4: +diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py +index 3bdd8108e..b9efe9dcf 100644 +--- a/tests/test_websocket_parser.py ++++ b/tests/test_websocket_parser.py +@@ -498,6 +498,74 @@ def test_compressed_msg_too_large(out) -> None: + assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG + + ++@pytest.mark.parametrize("fin", (0x80, 0x00), ids=("fin", "non-fin")) ++def test_msg_too_large_at_header(out, fin: int) -> None: ++ max_msg_size = 256 ++ parser = WebSocketReader(out, max_msg_size, compress=False) ++ ++ # Header alone: TEXT, 64-bit length, declares 1 MiB of payload. ++ header = PACK_LEN3(fin | WSMsgType.TEXT, 127, 1024 * 1024) ++ with pytest.raises( ++ WebSocketError, match=r"^Message size 1048576 exceeds limit 256$" ++ ) as ctx: ++ parser._feed_data(header) ++ assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG ++ ++ ++def test_msg_too_large_across_fragments(out) -> None: ++ # Individual fragments fit under max_msg_size but accumulate past it. ++ max_msg_size = 256 ++ parser = WebSocketReader(out, max_msg_size, compress=False) ++ ++ first = build_frame(b"a" * 100, WSMsgType.TEXT, is_fin=False) ++ parser._feed_data(first) ++ middle = build_frame(b"b" * 100, WSMsgType.CONTINUATION, is_fin=False) ++ parser._feed_data(middle) ++ ++ # Third 100-byte fragment would push the accumulated total to 300. ++ last = build_frame(b"c" * 100, WSMsgType.CONTINUATION, is_fin=False) ++ with pytest.raises( ++ WebSocketError, match=r"^Message size 300 exceeds limit 256$" ++ ) as ctx: ++ parser._feed_data(last) ++ assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG ++ ++ ++def test_msg_too_large_text_after_non_fin_text(out) -> None: ++ # Protocol-violating sequence: a fresh TEXT arrives while a fragmented ++ # message is still open. ++ max_msg_size = 256 ++ parser = WebSocketReader(out, max_msg_size, compress=False) ++ ++ first = build_frame(b"a" * 200, WSMsgType.TEXT, is_fin=False) ++ parser._feed_data(first) ++ ++ # Second TEXT header alone announces 100 bytes; 100 + 200 partial = 300. ++ second_header = PACK_LEN1(WSMsgType.TEXT, 100) ++ with pytest.raises( ++ WebSocketError, match=r"^Message size 300 exceeds limit 256$" ++ ) as ctx: ++ parser._feed_data(second_header) ++ assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG ++ ++ ++@pytest.mark.parametrize( ++ "opcode", ++ (0x3, 0x4, 0x5, 0x6, 0x7, 0xB, 0xC, 0xD, 0xE, 0xF), ++ ids=lambda v: f"0x{v:x}", ++) ++def test_reserved_opcode_rejected_at_header( ++ out, opcode: int ++) -> None: ++ # RFC 6455 reserves opcodes 0x3-0x7 (non-control) and 0xB-0xF (control). ++ parser = WebSocketReader(out, max_msg_size=256, compress=False) ++ ++ header = PACK_LEN3(0x80 | opcode, 127, 1024 * 1024) ++ with pytest.raises(WebSocketError, match=rf"^Unexpected opcode={opcode}$") as ctx: ++ parser._feed_data(header) ++ assert ctx.value.code == WSCloseCode.PROTOCOL_ERROR ++ ++ + class TestWebSocketError: + def test_ctor(self) -> None: + err = WebSocketError(WSCloseCode.PROTOCOL_ERROR, "Something invalid") +-- +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 58ae583423..5710e38943 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 @@ -29,6 +29,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-34525_p2.patch \ file://CVE-2026-47265.patch \ file://CVE-2026-50269.patch \ + file://CVE-2026-54274.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:19 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: 99503 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 5AD57CA5FA3 for ; Mon, 28 Sep 2026 17:43:44 +0000 (UTC) Received: from rcdn-iport-9.cisco.com (rcdn-iport-9.cisco.com [173.37.86.80]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63549.1790617412936066334 for ; Mon, 28 Sep 2026 10:43:33 -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=KATvy/pY; spf=pass (domain: cisco.com, ip: 173.37.86.80, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=6558; q=dns/txt; s=iport01; t=1790617412; x=1791827012; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=tbgtSRDzx7tmgQ0w4Js6uLWmUnFwqAvVSRa534WBZzE=; b=KATvy/pYMazeB3+BUzGhuLGTpgW2BSVs6LVqge3IRSFLhfKNNg7XRxVz Rxa43u0ZSTx6EwhVvuDsOhoc+3iPisj8ho0OEZLR3B6XHxaKthrB3Wgl7 w9oN7IIL1/yy165VJg50FdJPaZ7p6EmQjxBOGyyjriVgbGibNia9xeNc0 a9X2TNGZA5glxxmhv2Dr4ujB78wkkzXh4pP7GkYPTTLMGgxFnBeerkFhL 7ADzjJYei/7pxjgiVlmcAzoRUfj67XPyuSWdE8ysnIGQ1jxTl98FBcWAW jBVAxndDywCzmuCkBtvr0mZw5zZagjWIyNq61ZT1Wb4Fn1Ij7vbocwjuR w==; X-CSE-ConnectionGUID: JosnDB8jTWKAEGXBsnm5ig== X-CSE-MsgGUID: qUmAod/UT065ktTJGAvDew== X-IPAS-Result: A0AAAwC9prpq/5L/Ja1aglmCV3VgQ0mWSgOeGoF+DwEBAQ9EDQQBAYQwD0YCjgkCJjYHDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDJwsBGS0QHAMBAi8rIwgZgwIBgnEDAxEDwkKBeTOBAYMqPwJDUNsyAQsUAYE4gXODTYgjXRgBRIQ4JxsbgXKCUIE4doEFTYEPAQGBOBCGXQSCIoEMgVqTQ0iBHgNZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPheBBxsGBYEdgSCCGSMZNnqBCV6BKylgARAXgQeCBwKCVIIBAgFJQw4HRVMJJUEKEkcmIggSCQETGjALgSE4QQkoERgNSBEsNxUZBD5uB5AuHoJlgQ4BKgGCFCYOpVqhDwoog3aMIpU6GjOEBIFXkkCSVAuYfY4KlgBQhGmBbwcugUcLB3AVgyIJShkPji0MC4NggX+DFMcmJzICCTIBAQcCBw4DC4FokAGBfQEB IronPort-Data: A9a23:Np2lyah5JPykPPmQR9P7EulyX161MBEKZh0ujC45NGQN5FlHY01je htvC2qHMvuLNmv1LY1zPtzg80kG7ZXSytQ3Twdprn01RCpjpJueD7x1DKtf0wB+jyHnZBg6h ynLQoCYdKjYdleF+FHwdOCn8ikkvU2xbuKUIPbePSxsThNTRi4kiBZy88Y0mYcAbeKRW2thg vus5ZeAULOZ82QsaDxMuvjT8EoHUMna4Vv0gHRvPZing3eG/5UlJMp3Db28KXL+Xr5VEoaSL 87fzKu093/u5BwkDNWoiN7TKiXmlZaLYGBiIlIPM0STqkAqSh4ai87XB9JAAatjsAhlqvgqo Dl7WTNcfi9yVkHEsLx1vxC1iEiSN4UekFPMCSDXXcB+UyQqflO0q8iCAn3aMqU6/f1xLUse9 8AnCxkgQSCpnOSznbO0H7wEasQLdKEHPasFsX1miDWcBvE8TNWbEuPB5MRT23E7gcUm8fT2P pVCL2EwKk6dPlsWZgl/5JEWxI9EglHzcDBcoVOErII84nPYy0p6172F3N/9J4bVG5kJzhnEz o7A133EBzgdOJui9WLbr3Gwus3gtD3XVZ1HQdVU8dYv2jV/3Fc7DwUbU1a+q/S1hkOyHt5SN UEQ0i4vtrQpskuzQ9/wWhe1rHKJslgbQdU4LgEhwBuGxqyR50OSAXIJC2YQLtcnr8QxAzct0 zdlgu/UONCmi5XNIVr1y1tehWna1fQ9RYPaWRI5cA== IronPort-HdrOrdr: A9a23:pggGmqu9AeGONL7c4B6Oh0jM7skDVNV00zEX/kB9WHVpmwKj+P xG+85rsiMc5wxxZJhNo7290ey7MBHhHP1OkO0s1MmZPDUO0VHAROoJ0WKh+UyEJ8SUzIBgPM lbH5SWcOeAbmRSvILd/BSyFcomzZ2s9aClgvqb8lJWJDsaDZ2JK2xCe36m+oocfng+OaYE X-Talos-CUID: 9a23:96LHTWAxM3VsOG76Ewtg70g4MJAnTkL+5Vr/E2WjD2M0SbLAHA== X-Talos-MUID: 9a23:ybn47wvgxsmZkPOTRM2nnQxnHcBkpJmSUB5Qkok6meatKwB9JGLI X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="527644874" Received: from rcdn-l-core-09.cisco.com ([173.37.255.146]) by rcdn-iport-9.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-09.cisco.com (Postfix) with ESMTPS id EA2BD18000238; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 4DF9BCBEFBF; Mon, 28 Sep 2026 10:43:31 -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 09/13] python3-aiohttp: fix CVE-2026-54275 Date: Mon, 28 Sep 2026 10:43:19 -0700 Message-Id: <20260928174323.1810308-10-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:44 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130447 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/0ca2b6c28a25726527a8b60f25960262a91ed0e0 [2] https://nvd.nist.gov/vuln/detail/CVE-2026-54275 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-54275.patch | 124 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 125 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54275.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54275.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54275.patch new file mode 100644 index 0000000000..21528ccb07 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54275.patch @@ -0,0 +1,124 @@ +From 87daca6e11c2123c04d737bde7ddef35a154b272 Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Sun, 7 Jun 2026 00:30:30 -0500 +Subject: [PATCH] [PR #12835/1e94b3e8 backport][3.14] Tls server hostname pool + key (#12847) + +CVE: CVE-2026-54275 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/0ca2b6c28a25726527a8b60f25960262a91ed0e0] + +Backport Changes: +- Upstream uses a NamedTuple ConnectionKey with PEP 604 union + annotations; aiohttp 3.9.5 uses an attr.s class, so server_hostname + is added with the equivalent Optional[str] annotation and included + in its constructor. +- aiohttp 3.9.5 does not define the newer AiohttpServer or + _RequestMaker test aliases, so their fixture annotations are + omitted. The request-key test is synchronous because this branch's + make_request fixture returns ClientRequest directly. + +(cherry picked from commit 0ca2b6c28a25726527a8b60f25960262a91ed0e0) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12835.bugfix.rst | 1 + + aiohttp/client_reqrep.py | 2 ++ + tests/test_client_functional.py | 29 +++++++++++++++++++++++++++++ + tests/test_client_request.py | 14 ++++++++++++++ + 4 files changed, 46 insertions(+) + create mode 100644 CHANGES/12835.bugfix.rst + +diff --git a/CHANGES/12835.bugfix.rst b/CHANGES/12835.bugfix.rst +new file mode 100644 +index 000000000..84a8ae006 +--- /dev/null ++++ b/CHANGES/12835.bugfix.rst +@@ -0,0 +1 @@ ++Included the per-request ``server_hostname`` override in the :class:`~aiohttp.TCPConnector` connection pool key, so a pooled TLS connection is no longer reused for a request that sets ``server_hostname`` to a different value -- by :user:`bdraco`. +diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py +index afe719da1..9abfc79de 100644 +--- a/aiohttp/client_reqrep.py ++++ b/aiohttp/client_reqrep.py +@@ -220,6 +220,7 @@ class ConnectionKey: + proxy: Optional[URL] + proxy_auth: Optional[BasicAuth] + proxy_headers_hash: Optional[int] # hash(CIMultiDict) ++ server_hostname: Optional[str] = None + + + def _is_expected_content_type( +@@ -377,6 +378,7 @@ class ClientRequest: + self.proxy, + self.proxy_auth, + h, ++ self.server_hostname, + ) + + @property +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index 2c531d7d3..40c551ffa 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -462,6 +462,35 @@ async def test_ssl_client( + assert txt == "Test message" + + ++async def test_server_hostname_override_not_reused(aiohttp_server) -> None: ++ """A pooled TLS connection must not be reused for a different server_hostname.""" ++ trustme = pytest.importorskip("trustme") ++ ++ ca = trustme.CA() ++ cert = ca.issue_cert("first.example") ++ server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ++ cert.configure_cert(server_ctx) ++ client_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) ++ ca.configure_trust(client_ctx) ++ ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(text="ok") ++ ++ app = web.Application() ++ app.router.add_route("GET", "/", handler) ++ server = await aiohttp_server(app, ssl=server_ctx) ++ url = server.make_url("/") ++ ++ connector = aiohttp.TCPConnector(ssl=client_ctx, limit=1, limit_per_host=1) ++ async with aiohttp.ClientSession(connector=connector) as session: ++ async with session.get(url, server_hostname="first.example") as resp: ++ assert resp.status == 200 ++ await resp.read() ++ ++ with pytest.raises(aiohttp.ClientConnectorCertificateError): ++ await session.get(url, server_hostname="second.example") ++ ++ + async def test_tcp_connector_fingerprint_ok( + aiohttp_server, + aiohttp_client, +diff --git a/tests/test_client_request.py b/tests/test_client_request.py +index 6084f6854..0ef6e92d6 100644 +--- a/tests/test_client_request.py ++++ b/tests/test_client_request.py +@@ -1326,6 +1326,20 @@ def test_insecure_fingerprint_sha1(loop) -> None: + Fingerprint(hashlib.sha1(b"foo").digest()) + + ++def test_connection_key_includes_server_hostname(make_request) -> None: ++ """A server_hostname override must be part of the connection reuse key.""" ++ url = URL("https://127.0.0.1:8443/") ++ none_req = make_request("GET", url) ++ first = make_request("GET", url, server_hostname="first.example") ++ first_again = make_request("GET", url, server_hostname="first.example") ++ second = make_request("GET", url, server_hostname="second.example") ++ ++ assert first.connection_key.server_hostname == "first.example" ++ assert first.connection_key != none_req.connection_key ++ assert first.connection_key != second.connection_key ++ assert first.connection_key == first_again.connection_key ++ ++ + def test_loose_cookies_types(loop) -> None: + req = ClientRequest("get", URL("http://python.org"), loop=loop) + morsel = Morsel() +-- +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 5710e38943..42402f799b 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 @@ -30,6 +30,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-47265.patch \ file://CVE-2026-50269.patch \ file://CVE-2026-54274.patch \ + file://CVE-2026-54275.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:20 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: 99499 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 E66FDCA5FB2 for ; Mon, 28 Sep 2026 17:43:43 +0000 (UTC) Received: from rcdn-iport-6.cisco.com (rcdn-iport-6.cisco.com [173.37.86.77]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63550.1790617413037125375 for ; Mon, 28 Sep 2026 10:43:33 -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=WXIy20k+; spf=pass (domain: cisco.com, ip: 173.37.86.77, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=6230; q=dns/txt; s=iport01; t=1790617413; x=1791827013; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=NJgI3N3hU+VxxGmzLSIkBWT3+F9XS35G7F/XTuHEQck=; b=WXIy20k+qDCdPuO+ccVyjaJ05LiDVC/jtuFKAFBcPAKe3JZ1AjNmNRjU FeSztM8TKQzlfdMtOa3CFtJpYJFugMeJDH18AAU9SJcfVUL3z2paJFO0C i3sEogj+HoEwUBq3NdjikVRBmW+XIztl+CdjLuRORtJ2sfaONf5FVahwU 3kDjD8ZEqurLTmLtWkYnWeQOz2odfHeUTiSUz9xJTG0+m6AQul9ZjPOgE vOnVGQgaCPke5ZWczQVWULvA3NKjaC/BLQ8gMzjIUgay7vkO0daZahUJi pae1+xQNZTl6w/fNNUhizEguxZXhoX25yn70vMuHG4fBxw3TFf4suQC3f g==; X-CSE-ConnectionGUID: OTJdruwTRkePves2pxd9Mg== X-CSE-MsgGUID: oH6/alXbSpC2nnCD8JMK7w== X-IPAS-Result: A0A4AAC9prpq/4v/Ja1aHQEBAQEJARIBBQUBgXwIAQsBglZ1YAoBOEmEV4gbiVgDnhqBfg8BAQEPRA0EAQGEP0YCjgkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDIwQLARgBLRAJEwMBAgMCJgICKyMIGYMCAYJ0AxGnIZsken8zgQGDKQE/AkNQ2zIBCxQBgQouAYU/gx8BhQNdGAGEfCcbG4FyglCBOHaBBYFcAQGBOIQDgmoEgiKBDIFak0NIgQIcA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+Fy9YGwYFgR2BIIIZIxk2eoEJXoErKWABEBeBB4IHAoJUggECAUlDDgdFUwklQQoSRyYiCBIJARMaMAuBIThBCSgRGA1IESw3FRkEPQFuB5AuHoJlgQ4BKgF7gRk0pVqhDwoog3aMIpU6GjOqbwuYfY4KlWgYUIRpgWg8gUcLB3AVgyIJShkPjisOC4NggX/KOicyAgkyAQEHAgcOAwuBaJABgX0BAQ IronPort-Data: A9a23:Zjoogalkk4GqLiYNYJbp8Iro5gzRJ0RdPkR7XQ2eYbSJt1+Wr1Gzt xIbUW+AM/7ea2Ojeo13b9nl/BxX75CHmNMxSQVkr31jF1tH+JHPbTi7wugcHM8zwunrFh8PA xA2M4GYRCwMZiaC4Errav6+/SEUOZigHtLUEPTDNj16WThqQSIgjQMLs+Mii+aEu/Dha++2k Y20+ZK31GONgWYubDpFsfLb8XuDgdyr0N8mlg1mDRx0lAe2e0k9VPo3Oay3Jn3kdYhYdsbSb /rD1ryw4lTC9B4rDN6/+p6jGqHdauePVeQmoiM+t5mK2nCulARrukoIHKZ0hXNsttm8t4sZJ OOhGnCHYVxB0qXkwIzxWvTDes10FfUuFLTveRBTvSEPpqHLWyOE/hlgMK05Faok2OptHH5yz /8VEC4AdUqbl+7n5b3uH4GAhux7RCXqFJkUtnclyXTSCuwrBMiZBa7L/tRfmjw3g6iiH96HO JFfMmUpNkmdJUQUaz/7C7pm9AusrnX8fjlRqUOcjaE2+GPUigd21dABNfKIIIzXH58JxBrwS mTu+ESnRRgeBNak0RHf9HSHqr/tuACkR9dHfFG/3rsw6LGJ/UQUEBAQWF6xrPW1h0L7V99BJ kg8/is1sbN081SmSNT4VRC0rHOI+BkGVLJt//YS8gqBzO/Qpg2eHGVBFmQHY909v8hwTjsvv rOUo+7U6fVUmOX9YRqgGn2891te5QB9wbc+WBI5 IronPort-HdrOrdr: A9a23:47vigK9g6LnecSk+4cJuk+AAI+orL9Y04lQ7vn2ZhyY7TiX+rb HJoB17726StN9/YhAdcLy7VZVoBEmsl6KdgrNhWYtKIjOHhILAFugLhuHfKn/bakjDH4Vmu5 uIHZITNDSJNykYse/KpC+lDt0n3N6LtIqshevY0jNRaDsCUdAY0++8YTzraXGfg2J9dOIEKK Y= X-Talos-CUID: 9a23:Kc211GxlOpc5sc6PUnl+BgUzJuIpaGDC8E3TLnemBH0zUK2ZGHqfrfY= X-Talos-MUID: 9a23:7hKjiwr4UTAmmmYRuZUezzZCaoBh55SkM1gmy5MAkOXUFwlTZCjI2Q== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="528522424" Received: from rcdn-l-core-02.cisco.com ([173.37.255.139]) by rcdn-iport-6.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-02.cisco.com (Postfix) with ESMTPS id EC1CD1800023D; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 53D39CBEF6B; Mon, 28 Sep 2026 10:43:31 -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 10/13] python3-aiohttp: fix CVE-2026-54277 Date: Mon, 28 Sep 2026 10:43:20 -0700 Message-Id: <20260928174323.1810308-11-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-l-core-02.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, 28 Sep 2026 17:43:43 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130450 From: Darsh Kelaiya This patch applies the reviewed upstream fix shown in [1]. The advisory identifying the fix is referenced in [2]. 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/5ab61bb4cd88f19b712f12c7c9295fe262bf804d [2] https://nvd.nist.gov/vuln/detail/CVE-2026-54277 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-54277.patch | 96 +++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 97 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54277.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54277.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54277.patch new file mode 100644 index 0000000000..4042abe623 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54277.patch @@ -0,0 +1,96 @@ +From 83788a36c646a1bdfba912267feab5db796a828e Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Sun, 7 Jun 2026 00:33:03 -0500 +Subject: [PATCH] [PR #12826/36df6c13 backport][3.14] Enforce max_line_size on + fragmented request target and reason in C parser (#12837) + +CVE: CVE-2026-54277 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/5ab61bb4cd88f19b712f12c7c9295fe262bf804d] + +Backport Changes: +- Omitted generated `aiohttp/_http_parser.c` changes because the + Scarthgap recipe regenerates that file from `_http_parser.pyx` + during `do_configure`. +- This fix depends on the max_line_size buffer logic introduced by + CVE-2026-22815. Keep CVE-2026-22815.patch earlier in SRC_URI. + +(cherry picked from commit 5ab61bb4cd88f19b712f12c7c9295fe262bf804d) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12826.bugfix.rst | 1 + + aiohttp/_http_parser.pyx | 4 ++-- + tests/test_http_parser.py | 22 ++++++++++++++++++++++ + 3 files changed, 25 insertions(+), 2 deletions(-) + create mode 100644 CHANGES/12826.bugfix.rst + +diff --git a/CHANGES/12826.bugfix.rst b/CHANGES/12826.bugfix.rst +new file mode 100644 +index 000000000..7e095615d +--- /dev/null ++++ b/CHANGES/12826.bugfix.rst +@@ -0,0 +1 @@ ++Fixed the C HTTP parser not enforcing ``max_line_size`` on a request target or response reason phrase that is split across multiple reads; each fragment was checked on its own, so an accumulated line could exceed the limit without raising ``LineTooLong``. The accumulated length is now checked, matching the pure-Python parser -- by :user:`bdraco`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index 8e9ecae69..01a0f859c 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -718,7 +718,7 @@ cdef int cb_on_url(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + try: +- if length > pyparser._max_line_size: ++ if len(pyparser._buf) + length > pyparser._max_line_size: + status = pyparser._buf + at[:length] + raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) + extend(pyparser._buf, at, length) +@@ -733,7 +733,7 @@ cdef int cb_on_status(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + try: +- if length > pyparser._max_line_size: ++ if len(pyparser._buf) + length > pyparser._max_line_size: + reason = pyparser._buf + at[:length] + raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) + extend(pyparser._buf, at, length) +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 3e1f7dfaa..a724aae63 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -1161,6 +1161,17 @@ def test_http_request_max_status_line_under_limit(parser: HttpRequestParser) -> + assert msg.url == URL("/path" + path.decode()) + + ++def test_http_request_max_status_line_fragmented( ++ parser: HttpRequestParser, ++) -> None: ++ # Split an overlong request target across reads so that each callback ++ # fragment is under the limit but the accumulated target is not. ++ match = "400, message:\n Got more than 8190 bytes when reading" ++ with pytest.raises(http_exceptions.LineTooLong, match=match): ++ parser.feed_data(b"GET /" + b"a" * 8000) ++ parser.feed_data(b"a" * 8000 + b" HTTP/1.1\r\nHost: a\r\n\r\n") ++ ++ + def test_http_response_parser_utf8(response) -> None: + text = "HTTP/1.1 200 Ok\r\nx-test:тест\r\n\r\n".encode() + +@@ -1238,6 +1249,17 @@ def test_http_response_parser_status_line_under_limit( + assert msg.reason == reason.decode() + + ++def test_http_response_parser_status_line_too_long_fragmented( ++ response: HttpResponseParser, ++) -> None: ++ # Split an overlong reason phrase across reads so that each callback ++ # fragment is under the limit but the accumulated reason is not. ++ 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 " + b"a" * 8000) ++ response.feed_data(b"a" * 8000 + b"\r\n\r\n") ++ ++ + def test_http_response_parser_bad_version(response) -> None: + with pytest.raises(http_exceptions.BadHttpMessage): + response.feed_data(b"HT/11 200 Ok\r\n\r\n") +-- +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 42402f799b..98dd5363db 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 @@ -31,6 +31,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-50269.patch \ file://CVE-2026-54274.patch \ file://CVE-2026-54275.patch \ + file://CVE-2026-54277.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:21 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: 99494 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 4C0C5CA5FA6 for ; Mon, 28 Sep 2026 17:43:41 +0000 (UTC) Received: from rcdn-iport-8.cisco.com (rcdn-iport-8.cisco.com [173.37.86.79]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63553.1790617415702148386 for ; Mon, 28 Sep 2026 10:43:36 -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=YynaORwi; spf=pass (domain: cisco.com, ip: 173.37.86.79, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=9263; q=dns/txt; s=iport01; t=1790617416; x=1791827016; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=uUgnKloZRHaW0sdezHaEz0kdJgm43hLNFImJVb3Dg1o=; b=YynaORwiybTNVteCNBWwf4Lig7F2lXx5+PYfJ4lXwFH/oJs/Le/YIXR1 QJRiVBuhmwJO0ZimBaQqMcoTzmVQo+BrmHw+3I+hD230xAFKZGHUodX5N JgiqyvnZ/ZRreKlISbEneNUkia+/hZHLNIPlHK3aa39VH20hzWxyWmXEG RQV4xgyeePKwntLOuHho5rqK/07IbiPjO7U9mYV4tFCLxGYvKSJwnrAuM L09Bo6TPYv0SEG+sNVSTxXgoYIYD4LVMTSU3GPfI0NOM/38UpnyEfV+gq UGX2wBSizT3tabzhqY7qZz5q5HdJoQuQ/pCbXTVd4JYHEahui+X1txG7O g==; X-CSE-ConnectionGUID: pwF1Ui+GSYm9GnCMBv+TuA== X-CSE-MsgGUID: kg3gkfJtSleJMnORHkEBDA== X-IPAS-Result: A0BJAgC9prpq/47/Ja1aHgEBCxIMggULghg/dWBDSZZKA54agX4PAQEBD0QNBAEBhD9GAo4JAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQEKAQEFAQEBAgEHBYEOE4ZPDZASAQIBAx0KCwEYAS0QHAMBAi8rIwgQCYIqWAGCdAMRwkWBeTOBAYMpAT8CQ1DbMgELFAGBOIVAiCNdGAGEfCcbG4FyglCBOHaBBYFcAQGBOBCGXQSCIoEMgVqBA4c3iwlIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQcbBgWBHYEgghkjGTZ6gQlegSspYAEQF4EHggcCglSCAQIBSUMOB0VTCSVBChJHJiIIEgkBExowC4EhOEEJKBEYDUgRLDcVGQQ+bgeQLh6CZYEOASoBgTQ1Kx4WMqUooQ8KKIN2jCKVOhozhASBV5JAklQLmH2OCpU9KxhQhGmBaDyBRwsHcBU7gmcJShkPjisDCwuDYIF/g2XGVScyAgkyAQEHAgcOAwuBaJABgX0BAQ IronPort-Data: A9a23:nnwkzKBBIp9S/RVW/3riw5YqxClBgxIJ4kV8jS/XYbTApGxw1zZSx zMcWWDTaamIZ2SgLd92PN/lpEIAvZKBx9ViOVdlrnsFo1CmBibm6XV1Cm+qYkt+++WaFBoPA /02M4eGcYZsCCCM/n9BC5C5xVFkz6aEW7HgP+DNPyF1VGdMRTwo4f5Zs7ZRbrVA357jXGthh fuo+5eBYA7/i2YtWo4pw/vrRC1H7ayaVAww5jTSVdgT1HfCmn8cCo4oJK3ZBxPQXolOE+emc P3Ixbe/83mx109F5gSNy+uTnuUiG9Y+DCDW4pZkc/HKbitq+kTe5p0G2M80Mi+7vdkmc+dZk 72hvbToIesg0zaldO41C3G0GAkmVUFKFSOuzXWX6aSuI0P6n3TE2+pAVXw3fqIjy+MmLmdl0 NYGawwQR0XW7w626OrTpuhEnM8vKozveYgYoHwllWyfBvc9SpeFSKLPjTNa9G5v3YYVQrCEO pdfMGY1BPjDS0Un1lM/BJEzmO6pl3DXeDxDo1XTrq0yi4TW5FEriOS3aoSOJLRmQ+1WkR2Xp Wzk41/9DzM/Mvul6Tmo2F6F07qncSTTHdh6+KeD3vlyjVuew2YeBBEbWR63rOe0jma6WslDM AoT4icooK04+UCnQ9W7WAe3yENopTYGUNZWVul/4waXx++MvkCSB3MPSXhKb9lOWNIKeAHGH 2Shx7vBbQGDepXMFBpxKp/8QeuOBBUo IronPort-HdrOrdr: A9a23:iLv/Va2CiIcIUyY6kUBL8gqjBJAkLtp133Aq2lEZdPWaSKOlfq eV7ZMmPHDP6Qr5NEtMpTnEAtjjfZq+z+8Q3WBuB9eftWDd0QPCRr2Kr7GSpgEIcBeRygcy78 tdmtBFeb7NJGk/q9rm6w+lFNtl6tyG/Ke0wdr69R5WPGdXg2UK1XYANu5deXcGPTV7OQ== X-Talos-CUID: 9a23:795SH2PVZBAOcO5DQDZJrXMVWe8eQ3iM43H7BmWmSlp0YejA X-Talos-MUID: 9a23:uLEfnwRXvcRytV1ZRXTvi2ptN8lK/5+qBWIol7I8m8qpHwBZbmI= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="520422636" Received: from rcdn-l-core-05.cisco.com ([173.37.255.142]) by rcdn-iport-8.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-05.cisco.com (Postfix) with ESMTPS id ED08218000201; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 59423CBEF6C; Mon, 28 Sep 2026 10:43:31 -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 11/13] python3-aiohttp: fix CVE-2026-54278 Date: Mon, 28 Sep 2026 10:43:21 -0700 Message-Id: <20260928174323.1810308-12-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-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, 28 Sep 2026 17:43:41 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130458 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/4f7480e474cccc6a8cc2c92ad3f17a31dedf8232 [2] https://nvd.nist.gov/vuln/detail/CVE-2026-54278 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-54278.patch | 209 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 210 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54278.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54278.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54278.patch new file mode 100644 index 0000000000..f5ab4f775b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54278.patch @@ -0,0 +1,209 @@ +From 804b9d48880222f72b98943a5ecf06d98b1c5074 Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Sun, 7 Jun 2026 00:39:29 -0500 +Subject: [PATCH] [PR #12828/13b635d7 backport][3.14] Bounded unread compressed + drain (#12845) + +CVE: CVE-2026-54278 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/4f7480e474cccc6a8cc2c92ad3f17a31dedf8232] + +Backport Changes: +- aiohttp 3.9.5's earlier decompression backport can buffer + one decompressed chunk larger than the read buffer. + In addition to snapshotting the initial chunk count, cap an + unbounded read at StreamReader._high_water so one such + chunk cannot be returned as a multi-megabyte bytes object. +- aiohttp 3.9.5 does not expose DEFAULT_CHUNK_SIZE. Use its equivalent + 64 KiB read-buffer value directly in the functional test. +- Import the existing AiohttpClient and AiohttpServer aliases from + aiohttp.pytest_plugin because this branch's test module did not + already import them. +- Mark the mock protocol paused in + test_readany_does_not_drain_reentrant_refill because aiohttp 3.9.5 + gates low-water resume_reading() calls on that state. +- Send the functional test's body after receiving the early 401 + response so aiohttp 3.9.5 deterministically processes it through + lingering cleanup. + +(cherry picked from commit 4f7480e474cccc6a8cc2c92ad3f17a31dedf8232) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12828.bugfix.rst | 1 + + aiohttp/streams.py | 23 ++++++++++--- + tests/test_streams.py | 29 ++++++++++++++++ + tests/test_web_functional.py | 64 ++++++++++++++++++++++++++++++++++++ + 4 files changed, 112 insertions(+), 5 deletions(-) + create mode 100644 CHANGES/12828.bugfix.rst + +diff --git a/CHANGES/12828.bugfix.rst b/CHANGES/12828.bugfix.rst +new file mode 100644 +index 000000000..9893577a5 +--- /dev/null ++++ b/CHANGES/12828.bugfix.rst +@@ -0,0 +1 @@ ++Fixed :meth:`~aiohttp.StreamReader.readany` and :meth:`~aiohttp.StreamReader.read_nowait` joining data fed back into the buffer during the call (when draining below the low water mark resumes reading) into a single unbounded :class:`bytes`; a call now returns only the chunks that were buffered when it started, keeping the drain of an unread auto-decompressed request body bounded by the read buffer -- by :user:`bdraco`. +diff --git a/aiohttp/streams.py b/aiohttp/streams.py +index dffaf374b..0d4d633ef 100644 +--- a/aiohttp/streams.py ++++ b/aiohttp/streams.py +@@ -526,14 +526,27 @@ class StreamReader(AsyncStreamReaderMixin): + """Read not more than n bytes, or whole buffer if n == -1""" + self._timer.assert_timeout() + +- chunks = [] +- while self._buffer: +- chunk = self._read_nowait_chunk(n) +- chunks.append(chunk) +- if n != -1: ++ if n == -1: ++ # Drain only chunks present now; _read_nowait_chunk() can ++ # re-entrantly resume_reading() and refill the buffer. ++ count = len(self._buffer) ++ n = self._high_water ++ chunks = [] ++ for _ in range(count): ++ chunk = self._read_nowait_chunk(n) ++ chunks.append(chunk) + n -= len(chunk) + if n == 0: + break ++ return b"".join(chunks) ++ ++ chunks: list[bytes] = [] ++ while self._buffer: ++ chunk = self._read_nowait_chunk(n) ++ chunks.append(chunk) ++ n -= len(chunk) ++ if n == 0: ++ break + + return b"".join(chunks) if chunks else b"" + +diff --git a/tests/test_streams.py b/tests/test_streams.py +index 2076bc9ba..e35dc0709 100644 +--- a/tests/test_streams.py ++++ b/tests/test_streams.py +@@ -1721,3 +1721,32 @@ async def test_stream_reader_small_limit_resumes_reading( + + protocol.resume_reading.assert_called() + assert protocol._reading_paused is False ++ ++ ++async def test_readany_does_not_drain_reentrant_refill( ++ protocol: mock.Mock, ++) -> None: ++ """A single readany() must not reassemble data fed re-entrantly. ++ ++ Draining below the low water mark resumes reading, which can synchronously ++ refill the buffer (e.g. decompressing another chunk). Joining that refill in ++ one call would reassemble an unbounded body. ++ """ ++ loop = asyncio.get_running_loop() ++ stream = streams.StreamReader(protocol, limit=4, loop=loop) ++ ++ refills = [b"second", b"third"] ++ ++ def resume_reading() -> None: ++ if refills: ++ stream.feed_data(refills.pop(0)) ++ ++ protocol.resume_reading.side_effect = resume_reading ++ ++ protocol._reading_paused = True ++ stream.feed_data(b"first") ++ ++ # Popping "first" refills "second", but this readany() returns only "first". ++ assert await stream.readany() == b"first" ++ assert await stream.readany() == b"second" ++ assert await stream.readany() == b"third" +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index 96dcd1c98..e6d01bffd 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -4,6 +4,7 @@ import json + import pathlib + import socket + import zlib ++from contextlib import suppress + from typing import Any, Optional + from unittest import mock + +@@ -22,6 +23,8 @@ from aiohttp import ( + web, + ) + from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING ++from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer ++from aiohttp.streams import StreamReader + from aiohttp.test_utils import make_mocked_coro + from aiohttp.typedefs import Handler + +@@ -1617,6 +1620,67 @@ async def test_response_prepared_with_clone(aiohttp_client) -> None: + await resp.release() + + ++@pytest.mark.parametrize("decompressed_size", [4 * 1024 * 1024, 32 * 1024 * 1024]) ++async def test_unread_compressed_body_drain_is_bounded( ++ aiohttp_server: AiohttpServer, ++ monkeypatch: pytest.MonkeyPatch, ++ decompressed_size: int, ++) -> None: ++ """Draining an unread compressed body stays bounded by the read buffer. ++ ++ A handler that rejects before reading still drains the payload during ++ lingering close; a small compressed body must not force a large transient ++ allocation (a deflate-bomb style DoS). ++ """ ++ drain_reads: list[int] = [] ++ drained = asyncio.Event() ++ readany = StreamReader.readany ++ ++ async def record_readany(self: StreamReader) -> bytes: ++ data = await readany(self) ++ assert data ++ drain_reads.append(len(data)) ++ drained.set() ++ return data ++ ++ monkeypatch.setattr(StreamReader, "readany", record_readany) ++ ++ async def handler(request: web.Request) -> web.Response: ++ return web.Response(status=401) ++ ++ app = web.Application(client_max_size=1024) ++ app.router.add_post("/", handler) ++ server = await aiohttp_server(app) ++ ++ body = zlib.compress(b"a" * decompressed_size) ++ assert len(body) < decompressed_size ++ head = ( ++ b"POST / HTTP/1.1\r\n" ++ b"Host: localhost\r\n" ++ b"Content-Encoding: deflate\r\n" ++ b"Content-Length: %d\r\n" ++ b"Connection: keep-alive\r\n\r\n" ++ ) % len(body) ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write(head) ++ await writer.drain() ++ status_line = await asyncio.wait_for(reader.readline(), 5) ++ assert status_line.startswith(b"HTTP/1.1 401 ") ++ writer.write(body) ++ await writer.drain() ++ await asyncio.wait_for(drained.wait(), 5) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ # Bounded by the buffer, not the decompressed size. ++ assert max(drain_reads) <= 3 * 2**16 ++ assert max(drain_reads) < decompressed_size ++ ++ + async def test_app_max_client_size(aiohttp_client) -> None: + async def handler(request): + await request.post() +-- +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 98dd5363db..f91c9cb181 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 @@ -32,6 +32,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-54274.patch \ file://CVE-2026-54275.patch \ file://CVE-2026-54277.patch \ + file://CVE-2026-54278.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:22 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: 99502 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 66B86CA5FAF for ; Mon, 28 Sep 2026 17:43:43 +0000 (UTC) Received: from rcdn-iport-6.cisco.com (rcdn-iport-6.cisco.com [173.37.86.77]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63550.1790617413037125375 for ; Mon, 28 Sep 2026 10:43:33 -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=S7SllN+d; spf=pass (domain: cisco.com, ip: 173.37.86.77, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=13950; q=dns/txt; s=iport01; t=1790617413; x=1791827013; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=94LC1+SnAj5vjlD7Axwacgh09OOQlWG1wJj2zdEcgLc=; b=S7SllN+dkxST1KUP6DTEpmkmBYcXoqHkyXnG075j55AOnAuwyZvjP9TM GoOmV5Sp1oLXo0KcIIFs4aR22cIISP6TY/UXRhfK6RcnFrg2xS8n8nhOX YC8H9uUVbn9JCi7U89a8ANlS3+Ff8qkKeh+kB9AqnflPlJUQdE/wLv6V8 jbNSZS36W1F/OfA+p7vUzJlBwkzxvyf974jHQKcX0BgMRYoSwNP+ajrQT kA/LhihxnwmJVpSPrRDArKTHc3Q0wP2GcwRWdbpp/dJIyv8YMEXflsvd7 GLkH1heKpHZs7dXwON6DZz/wR/hmgzd5fwDtrVOnahTcxOhvWHVikdncs A==; X-CSE-ConnectionGUID: LyUu0gA/RA+jxp8wbdijFg== X-CSE-MsgGUID: qNSTTm7OTZG81ykwkxZaEg== X-IPAS-Result: A0BKAgC9prpq/4v/Ja1aglmCV3VgQ0kDlkcDkUmMUYF+DwEBAQ9EDQQBAYQ/RgKOCQImNAkOAQIEAwIDAQEBAQEBAQEBAQEBCgEBBQEBAQIBBwWBDhOGTw2QEgECAQMnCwEYAS0QHAMBAhcBFysjCBmDAgGCdAMRwkWBeTOBAYMpAT8CQ1DbMgELFAGBOIVAiCNdGAGEfCcbG4FyglCCLoEFgVwBAYIoAoV7BIIigQyBWoFNkXZIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQcbBgWBHYEgghkjGTZ6gQlegSspYAEQF4EHggcCglSCAQIBSUMOB0VTCSVBChJHJiIIEgkBExowC4EhOEEJKBEYDUgRLDcVGQQ+bgeQLh6Bc3ItEEQNASsEgQ0HCFgTAgc0BhiScgyQHYIhgTWfWgoog3aMIo4Nhy0aM4VbpRQLmH2JAYI2glOKDYtJEmiEaYFoPIFHCwdwFYMiCUoZD1iNVgsLg2CBf4NlxlUnMgIIATIBAQcCBw4DC4FokAItb2ABAQ IronPort-Data: A9a23:wLqog6oarnZ35K/gOHsvVK8wO41eBmJIZBIvgKrLsJaIsI4StFCzt garIBmDPPeIajD1KY93aN/kpBgEupHTy9FnSFQ+pX83RXxG8OPIVI+TRqvS04x+DSFioGZPt Zh2hgzodZhsJpPkjk7zdOWn9T8jhfngqoPUUIbsIjp2SRJvVBAvgBdin/9RqoNziLBVOSvV0 T/Ji5OZYgDNNwJcaDpOtfrS8kM35ZwehRtB1rAATaET1LPhvyF94KI3fcmZM3b+S49IKe+2L 86r5K255G7Q4yA2AdqjlLvhGmVSKlIFFVHT4pb+c/HKbilq/kTe4I5iXBYvQRs/ZwGyojxE4 I4lWapc5useFvakdOw1C3G0GszlVEFM0OevzXOX6aR/w6BaGpfh660GMa04AWEX0sJmPHwW+ tVfEhlObACb1/28zLO5ZOY506zPLOGzVG8ekmtrwTecCbMtRorOBv2Qo9RZxzw3wMtJGJ4yZ eJANmEpN0uGOUASfA5MWfrSn8/w7pX7WzFbpVacpLs+y2PS1wd2lrPqNbI5f/TXH5UIxBzE/ D+uE2LRGzchCOCalh25wF383/7MgiDraos/G+jtnhJtqBjJroAJMzURTVa9rPyzh0KyVt4aJ 0EK9y4Gqakp6FftScHwWRC9qnOIshMQHd1KHIUHBBql0KHY5UOdQ2MDVDMEMIZgv84tTjts3 ViM9z/0OQFSXHSuYSr13t+pQfmaYkD58Udqifc4cDY4 IronPort-HdrOrdr: A9a23:puDcO6HPXfQjq7COpLqEyseALOsnbusQ8zAXPo5KJiC9Ffbo8P xG88576faZslsssTQb6LK90cq7MBfhHOBOgbX5VI3KNGKNhILrFvAG0WKI+VPd8kPFmtK1rZ 0QEJRWOZnXEUVwi9r87U2TFtYtx8TCzYWT7N2uqUuEiWpRGtldB8ATMHfjLnFL X-Talos-CUID: 9a23:HbiZo2h7ucHw8rdbNW08OjjzrDJuVmzg0S/fCgiCICVrWKXFVlWc+aFKnJ87 X-Talos-MUID: 9a23:5/uiuQt0sUfDJjDrSc2nnRdZCPttvIOSOm9dyIQFpMqUByVdNGLI X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="528522425" Received: from rcdn-l-core-02.cisco.com ([173.37.255.139]) by rcdn-iport-6.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-02.cisco.com (Postfix) with ESMTPS id EFA471800034A; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 5E9F1CBEF6D; Mon, 28 Sep 2026 10:43:31 -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 12/13] python3-aiohttp: fix CVE-2026-54279 Date: Mon, 28 Sep 2026 10:43:22 -0700 Message-Id: <20260928174323.1810308-13-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-l-core-02.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, 28 Sep 2026 17:43:43 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130451 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/a329a7aacad5284f087af36103aff778746da0f2 [2] https://nvd.nist.gov/vuln/detail/CVE-2026-54279 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-54279.patch | 317 ++++++++++++++++++ .../python/python3-aiohttp_3.9.5.bb | 1 + 2 files changed, 318 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54279.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54279.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54279.patch new file mode 100644 index 0000000000..8c735c209b --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54279.patch @@ -0,0 +1,317 @@ +From 7b86b0ebf15a848e2c8e61ff58186b3bd340edfb Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Sun, 7 Jun 2026 00:40:24 -0500 +Subject: [PATCH] [PR #12824/60b85e98 backport][3.14] Preserve host-only cookie + scope across CookieJar save/load (#12833) + +CVE: CVE-2026-54279 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/a329a7aacad5284f087af36103aff778746da0f2] + +Backport Changes: +- Adapted regression tests to aiohttp 3.9.5's private + `_host_only_cookies` state because the newer public + `host_only_cookies` property is absent. +- Used `SimpleCookie` with `update_cookies()` because aiohttp 3.9.5 + predates the `update_cookies_from_headers()` test API. +- This fix depends on the JSON CookieJar persistence implementation + introduced by CVE-2026-34993.patch. Keep CVE-2026-34993.patch + earlier in SRC_URI. + +(cherry picked from commit a329a7aacad5284f087af36103aff778746da0f2) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12824.bugfix.rst | 1 + + aiohttp/cookiejar.py | 52 +++++++++----- + tests/test_cookiejar.py | 144 +++++++++++++++++++++++++++++++++++++++ + 3 files changed, 180 insertions(+), 17 deletions(-) + create mode 100644 CHANGES/12824.bugfix.rst + +diff --git a/CHANGES/12824.bugfix.rst b/CHANGES/12824.bugfix.rst +new file mode 100644 +index 000000000..f8dbd169c +--- /dev/null ++++ b/CHANGES/12824.bugfix.rst +@@ -0,0 +1 @@ ++Fixed :class:`~aiohttp.CookieJar` dropping the host-only flag of cookies when persisted with :meth:`~aiohttp.CookieJar.save` and reloaded with :meth:`~aiohttp.CookieJar.load`, so a cookie set without a ``Domain`` attribute is again scoped to the exact host that set it after a reload; the absolute expiration deadline is now persisted as well, so a reloaded cookie keeps its original lifetime instead of being rescheduled from the load time. :meth:`~aiohttp.CookieJar.load` now replaces the jar contents rather than merging onto prior state, and loaded cookies pass through the same acceptance rules as :meth:`~aiohttp.CookieJar.update_cookies`, so a cookie for an IP-address host is dropped when loaded into a jar created without ``unsafe=True`` -- by :user:`bdraco`. +diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py +index 376f7597a..4f694b6d7 100644 +--- a/aiohttp/cookiejar.py ++++ b/aiohttp/cookiejar.py +@@ -36,6 +36,9 @@ __all__ = ("CookieJar", "DummyCookieJar") + + CookieItem = Union[str, "Morsel[str]"] + ++# Not persisted; the absolute deadline is saved instead. ++_RELATIVE_EXPIRY_ATTRS = frozenset(("max-age", "expires")) ++ + + class _RestrictedCookieUnpickler(pickle.Unpickler): + """A restricted unpickler that only allows cookie-related types. +@@ -146,21 +149,28 @@ class CookieJar(AbstractCookieJar): + :class:`str` or :class:`pathlib.Path` instance. + """ + file_path = pathlib.Path(file_path) +- data: dict[str, dict[str, dict[str, str | bool]]] = {} ++ data: dict[str, dict[str, dict[str, str | bool | float]]] = {} + 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] = { ++ morsel_data: dict[str, str | bool | float] = { + "key": morsel.key, + "value": morsel.value, + "coded_value": morsel.coded_value, + } +- # Save all morsel attributes that have values ++ # Skip relative expiry; the absolute deadline is saved below. + for attr in morsel._reserved: # type: ignore[attr-defined] ++ if attr in _RELATIVE_EXPIRY_ATTRS: ++ continue + attr_val = morsel[attr] + if attr_val: + morsel_data[attr] = attr_val ++ # Persist or it reloads as a domain cookie and leaks to subdomains. ++ if (domain, name) in self._host_only_cookies: ++ morsel_data["host_only"] = True ++ if (exp := self._expirations.get((domain, path, name))) is not None: ++ morsel_data["expires_timestamp"] = exp + data[key][name] = morsel_data + with file_path.open(mode="w", encoding="utf-8") as f: + json.dump(data, f, indent=2) +@@ -172,6 +182,9 @@ class CookieJar(AbstractCookieJar): + pickle format (using a restricted unpickler) for backward + compatibility with existing cookie files. + ++ Replaces the current jar contents; loaded cookies pass through the ++ same acceptance rules as :meth:`update_cookies`. ++ + :param file_path: Path to file from where cookies will be + imported, :class:`str` or :class:`pathlib.Path` instance. + """ +@@ -180,32 +193,28 @@ class CookieJar(AbstractCookieJar): + try: + with file_path.open(mode="r", encoding="utf-8") as f: + data = json.load(f) +- self._cookies = self._load_json_data(data) ++ 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) ++ self, data: dict[str, dict[str, dict[str, str | bool | float]]] ++ ) -> None: ++ """Replace contents, routing cookies through update_cookies().""" ++ self.clear() + 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, ++ "key": morsel_data["key"], ++ "value": morsel_data["value"], ++ "coded_value": morsel_data["coded_value"], + } + ) + # Restore morsel attributes +@@ -216,8 +225,17 @@ class CookieJar(AbstractCookieJar): + "coded_value", + ): + morsel[attr] = morsel_data[attr] +- cookies[key][name] = morsel +- return cookies ++ # Drop the domain so update_cookies() re-marks it host-only. ++ if morsel_data.get("host_only"): ++ morsel["domain"] = "" ++ response_url = ( ++ URL.build(scheme="https", host=domain) if domain else URL() ++ ) ++ self.update_cookies({name: morsel}, response_url) ++ # Restore the absolute deadline; update_cookies() schedules none. ++ if (exp := morsel_data.get("expires_timestamp")) is not None: ++ self._expire_cookie(float(exp), domain, path, name) ++ self._do_expiration() + + 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 bdc5cfd72..df59759c5 100644 +--- a/tests/test_cookiejar.py ++++ b/tests/test_cookiejar.py +@@ -1,6 +1,7 @@ + import asyncio + import datetime + import itertools ++import json + import pathlib + import pickle + import unittest +@@ -979,6 +980,149 @@ async def test_save_load_json_roundtrip( + assert saved_cookies == loaded_cookies + + ++async def test_save_load_json_preserves_host_only_scope(tmp_path: Path) -> None: ++ """Verify save/load keeps host-only cookies off subdomains.""" ++ file_path = tmp_path / "host_only.json" ++ issuer = URL("https://auth.example.com/login") ++ subdomain = URL("https://sub.auth.example.com/") ++ ++ jar_save = CookieJar() ++ jar_save.update_cookies({"sid": "hostonly"}, response_url=issuer) ++ assert "sid" not in jar_save.filter_cookies(subdomain) ++ jar_save.save(file_path=file_path) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ assert jar_load._host_only_cookies == {("auth.example.com", "sid")} ++ assert "sid" not in jar_load.filter_cookies(subdomain) ++ assert "sid" in jar_load.filter_cookies(issuer) ++ ++ ++async def test_save_load_json_domain_cookie_still_matches_subdomain( ++ tmp_path: Path, ++) -> None: ++ """Verify save/load keeps an explicit Domain cookie valid for subdomains.""" ++ file_path = tmp_path / "domain.json" ++ subdomain = URL("https://sub.example.com/") ++ ++ jar_save = CookieJar() ++ jar_save.update_cookies( ++ SimpleCookie("sid=domaincookie; Domain=example.com"), ++ URL("https://example.com/"), ++ ) ++ jar_save.save(file_path=file_path) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ assert jar_load._host_only_cookies == set() ++ assert "sid" in jar_load.filter_cookies(subdomain) ++ ++ ++async def test_save_load_json_preserves_max_age_deadline(tmp_path: Path) -> None: ++ """Verify save/load restores the absolute deadline without resetting it.""" ++ file_path = tmp_path / "max_age.json" ++ url = URL("https://example.com/") ++ ++ jar_save = CookieJar() ++ jar_save.update_cookies( ++ SimpleCookie("sid=x; Max-Age=3600; Domain=example.com"), url ++ ) ++ expirations = dict(jar_save._expirations) ++ jar_save.save(file_path=file_path) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ # The deadline is restored as the original absolute time, not now + Max-Age. ++ assert dict(jar_load._expirations) == expirations ++ assert "sid" in jar_load.filter_cookies(url) ++ ++ ++async def test_save_load_json_drops_expired_cookie(tmp_path: Path) -> None: ++ """Verify a cookie whose persisted deadline is in the past is dropped on load.""" ++ file_path = tmp_path / "expired.json" ++ url = URL("https://example.com/") ++ ++ # Save a future-expiring cookie, then rewrite its persisted deadline to the ++ # past so the cookie survives save() and the drop happens on the load path. ++ jar_save = CookieJar() ++ jar_save.update_cookies( ++ SimpleCookie( ++ "sid=x; Expires=Tue, 1 Jan 2999 12:00:00 GMT; Domain=example.com" ++ ), ++ url, ++ ) ++ jar_save.save(file_path=file_path) ++ data = json.loads(file_path.read_text()) ++ _, cookies = next(iter(data.items())) ++ cookies["sid"]["expires_timestamp"] = 0.0 ++ file_path.write_text(json.dumps(data)) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ assert len(jar_load) == 0 ++ assert "sid" not in jar_load.filter_cookies(url) ++ ++ ++async def test_save_load_json_preserves_expires_deadline(tmp_path: Path) -> None: ++ """Verify a future Expires deadline survives a save/load roundtrip.""" ++ file_path = tmp_path / "expires.json" ++ url = URL("https://example.com/") ++ ++ jar_save = CookieJar() ++ jar_save.update_cookies( ++ SimpleCookie( ++ "sid=x; Expires=Tue, 1 Jan 2999 12:00:00 GMT; Domain=example.com" ++ ), ++ url, ++ ) ++ expirations = dict(jar_save._expirations) ++ jar_save.save(file_path=file_path) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ assert dict(jar_load._expirations) == expirations ++ assert "sid" in jar_load.filter_cookies(url) ++ ++ ++async def test_load_json_old_format_without_new_keys(tmp_path: Path) -> None: ++ """Verify a file written by an older version (no host_only/expires_timestamp) loads.""" ++ file_path = tmp_path / "old.json" ++ # Old schema: no host_only, no expires_timestamp; relative max-age morsel attr. ++ file_path.write_text( ++ json.dumps( ++ { ++ "example.com|/": { ++ "sid": { ++ "key": "sid", ++ "value": "x", ++ "coded_value": "x", ++ "domain": "example.com", ++ "max-age": "3600", ++ } ++ } ++ } ++ ) ++ ) ++ url = URL("https://example.com/") ++ subdomain = URL("https://sub.example.com/") ++ ++ jar_load = CookieJar() ++ # No exception when the new keys are absent. ++ jar_load.load(file_path=file_path) ++ ++ # The old schema has a domain field but no host_only marker, so a cookie ++ # originally set without Domain is indistinguishable from a domain cookie. ++ assert "sid" in jar_load.filter_cookies(url) ++ assert "sid" in jar_load.filter_cookies(subdomain) ++ # max-age is rescheduled from load time rather than an absolute deadline. ++ assert any(key[2] == "sid" for key in jar_load._expirations) ++ ++ + async def test_json_format_is_safe(tmp_path: Path) -> None: + """Verify the JSON file format cannot execute code on load.""" + import json +-- +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 f91c9cb181..88ae49dec4 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 @@ -33,6 +33,7 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-54275.patch \ file://CVE-2026-54277.patch \ file://CVE-2026-54278.patch \ + file://CVE-2026-54279.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows" From patchwork Mon Sep 28 17:43:23 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: 99504 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 3B9C1CA5FA1 for ; Mon, 28 Sep 2026 17:43:44 +0000 (UTC) Received: from rcdn-iport-2.cisco.com (rcdn-iport-2.cisco.com [173.37.86.73]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.63547.1790617412808715790 for ; Mon, 28 Sep 2026 10:43:33 -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=VUawnaeF; spf=pass (domain: cisco.com, ip: 173.37.86.73, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=37452; q=dns/txt; s=iport01; t=1790617413; x=1791827013; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=xnKXtIfCadqQPvj2ZLsr2kILT8JKxs6XpuMJAGU13Vk=; b=VUawnaeFA7YJfeazJmyXS+iYB16RTXqByo0SvPuiAxDfbohfsS/4AFDH 5QPiOK7nYpQzeSSfOwq8p3keSP6RdhO+Ph2vrb4fLmz8Tq2qcFl2SfXmm OifHi10AK8OLjDCmqtsNDZ3v/Tp+XNCJiYLnHYnPRZDSvBLVyd6KCpYsl WFpUK1QQOVJt83Syrm8f08+PEsuvDfc2cycQ45BnIOHfyKuxp7eAWVDud EUnEr6XW2bngNovCn/fSHmfdq3UuFrx4GkNnvcKUtAjw7FfU7bs+trZ/a 3kGDrj1XnQG9hr779LjOG5UC6vA2X8PNwv3apoJIsTJH2htbEBz7J7Pem w==; X-CSE-ConnectionGUID: ocO5xfVIR52+NA8FBQKIlg== X-CSE-MsgGUID: GEKqcl3/Rx24sVeutF44bg== X-IPAS-Result: A0D1AwBOprpq/4r/Ja1aglmCGD91XAQaKUmWSgOLZIVljFGBfg8BAQEPRA0EAQGEP0YCjgkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDGgEMCwEYAS0QHAMBAi8gCyMIGYIqWAGCOgM3AxHCV4F5M4EBgykBPwJDUNhLDYJaAQsUAYE4hUCCf4UkXRgBg2uBEScbG4FygRWBO4IugQWBGkIBAYE4EAmGVASCIoEMgVqBA5JASIEeA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+F4EHGwYFgR2BIIIZIxk2eoEJXoErKWABEBeBB4IHAoJUggECAUlDDgdFUwklQQoSRyYiCBIJARMaMAuBIThBCSgRGA1IESw3FRkEPm4HkC4eglMLBgEBFRswLQEHDBcBBHsGFQYTMggWARApBQYCMJJoAQFCApF6oB5xCiiDdowijz6FfBozhASBV5JAklQLmH2OCoQJkV8LDQNNhGmBaDyBRwsHcBU7gmcJShkPjisDCwuDYIF/yjonMgIJMgEBBwIHDgMLgWiEY4seASaBVgEB IronPort-Data: A9a23:h9wXY6u1eB9bjBUpBQyGWFCPnufnVAZfMUV32f8akzHdYApBsoF/q tZmKTrXO6yIZTCmf4tyadvgoBtUvpHWmIM2TwNurn8wFCpEgMeUXt7xwmUckM+xwmwvaGo9s q3yv/GZdJhcokf0/0nrb/656yYsjclkf5KkYMbcICd9WAR4fykojBNnioYRj5Vh6TSDK1vlV eja/YuFZzdJ5xYuajhKs/PZ+Es21BjPkGpwUmIWNKgjUGD2zxH5PLpHTYmtIn3xRJVjH+LSb 47r0LGj82rFyAwmA9Wjn6yTWhVirmn6ZFXmZtJ+AsBOszAazsAA+v9T2Mk0NS+7vw60c+VZk 72hg3AfpTABZcUgkMxFO/VR/roX0aduoNcrKlDn2SCfItGvn3bEm51T4E8K0YIw/e1QXHFAy 90idWpOSDnTqciP45O/Rbw57igjBJGD0II3oHpsy3TdSP0hW52GG/yM7t5D1zB2jcdLdRrcT 5NGMnw0M1KaPkAJYwxHYH49tL/Aan3Xfz5VrFuUtKMf6GnIxws327/oWDbQUoHTGZ4MzxnH/ woq+UzWLzIDP9jEjgCVqC63uOLSgQ2nBIgdQejQGvlCxQf7KnYoIBoOWF22pPO0hkKzV5dTJ lIZ/gIqrLMu7wqsVtT7UhiyrXKIsxJaXMBfe9DW8ymXwabSpgLcDW8eQ3sZMZottdQ9Qnoh0 Vrhc87VOAGDeYa9ERq1nop4ZxvrUcTJBQfuvRM5cDY= IronPort-HdrOrdr: A9a23:JNHy46DT5WYeMb/lHemO55DYdb4zR+YMi2TDGXofdfUzSL3+qy nAppUmPHPP5Qr5HUtQ++xoW5PwJU80i6QU3WB5B97LN2PbUSmTXeRfBODZrQEIdReTygck79 YCT4FOTPvtEFN9kcH2pCO8E9om3Z271ZrAv5a585+oJjsaE52JKGxCe3+mLnE= X-Talos-CUID: 9a23:Xz2pc2vQoKvjg1h/IiZ1UWn96IsieVqA3HnuAnXlUyVPYe27Y2az2adrxp8= X-Talos-MUID: 9a23:kXdUYAXb9Pzvg2Lq/BDBpyhpMJwv2bT0VFsznIgalZXYPyMlbg== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,128,1787011200"; d="scan'208";a="514319771" Received: from rcdn-l-core-01.cisco.com ([173.37.255.138]) by rcdn-iport-2.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 28 Sep 2026 17:43:32 +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 rcdn-l-core-01.cisco.com (Postfix) with ESMTPS id F39541800029F; Mon, 28 Sep 2026 17:43:31 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 65311CBEF6F; Mon, 28 Sep 2026 10:43:31 -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 13/13] python3-aiohttp: fix CVE-2026-54273 Date: Mon, 28 Sep 2026 10:43:23 -0700 Message-Id: <20260928174323.1810308-14-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20260928174323.1810308-1-dkelaiya@cisco.com> References: <20260928174323.1810308-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: rcdn-l-core-01.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, 28 Sep 2026 17:43:44 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/130452 From: Darsh Kelaiya This patch applies the reviewed upstream fix shown in [1]. The advisory identifying the fix is referenced in [4]. The follow-up from [2] is kept as a separate recipe patch. It adapts the declined WebSocket-upgrade replay from [3], applies the queue pause after replay, and includes the corresponding regression tests. 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/dfdfa9d5aad5d21f91c79fb2ceeba0f8046cb6cf [2] https://github.com/aio-libs/aiohttp/commit/47babd8c23ca79e2bae6cc8dcb5752b61e369fc7 [3] https://github.com/aio-libs/aiohttp/commit/8943d34337c133aa2a33f1fd10a30950d8dcb20e [4] https://nvd.nist.gov/vuln/detail/CVE-2026-54273 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-54273_p1.patch | 798 ++++++++++++++++++ .../python3-aiohttp/CVE-2026-54273_p2.patch | 181 ++++ .../python/python3-aiohttp_3.9.5.bb | 2 + 3 files changed, 981 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch new file mode 100644 index 0000000000..2fc96d59a7 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p1.patch @@ -0,0 +1,798 @@ +From d41ceb6e193960ebd24867485cfcb396ab5f4fc7 Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +Date: Sun, 7 Jun 2026 01:16:13 -0500 +Subject: [PATCH] [PR #12830/93a2b1c3 backport][3.14] Bound pipelined request + queue per connection (#12854) + +CVE: CVE-2026-54273 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/dfdfa9d5aad5d21f91c79fb2ceeba0f8046cb6cf] + +Backport Changes: +- Adapted the C parser to retain paused input in an internal tail and + declared `llhttp_resume()`. aiohttp 3.9.5 predates the upstream + parser pause and resume plumbing. +- Adapted `RequestHandler` and `BaseProtocol` field names and parser + ownership to the aiohttp 3.9.5 protocol layout. This preserves its + no-transport read-pause semantics. +- Adapted regression tests to the 3.9.5 request-state and upgrade + field names, existing `2**16` parser-limit fixture, and older + typing imports. Omitted two unrelated tests from the upstream + parent that require the newer `data_received_cb` API. +- Omitted generated `aiohttp/_http_parser.c` changes because the + Scarthgap recipe regenerates that file from `_http_parser.pyx` + during `do_configure`. + +(cherry picked from commit dfdfa9d5aad5d21f91c79fb2ceeba0f8046cb6cf) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/12830.bugfix.rst | 1 + + aiohttp/_cparser.pxd | 1 + + aiohttp/_http_parser.pyx | 52 ++++++++++++-- + aiohttp/base_protocol.py | 22 +++++- + aiohttp/http_parser.py | 20 ++++++ + aiohttp/web_protocol.py | 69 +++++++++++++++++- + docs/spelling_wordlist.txt | 1 + + tests/test_http_parser.py | 72 +++++++++++++++++++ + tests/test_web_functional.py | 133 ++++++++++++++++++++++++++++++++++- + tests/test_web_protocol.py | 110 +++++++++++++++++++++++++++++ + 10 files changed, 470 insertions(+), 11 deletions(-) + create mode 100644 CHANGES/12830.bugfix.rst + create mode 100644 tests/test_web_protocol.py + +diff --git a/CHANGES/12830.bugfix.rst b/CHANGES/12830.bugfix.rst +new file mode 100644 +index 0000000..d44d76d +--- /dev/null ++++ b/CHANGES/12830.bugfix.rst +@@ -0,0 +1 @@ ++Bounded the number of parsed-but-unhandled pipelined HTTP/1 requests buffered per connection on the server; once the queue reaches an internal limit the parser stops emitting and the transport is paused, resuming as the request handler drains the queue, so a client keeping one handler busy can no longer accumulate an unbounded backlog of pipelined requests -- by :user:`bdraco`. +diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd +index c2cd5a9..b7de1d4 100644 +--- a/aiohttp/_cparser.pxd ++++ b/aiohttp/_cparser.pxd +@@ -145,6 +145,7 @@ cdef extern from "../vendor/llhttp/build/llhttp.h": + + int llhttp_should_keep_alive(const llhttp_t* parser) + ++ void llhttp_resume(llhttp_t* parser) + void llhttp_resume_after_upgrade(llhttp_t* parser) + + llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index 89a0370..4eb5789 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -319,6 +319,9 @@ cdef class HttpParser: + list _raw_headers + bint _upgraded + list _messages ++ bytes _tail ++ Py_ssize_t _msg_in_flight ++ Py_ssize_t _max_msg_queue_size + object _payload + bint _payload_error + object _payload_exception +@@ -353,6 +356,7 @@ cdef class HttpParser: + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, ++ Py_ssize_t max_msg_queue_size=0, + ): + cparser.llhttp_settings_init(self._csettings) + cparser.llhttp_init(self._cparser, mode, self._csettings) +@@ -364,6 +368,9 @@ cdef class HttpParser: + self._timer = timer + + self._buf = bytearray() ++ self._tail = b'' ++ self._msg_in_flight = 0 ++ self._max_msg_queue_size = max_msg_queue_size + self._payload = None + self._payload_error = 0 + self._payload_exception = payload_exception +@@ -544,6 +551,11 @@ cdef class HttpParser: + + ### Public API ### + ++ def message_consumed(self): ++ # Protocol drained a queued message; free a slot for parsing. ++ if self._msg_in_flight > 0: ++ self._msg_in_flight -= 1 ++ + def feed_eof(self): + cdef bytes desc + +@@ -564,35 +576,55 @@ cdef class HttpParser: + if self._messages: + return self._messages[-1][0] + +- def feed_data(self, data): ++ def feed_data(self, incoming_data): + cdef: + size_t data_len + size_t nb ++ size_t pos ++ char* base + cdef cparser.llhttp_errno_t errno ++ cdef bytes data ++ ++ if type(incoming_data) is not bytes: ++ data = bytes(incoming_data) ++ else: ++ data = incoming_data ++ ++ if self._tail: ++ data, self._tail = self._tail + data, b'' + + PyObject_GetBuffer(data, &self.py_buf, PyBUF_SIMPLE) ++ base = self.py_buf.buf + data_len = self.py_buf.len + + errno = cparser.llhttp_execute( + self._cparser, +- self.py_buf.buf, ++ base, + data_len) + + if errno is cparser.HPE_PAUSED_UPGRADE: + cparser.llhttp_resume_after_upgrade(self._cparser) + +- nb = cparser.llhttp_get_error_pos(self._cparser) - self.py_buf.buf ++ nb = cparser.llhttp_get_error_pos(self._cparser) - base ++ elif errno is cparser.HPE_PAUSED: ++ cparser.llhttp_resume(self._cparser) ++ pos = cparser.llhttp_get_error_pos(self._cparser) - base ++ self._tail = data[pos:] + + PyBuffer_Release(&self.py_buf) + +- if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED_UPGRADE): ++ if errno not in ( ++ cparser.HPE_OK, ++ cparser.HPE_PAUSED, ++ cparser.HPE_PAUSED_UPGRADE, ++ ): + if self._payload_error == 0: + if self._last_error is not None: + ex = self._last_error + self._last_error = None + else: + after = cparser.llhttp_get_error_pos(self._cparser) +- before = data[:after - self.py_buf.buf] ++ before = data[:after - base] + after_b = after.split(b"\r\n", 1)[0] + before = before.rsplit(b"\r\n", 1)[-1] + data = before + after_b +@@ -623,12 +655,12 @@ cdef class HttpRequestParser(HttpParser): + 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, ++ bint auto_decompress=True, Py_ssize_t max_msg_queue_size=0, + ): + self._init(cparser.HTTP_REQUEST, protocol, loop, limit, timer, + max_line_size, max_headers, max_field_size, + payload_exception, response_with_body, read_until_eof, +- auto_decompress) ++ auto_decompress, max_msg_queue_size) + + cdef object _on_status_complete(self): + cdef int idx1, idx2 +@@ -832,6 +864,12 @@ cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1: + pyparser._last_error = exc + return -1 + else: ++ if pyparser._max_msg_queue_size: ++ pyparser._msg_in_flight += 1 ++ if pyparser._msg_in_flight >= pyparser._max_msg_queue_size: ++ # Queue full: pause llhttp between messages. feed_data() buffers ++ # the remainder as tail; resumes once the queue drains. ++ return cparser.HPE_PAUSED + return 0 + + +diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py +index dc1f24f..bd4f931 100644 +--- a/aiohttp/base_protocol.py ++++ b/aiohttp/base_protocol.py +@@ -4,6 +4,13 @@ from typing import Optional, cast + from .helpers import set_exception + from .tcp_helpers import tcp_nodelay + ++# Raised by transport.pause_reading()/resume_reading() when the transport ++# does not support flow control; safe to ignore. ++# NOTE: Catch these with a plain try/except/pass, never contextlib.suppress(): ++# pause/resume run on the hot read path and suppress() is ~6x slower than ++# try/except here (it builds a context manager and unpacks this tuple per call). ++PAUSE_RESUME_READING_ERRORS = (AttributeError, NotImplementedError, RuntimeError) ++ + + class BaseProtocol(asyncio.Protocol): + __slots__ = ( +@@ -46,15 +53,26 @@ class BaseProtocol(asyncio.Protocol): + if not self._reading_paused and self.transport is not None: + try: + self.transport.pause_reading() +- except (AttributeError, NotImplementedError, RuntimeError): ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to pause. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass + self._reading_paused = True + ++ def _reading_paused_for_msg_queue(self) -> bool: ++ """Keep the transport paused for protocol-specific reasons (overridden).""" ++ return False ++ + def resume_reading(self) -> None: + if self._reading_paused and self.transport is not None: ++ self._reading_paused = False ++ if self._reading_paused_for_msg_queue(): ++ return + try: + self.transport.resume_reading() +- except (AttributeError, NotImplementedError, RuntimeError): ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to resume. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass + self._reading_paused = False + +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index 08950aa..6cf49c7 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -270,6 +270,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + response_with_body: bool = True, + read_until_eof: bool = False, + auto_decompress: bool = True, ++ max_msg_queue_size: int = 0, + ) -> None: + self.protocol = protocol + self.loop = loop +@@ -294,11 +295,19 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + self._headers_parser = HeadersParser( + max_line_size, max_headers, max_field_size, self.lax + ) ++ # Stop emitting messages once this many are queued unconsumed (0 = off). ++ self._max_msg_queue_size = max_msg_queue_size ++ self._msg_in_flight = 0 + + @abc.abstractmethod + def parse_message(self, lines: List[bytes]) -> _MsgT: + pass + ++ def message_consumed(self) -> None: ++ """Protocol drained a queued message; free a slot for parsing.""" ++ if self._msg_in_flight > 0: ++ self._msg_in_flight -= 1 ++ + def feed_eof(self) -> Optional[_MsgT]: + if self._payload_parser is not None: + self._payload_parser.feed_eof() +@@ -340,6 +349,15 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + # read HTTP message (request/response line + headers), \r\n\r\n + # and split by lines + if self._payload_parser is None and not self._upgraded: ++ if ( ++ self._max_msg_queue_size ++ and self._msg_in_flight >= self._max_msg_queue_size ++ ): ++ # Queue full: buffer the rest and stop. Safe pause point; ++ # any preceding body is consumed before the next request ++ # line. Resumes via feed_data(b"") when the queue drains. ++ self._tail = data[start_pos:] ++ break + pos = data.find(SEP, start_pos) + # consume \r\n + if pos == start_pos and not self._lines: +@@ -481,6 +499,8 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + payload = EMPTY_PAYLOAD + + messages.append((msg, payload)) ++ if self._max_msg_queue_size: ++ self._msg_in_flight += 1 + else: + self._tail = data[start_pos:] + if len(self._tail) > self.max_line_size: +diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py +index a73bb43..baa0290 100644 +--- a/aiohttp/web_protocol.py ++++ b/aiohttp/web_protocol.py +@@ -25,7 +25,7 @@ import attr + import yarl + + from .abc import AbstractAccessLogger, AbstractStreamWriter +-from .base_protocol import BaseProtocol ++from .base_protocol import PAUSE_RESUME_READING_ERRORS, BaseProtocol + from .helpers import ceil_timeout, set_exception + from .http import ( + HttpProcessingError, +@@ -44,6 +44,11 @@ from .web_response import Response, StreamResponse + + __all__ = ("RequestHandler", "RequestPayloadError", "PayloadAccessError") + ++# Max parsed-but-unhandled pipelined requests buffered per connection before ++# reading is paused. Bounds memory a client can pin by keeping one handler busy ++# and pipelining behind it; reading resumes as the queue drains. ++MAX_MSG_QUEUE_SIZE = 32 ++ + if TYPE_CHECKING: + from .web_server import Server + +@@ -150,6 +155,9 @@ class RequestHandler(BaseProtocol): + "_keepalive_timeout", + "_lingering_time", + "_messages", ++ "_max_msg_queue_size", ++ "_msg_queue_resume_size", ++ "_msg_queue_paused", + "_message_tail", + "_waiter", + "_task_handler", +@@ -187,6 +195,14 @@ class RequestHandler(BaseProtocol): + auto_decompress: bool = True, + timeout_ceil_threshold: float = 5, + ): ++ self._max_msg_queue_size = MAX_MSG_QUEUE_SIZE ++ # Low-water mark: resume reading once the queue drains to half the limit ++ # so we refill in batches instead of churning pause/resume per request. ++ self._msg_queue_resume_size = MAX_MSG_QUEUE_SIZE // 2 ++ # Set before super().__init__ so _reading_paused_for_msg_queue() is safe ++ # if BaseProtocol ever triggers a resume during init. ++ self._msg_queue_paused = False ++ + super().__init__(loop) + + self._request_count = 0 +@@ -224,6 +240,7 @@ class RequestHandler(BaseProtocol): + max_headers=max_headers, + payload_exception=RequestPayloadError, + auto_decompress=auto_decompress, ++ max_msg_queue_size=MAX_MSG_QUEUE_SIZE, + ) + + self._timeout_ceil_threshold: float = 5 +@@ -371,6 +388,14 @@ class RequestHandler(BaseProtocol): + # don't set result twice + waiter.set_result(None) + ++ # Queue full: pause the transport (the parser already stopped ++ # emitting). start() resumes as it drains the queue. ++ if ( ++ not self._msg_queue_paused ++ and len(self._messages) >= self._max_msg_queue_size ++ ): ++ self._pause_msg_queue_reading() ++ + self._upgrade = upgraded + if upgraded and tail: + self._message_tail = tail +@@ -385,6 +410,36 @@ class RequestHandler(BaseProtocol): + if eof: + self.close() + ++ def _reading_paused_for_msg_queue(self) -> bool: ++ return self._msg_queue_paused ++ ++ def _pause_msg_queue_reading(self) -> None: ++ self._msg_queue_paused = True ++ if self.transport is not None: ++ try: ++ self.transport.pause_reading() ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to pause. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). ++ pass ++ ++ def _resume_msg_queue_reading(self) -> None: ++ if not self._upgrade: ++ # Reparse buffered pipelined requests while still marked paused so ++ # a refill past the limit does not re-pause an already-paused ++ # transport; only resume below once it stayed under the limit. ++ self.data_received(b"") ++ if len(self._messages) >= self._max_msg_queue_size: ++ return ++ self._msg_queue_paused = False ++ if not self._reading_paused and self.transport is not None: ++ try: ++ self.transport.resume_reading() ++ except PAUSE_RESUME_READING_ERRORS: ++ # Transport lacks flow control; nothing to resume. Intentionally ++ # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). ++ pass ++ + def keep_alive(self, val: bool) -> None: + """Set keep-alive connection mode. + +@@ -517,6 +572,18 @@ class RequestHandler(BaseProtocol): + + message, payload = self._messages.popleft() + ++ # Free a parser slot; resume reading once drained to low water so ++ # pipelining keeps flowing while this request is handled. ++ # no branch: _request_parser is only None after connection_lost, ++ # whose path exits this loop, so the None case is not reachable. ++ if self._request_parser is not None: # pragma: no branch ++ self._request_parser.message_consumed() ++ if ( ++ self._msg_queue_paused ++ and len(self._messages) <= self._msg_queue_resume_size ++ ): ++ self._resume_msg_queue_reading() ++ + start = loop.time() + + manager.requests_count += 1 +diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt +index 34399e6..3fb6d35 100644 +--- a/docs/spelling_wordlist.txt ++++ b/docs/spelling_wordlist.txt +@@ -228,6 +228,7 @@ peername + performant + pickleable + ping ++pipelined + pipelining + pluggable + plugin +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 24b4e50..213761c 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -111,6 +111,78 @@ def test_c_parser_loaded(): + assert "RawResponseMessageC" in dir(aiohttp.http_parser) + + ++_PIPELINED_GET = b"GET / HTTP/1.1\r\nHost: a\r\n\r\n" ++ ++ ++def _build_request_parser( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++ max_msg_queue_size: int, ++) -> HttpRequestParser: ++ return request_cls( ++ protocol, ++ loop, ++ 2**16, ++ max_line_size=8190, ++ max_headers=128, ++ max_field_size=8190, ++ max_msg_queue_size=max_msg_queue_size, ++ ) ++ ++ ++def test_max_msg_queue_size_caps_emitted_messages( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ parser = _build_request_parser(request_cls, protocol, loop, 4) ++ messages, upgraded, _tail = parser.feed_data(_PIPELINED_GET * 10) ++ assert len(messages) == 4 ++ assert not upgraded ++ ++ ++def test_max_msg_queue_size_resumes_after_consume( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ limit = 4 ++ total = 10 ++ parser = _build_request_parser(request_cls, protocol, loop, limit) ++ messages, _upgraded, _tail = parser.feed_data(_PIPELINED_GET * total) ++ seen = 0 ++ while messages: ++ assert len(messages) <= limit ++ seen += len(messages) ++ for _msg, _payload in messages: ++ parser.message_consumed() ++ messages, _upgraded, _tail = parser.feed_data(b"") ++ assert seen == total ++ ++ ++def test_max_msg_queue_size_zero_is_unbounded( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ parser = _build_request_parser(request_cls, protocol, loop, 0) ++ messages, _upgraded, _tail = parser.feed_data(_PIPELINED_GET * 50) ++ assert len(messages) == 50 ++ ++ ++def test_message_consumed_underflow_is_ignored( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++ loop: asyncio.AbstractEventLoop, ++) -> None: ++ parser = _build_request_parser(request_cls, protocol, loop, 4) ++ # No message is in flight; consuming must not underflow the counter. ++ parser.message_consumed() ++ messages, _upgraded, _tail = parser.feed_data(_PIPELINED_GET * 4) ++ assert len(messages) == 4 ++ ++ + def test_parse_headers(parser: Any) -> None: + text = b"""GET /test HTTP/1.1\r + test: a line\r +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index e6d01bf..533b132 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -5,7 +5,7 @@ import pathlib + import socket + import zlib + from contextlib import suppress +-from typing import Any, Optional ++from typing import Any, NoReturn, Optional + from unittest import mock + + import pytest +@@ -27,6 +27,7 @@ from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer + from aiohttp.streams import StreamReader + from aiohttp.test_utils import make_mocked_coro + from aiohttp.typedefs import Handler ++from aiohttp.web_protocol import MAX_MSG_QUEUE_SIZE, RequestHandler + + try: + import brotlicffi as brotli +@@ -1620,6 +1621,136 @@ async def test_response_prepared_with_clone(aiohttp_client) -> None: + await resp.release() + + ++async def test_http1_pipelined_requests_are_count_limited( ++ aiohttp_server: AiohttpServer, ++ monkeypatch: pytest.MonkeyPatch, ++) -> None: ++ """Requests pipelined behind a busy handler must not grow unbounded. ++ ++ A client can keep one handler active and pipeline many complete requests ++ behind it; the per-connection queue stays bounded by MAX_MSG_QUEUE_SIZE. ++ """ ++ pipelined_requests = 500 ++ slow_handler_started = asyncio.Event() ++ queue_observed = asyncio.Event() ++ max_queued = 0 ++ data_received = RequestHandler.data_received ++ ++ def observe_data_received(self: RequestHandler, data: bytes) -> None: ++ nonlocal max_queued ++ data_received(self, data) ++ if self._current_request is not None and self._messages: ++ max_queued = max(max_queued, len(self._messages)) ++ queue_observed.set() ++ ++ monkeypatch.setattr(RequestHandler, "data_received", observe_data_received) ++ ++ async def slow_handler(request: web.Request) -> web.Response: ++ slow_handler_started.set() ++ await asyncio.sleep(0.5) ++ return web.Response(text="slow") ++ ++ async def fast_handler(request: web.Request) -> NoReturn: ++ # The pipelined requests are only counted, never handled: the test ++ # closes the connection while the slow handler still holds the loop. ++ assert False ++ ++ app = web.Application() ++ app.router.add_get("/slow", slow_handler) ++ app.router.add_get("/x", fast_handler) ++ server = await aiohttp_server(app) ++ ++ def raw_get(path: str) -> bytes: ++ return ( ++ f"GET {path} HTTP/1.1\r\nHost: localhost\r\n" ++ "Connection: keep-alive\r\n\r\n" ++ ).encode("ascii") ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write(raw_get("/slow")) ++ await writer.drain() ++ await asyncio.wait_for(slow_handler_started.wait(), 1) ++ ++ writer.write(raw_get("/x") * pipelined_requests) ++ await writer.drain() ++ await asyncio.wait_for(queue_observed.wait(), 1) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ # Tight lower bound also catches over-aggressive pausing (e.g. clamping to 1). ++ assert MAX_MSG_QUEUE_SIZE // 2 < max_queued <= MAX_MSG_QUEUE_SIZE ++ ++ ++async def test_http1_pipelined_queue_resumes_after_drain( ++ aiohttp_server: AiohttpServer, ++ monkeypatch: pytest.MonkeyPatch, ++) -> None: ++ """A paused pipeline queue resumes reading once handlers drain it. ++ ++ Once enough requests are pipelined behind a busy handler to fill the queue, ++ reading is paused; as the handlers drain the queue past the low-water mark ++ reading must resume so the remaining buffered requests are still served. ++ """ ++ # Several times the limit so the queue refills and re-pauses while draining. ++ pipelined_requests = MAX_MSG_QUEUE_SIZE * 3 ++ first_started = asyncio.Event() ++ release_first = asyncio.Event() ++ resumed = asyncio.Event() ++ handled: list[str] = [] ++ all_handled = asyncio.Event() ++ ++ resume = RequestHandler._resume_msg_queue_reading ++ ++ def observe_resume(self: RequestHandler) -> None: ++ resume(self) ++ resumed.set() ++ ++ monkeypatch.setattr(RequestHandler, "_resume_msg_queue_reading", observe_resume) ++ ++ async def handler(request: web.Request) -> web.Response: ++ if request.path == "/first": ++ first_started.set() ++ await release_first.wait() ++ handled.append(request.path) ++ if len(handled) == pipelined_requests + 1: ++ all_handled.set() ++ return web.Response() ++ ++ app = web.Application() ++ app.router.add_get("/{tail:.*}", handler) ++ server = await aiohttp_server(app) ++ ++ def raw_get(path: str) -> bytes: ++ return ( ++ f"GET {path} HTTP/1.1\r\nHost: localhost\r\n" ++ "Connection: keep-alive\r\n\r\n" ++ ).encode("ascii") ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write(raw_get("/first")) ++ await writer.drain() ++ await asyncio.wait_for(first_started.wait(), 1) ++ ++ writer.write(b"".join(raw_get(f"/r{i}") for i in range(pipelined_requests))) ++ await writer.drain() ++ ++ # Let the busy handler finish so the queue drains and reading resumes. ++ release_first.set() ++ await asyncio.wait_for(resumed.wait(), 5) ++ # Every pipelined request is still served only if reading resumed. ++ await asyncio.wait_for(all_handled.wait(), 5) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ assert len(handled) == pipelined_requests + 1 ++ ++ + @pytest.mark.parametrize("decompressed_size", [4 * 1024 * 1024, 32 * 1024 * 1024]) + async def test_unread_compressed_body_drain_is_bounded( + aiohttp_server: AiohttpServer, +diff --git a/tests/test_web_protocol.py b/tests/test_web_protocol.py +new file mode 100644 +index 0000000..dcda539 +--- /dev/null ++++ b/tests/test_web_protocol.py +@@ -0,0 +1,110 @@ ++import asyncio ++from unittest import mock ++ ++import pytest ++ ++from aiohttp.web_protocol import RequestHandler ++from aiohttp.web_server import Server ++ ++ ++@pytest.fixture ++def dummy_manager() -> Server: ++ return mock.create_autospec( ++ Server, ++ request_handler=mock.Mock(), ++ request_factory=mock.Mock(), ++ instance=True, ++ ) # type: ignore[no-any-return] ++ ++ ++def test_pause_msg_queue_reading_without_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Pausing with no transport still records the paused state.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ handler.transport = None ++ ++ handler._pause_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is True ++ ++ ++def test_resume_msg_queue_reading_after_upgrade_skips_reparse( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Resume after an upgrade clears the pause and resumes without reparsing.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ transport = mock.Mock() ++ handler.transport = transport ++ handler._upgrade = True ++ handler._msg_queue_paused = True ++ handler._reading_paused = False ++ ++ with mock.patch.object(RequestHandler, "data_received") as data_received: ++ handler._resume_msg_queue_reading() ++ ++ data_received.assert_not_called() ++ assert handler._msg_queue_paused is False ++ transport.resume_reading.assert_called_once_with() ++ ++ ++def test_resume_msg_queue_reading_without_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Resume clears the pause but does not touch a missing transport.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ handler.transport = None ++ handler._upgrade = True # skip the reparse branch ++ handler._msg_queue_paused = True ++ ++ handler._resume_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is False ++ ++ ++def test_resume_reading_stays_paused_for_msg_queue( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """Base resume_reading must not un-pause the transport while queue-paused.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ transport = mock.Mock() ++ handler.transport = transport ++ handler._msg_queue_paused = True ++ ++ handler.resume_reading() ++ ++ transport.resume_reading.assert_not_called() ++ ++ ++def test_pause_msg_queue_reading_ignores_unsupported_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """A transport without flow control raising on pause is ignored.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ # Bare asyncio.Transport.pause_reading() raises NotImplementedError. ++ handler.transport = asyncio.Transport() ++ ++ handler._pause_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is True ++ ++ ++def test_resume_msg_queue_reading_ignores_unsupported_transport( ++ loop: asyncio.AbstractEventLoop, ++ dummy_manager: Server, ++) -> None: ++ """A transport without flow control raising on resume is ignored.""" ++ handler = RequestHandler(dummy_manager, loop=loop) ++ # Bare asyncio.Transport.resume_reading() raises NotImplementedError. ++ handler.transport = asyncio.Transport() ++ handler._upgrade = True # skip the reparse branch ++ handler._msg_queue_paused = True ++ ++ handler._resume_msg_queue_reading() ++ ++ assert handler._msg_queue_paused is False +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch new file mode 100644 index 0000000000..d4f33b6c44 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54273_p2.patch @@ -0,0 +1,181 @@ +From d460210c19b97fde184843d17487e336698d59da Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Sun, 9 Aug 2026 17:17:34 +0100 +Subject: [PATCH] [PR #13356/72eaa429 backport][3.14] Stop handing back + pipelined requests the parser buffered (#13360) + +**This is a backport of PR #13356 as merged into master +(72eaa429cf89b1e20212590f3e704444239569fb).** + +--------- + +Co-authored-by: Rodrigo Nogueira + +CVE: CVE-2026-54273 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/47babd8c23ca79e2bae6cc8dcb5752b61e369fc7] + +Backport Changes: +- Adapted the required 8943d343 replay behavior to aiohttp 3.9.5 + `_request_parser` and `_upgrade` fields. Replayed messages are + retained and queued, and the returned parser tail is preserved. +- Applied the 47babd8c queue-pause check after replay and retained + its pure-Python parser tail fix, change note, parser test, and + declined-upgrade functional regression test. + +(cherry picked from commit 47babd8c23ca79e2bae6cc8dcb5752b61e369fc7) +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13356.bugfix.rst | 4 +++ + aiohttp/http_parser.py | 2 ++ + aiohttp/web_protocol.py | 18 +++++++++++-- + tests/test_http_parser.py | 18 +++++++++++++ + tests/test_web_functional.py | 51 ++++++++++++++++++++++++++++++++++++ + 5 files changed, 91 insertions(+), 2 deletions(-) + create mode 100644 CHANGES/13356.bugfix.rst + +diff --git a/CHANGES/13356.bugfix.rst b/CHANGES/13356.bugfix.rst +new file mode 100644 +index 0000000..a961723 +--- /dev/null ++++ b/CHANGES/13356.bugfix.rst +@@ -0,0 +1,4 @@ ++Fixed requests pipelined behind a request whose upgrade the handler declined ++going unanswered once there were more of them than the per-connection queue ++holds. With the pure-Python parser the same requests were also served more ++than once -- by :user:`rodrigobnogueira`. +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index 6cf49c7..37a0dff 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -357,6 +357,8 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + # any preceding body is consumed before the next request + # line. Resumes via feed_data(b"") when the queue drains. + self._tail = data[start_pos:] ++ # The remainder now lives in self._tail only. Don't return it. ++ data = EMPTY + break + pos = data.find(SEP, start_pos) + # consume \r\n +diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py +index baa0290..39a1b68 100644 +--- a/aiohttp/web_protocol.py ++++ b/aiohttp/web_protocol.py +@@ -687,8 +687,22 @@ class RequestHandler(BaseProtocol): + self._request_parser.set_upgraded(False) + self._upgrade = False + if self._message_tail: +- self._request_parser.feed_data(self._message_tail) +- self._message_tail = b"" ++ messages, _upgraded, tail = self._request_parser.feed_data( ++ self._message_tail ++ ) ++ self._message_tail = tail ++ for msg, payload in messages: ++ self._request_count += 1 ++ self._messages.append((msg, payload)) ++ # Pause the transport, like in data_received(). ++ if ( ++ not self._msg_queue_paused ++ and len(self._messages) >= self._max_msg_queue_size ++ ): ++ self._pause_msg_queue_reading() ++ # This shouldn't be possible. If a future refactor results in this ++ # failing, then the code may need to be updated to set the waiter. ++ assert self._waiter is None + try: + prepare_meth = resp.prepare + except AttributeError: +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 213761c..b29518a 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -142,6 +142,24 @@ def test_max_msg_queue_size_caps_emitted_messages( + assert not upgraded + + ++async def test_max_msg_queue_size_keeps_tail_to_itself( ++ request_cls: type[HttpRequestParser], ++ protocol: BaseProtocol, ++) -> None: ++ """The remainder is buffered for the next feed, so it must not be returned. ++ ++ Handing it back as well gives the caller a second copy of bytes the parser ++ is already holding, and both copies get parsed. ++ """ ++ loop = asyncio.get_running_loop() ++ parser = _build_request_parser(request_cls, protocol, loop, 4) ++ ++ messages, _upgraded, tail = parser.feed_data(_PIPELINED_GET * 10) ++ ++ assert len(messages) == 4 ++ assert tail == b"" ++ ++ + def test_max_msg_queue_size_resumes_after_consume( + request_cls: type[HttpRequestParser], + protocol: BaseProtocol, +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index 533b132..a0e3a8e 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -1751,6 +1751,57 @@ async def test_http1_pipelined_queue_resumes_after_drain( + assert len(handled) == pipelined_requests + 1 + + ++async def test_http1_pipelined_behind_declined_upgrade_served_once( ++ aiohttp_server: AiohttpServer, ++) -> None: ++ """Requests pipelined behind a declined upgrade are each served once. ++ ++ The bytes following an upgrade request are buffered whole, then re-fed once ++ the handler answers it normally. More of them than the queue holds must ++ still be served, and none of them twice. ++ """ ++ pipelined_requests = MAX_MSG_QUEUE_SIZE + 8 ++ handled: list[str] = [] ++ all_handled = asyncio.Event() ++ ++ async def handler(request: web.Request) -> web.Response: ++ handled.append(request.path) ++ if len(handled) == pipelined_requests + 1: ++ all_handled.set() ++ return web.Response() ++ ++ app = web.Application() ++ app.router.add_get("/{tail:.*}", handler) ++ server = await aiohttp_server(app) ++ ++ def raw_get(path: str) -> bytes: ++ return ( ++ f"GET {path} HTTP/1.1\r\nHost: localhost\r\n" ++ "Connection: keep-alive\r\n\r\n" ++ ).encode("ascii") ++ ++ # An upgrade the handler answers normally, then the pipeline, in one write. ++ upgrade = ( ++ b"GET /upgrade HTTP/1.1\r\nHost: localhost\r\n" ++ b"Connection: Upgrade\r\nUpgrade: websocket\r\n\r\n" ++ ) ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write( ++ upgrade + b"".join(raw_get(f"/r{i}") for i in range(pipelined_requests)) ++ ) ++ await writer.drain() ++ await asyncio.wait_for(all_handled.wait(), 10) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ assert len(handled) == pipelined_requests + 1 ++ assert len(set(handled)) == len(handled) ++ ++ + @pytest.mark.parametrize("decompressed_size", [4 * 1024 * 1024, 32 * 1024 * 1024]) + async def test_unread_compressed_body_drain_is_bounded( + aiohttp_server: AiohttpServer, +-- +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 88ae49dec4..893d5124f7 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 @@ -34,6 +34,8 @@ SRC_URI += "file://CVE-2024-52304.patch \ file://CVE-2026-54277.patch \ file://CVE-2026-54278.patch \ file://CVE-2026-54279.patch \ + file://CVE-2026-54273_p1.patch \ + file://CVE-2026-54273_p2.patch \ " CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows"