All of lore.kernel.org
 help / color / mirror / Atom feed
* [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs
@ 2026-09-28 17:43 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 01/13] python3-aiohttp: fix CVE-2026-34519 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (13 more replies)
  0 siblings, 14 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

Backport thirteen upstream aiohttp security fixes to the 3.9.5 recipe on
scarthgap.

The corresponding fixes are available in aiohttp 3.14.0 and 3.14.1.
Carry them as focused backports instead of upgrading the recipe because
the 3.14 release also introduces new APIs, deprecations, and
backward-incompatible behavior outside the security scope.

The individual commits retain the upstream fix provenance and public
advisory references for each CVE.

Testing:
- Applied the thirteen layer commits to the upstream scarthgap baseline.
- Applied all fifteen embedded patches to aiohttp 3.9.5 in series order
  with no conflicts or fuzz.
- Package build and image build validation completed successfully.

Darsh Kelaiya (13):
  python3-aiohttp: fix CVE-2026-34519
  python3-aiohttp: fix CVE-2026-34516
  python3-aiohttp: fix CVE-2026-34517
  python3-aiohttp: fix CVE-2026-34520
  python3-aiohttp: fix CVE-2026-34525
  python3-aiohttp: fix CVE-2026-47265
  python3-aiohttp: fix CVE-2026-50269
  python3-aiohttp: fix CVE-2026-54274
  python3-aiohttp: fix CVE-2026-54275
  python3-aiohttp: fix CVE-2026-54277
  python3-aiohttp: fix CVE-2026-54278
  python3-aiohttp: fix CVE-2026-54279
  python3-aiohttp: fix CVE-2026-54273

 .../python3-aiohttp/CVE-2026-34516.patch    | 356 +++++++
 .../python3-aiohttp/CVE-2026-34517.patch    |  65 ++
 .../python3-aiohttp/CVE-2026-34519.patch    | 638 ++++++++++++
 .../python3-aiohttp/CVE-2026-34520.patch    | 121 +++
 .../python3-aiohttp/CVE-2026-34525_p1.patch | 128 +++
 .../python3-aiohttp/CVE-2026-34525_p2.patch | 331 +++++++
 .../python3-aiohttp/CVE-2026-47265.patch    |  61 ++
 .../python3-aiohttp/CVE-2026-50269.patch    | 166 ++++
 .../python3-aiohttp/CVE-2026-54273_p1.patch | 798 ++++++++++++++++
 .../python3-aiohttp/CVE-2026-54273_p2.patch | 181 ++++
 .../python3-aiohttp/CVE-2026-54274.patch    | 195 ++++
 .../python3-aiohttp/CVE-2026-54275.patch    | 124 +++
 .../python3-aiohttp/CVE-2026-54277.patch    |  96 ++
 .../python3-aiohttp/CVE-2026-54278.patch    | 209 ++++
 .../python3-aiohttp/CVE-2026-54279.patch    | 317 ++++++
 .../python/python3-aiohttp_3.9.5.bb         |  15 +
 16 files changed, 3801 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34517.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34519.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34520.patch
 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
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-47265.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-50269.patch
 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
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54274.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54275.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54277.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54278.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54279.patch

-- 
2.35.6


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 01/13] python3-aiohttp: fix CVE-2026-34519
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 02/13] python3-aiohttp: fix CVE-2026-34516 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (12 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <git@sambull.org>
+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 <dhiral@users.noreply.github.com>
+(cherry picked from commit 53b35a2f8869c37a133e60bf1a82a1c01642ba2b)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 02/13] python3-aiohttp: fix CVE-2026-34516
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 01/13] python3-aiohttp: fix CVE-2026-34519 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 03/13] python3-aiohttp: fix CVE-2026-34517 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (11 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <git@sambull.org>
+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 <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 03/13] python3-aiohttp: fix CVE-2026-34517
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 01/13] python3-aiohttp: fix CVE-2026-34519 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 02/13] python3-aiohttp: fix CVE-2026-34516 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 04/13] python3-aiohttp: fix CVE-2026-34520 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (10 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <git@sambull.org>
+(cherry picked from commit cbb774f38330563422ca0c413a71021d7b944145)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 04/13] python3-aiohttp: fix CVE-2026-34520
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (2 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 03/13] python3-aiohttp: fix CVE-2026-34517 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 05/13] python3-aiohttp: fix CVE-2026-34525 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (9 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <rodrigo.b.nogueira@gmail.com>
+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 <rodrigo.nogueira@prf.gov.br>
+(cherry picked from commit 9370b9714a7a56003cacd31a9b4ae16eab109ba4)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 05/13] python3-aiohttp: fix CVE-2026-34525
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (3 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 04/13] python3-aiohttp: fix CVE-2026-34520 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 06/13] python3-aiohttp: fix CVE-2026-47265 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (8 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <rodrigo.b.nogueira@gmail.com>
+(cherry picked from commit e00ca3cca92c465c7913c4beb763a72da9ed8349)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ 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 <nick@koston.org>
+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 <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 06/13] python3-aiohttp: fix CVE-2026-47265
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (4 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 05/13] python3-aiohttp: fix CVE-2026-34525 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 07/13] python3-aiohttp: fix CVE-2026-50269 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (7 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <git@sambull.org>
+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 <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 07/13] python3-aiohttp: fix CVE-2026-50269
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (5 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 06/13] python3-aiohttp: fix CVE-2026-47265 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 08/13] python3-aiohttp: fix CVE-2026-54274 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (6 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <git@sambull.org>
+(cherry picked from commit 1e3ecd48ec423c1fcc729c98adff31172faae1ef)
+(cherry picked from commit bf88077ebb14f4c29924b8e8904cba20c55c28b8)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 08/13] python3-aiohttp: fix CVE-2026-54274
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (6 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 07/13] python3-aiohttp: fix CVE-2026-50269 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 09/13] python3-aiohttp: fix CVE-2026-54275 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (5 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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 <git@sambull.org>
+(cherry picked from commit 14b6ee851fb16ec199acb950de0c82d476799e7d)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 09/13] python3-aiohttp: fix CVE-2026-54275
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (7 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 08/13] python3-aiohttp: fix CVE-2026-54274 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 10/13] python3-aiohttp: fix CVE-2026-54277 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (4 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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" <nick@koston.org>
+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 <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 10/13] python3-aiohttp: fix CVE-2026-54277
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (8 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 09/13] python3-aiohttp: fix CVE-2026-54275 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 11/13] python3-aiohttp: fix CVE-2026-54278 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (3 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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" <nick@koston.org>
+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 <dkelaiya@cisco.com>
+---
+ 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 = <HttpParser>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 = <HttpParser>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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 11/13] python3-aiohttp: fix CVE-2026-54278
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (9 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 10/13] python3-aiohttp: fix CVE-2026-54277 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 12/13] python3-aiohttp: fix CVE-2026-54279 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (2 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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" <nick@koston.org>
+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 <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 12/13] python3-aiohttp: fix CVE-2026-54279
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (10 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 11/13] python3-aiohttp: fix CVE-2026-54278 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 13/13] python3-aiohttp: fix CVE-2026-54273 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-10-20  5:56 ` [meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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" <nick@koston.org>
+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 <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [oe][meta-python][scarthgap][PATCH 13/13] python3-aiohttp: fix CVE-2026-54273
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (11 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 12/13] python3-aiohttp: fix CVE-2026-54279 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-09-28 17:43 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2026-10-20  5:56 ` [meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-09-28 17:43 UTC (permalink / raw)
  To: openembedded-devel; +Cc: Darsh Kelaiya

From: Darsh Kelaiya <dkelaiya@cisco.com>

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 <dkelaiya@cisco.com>
---
 .../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" <nick@koston.org>
+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 <dkelaiya@cisco.com>
+---
+ 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 = <char*>self.py_buf.buf
+         data_len = <size_t>self.py_buf.len
+ 
+         errno = cparser.llhttp_execute(
+             self._cparser,
+-            <char*>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) - <char*>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 - <char*>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 <rodrigo.b.nogueira@gmail.com>
+
+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 <dkelaiya@cisco.com>
+---
+ 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"
-- 
2.35.6



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* Re: [meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs
  2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
                   ` (12 preceding siblings ...)
  2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 13/13] python3-aiohttp: fix CVE-2026-54273 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-20  5:56 ` Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
  13 siblings, 0 replies; 15+ messages in thread
From: Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-10-20  5:56 UTC (permalink / raw)
  To: openembedded-devel

[-- Attachment #1: Type: text/plain, Size: 4034 bytes --]

Hello Anuj, Khem,

Is there an update on the review, or anything you’d like me to change in this series?

Thanks,
Darsh

On Mon, Sep 28, 2026 at 11:13 PM, Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco) wrote:

> 
> From: Darsh Kelaiya <dkelaiya@cisco.com>
> 
> Backport thirteen upstream aiohttp security fixes to the 3.9.5 recipe on
> scarthgap.
> 
> The corresponding fixes are available in aiohttp 3.14.0 and 3.14.1.
> Carry them as focused backports instead of upgrading the recipe because
> the 3.14 release also introduces new APIs, deprecations, and
> backward-incompatible behavior outside the security scope.
> 
> The individual commits retain the upstream fix provenance and public
> advisory references for each CVE.
> 
> Testing:
> - Applied the thirteen layer commits to the upstream scarthgap baseline.
> - Applied all fifteen embedded patches to aiohttp 3.9.5 in series order
> with no conflicts or fuzz.
> - Package build and image build validation completed successfully.
> 
> Darsh Kelaiya (13):
> python3-aiohttp: fix CVE-2026-34519
> python3-aiohttp: fix CVE-2026-34516
> python3-aiohttp: fix CVE-2026-34517
> python3-aiohttp: fix CVE-2026-34520
> python3-aiohttp: fix CVE-2026-34525
> python3-aiohttp: fix CVE-2026-47265
> python3-aiohttp: fix CVE-2026-50269
> python3-aiohttp: fix CVE-2026-54274
> python3-aiohttp: fix CVE-2026-54275
> python3-aiohttp: fix CVE-2026-54277
> python3-aiohttp: fix CVE-2026-54278
> python3-aiohttp: fix CVE-2026-54279
> python3-aiohttp: fix CVE-2026-54273
> 
> .../python3-aiohttp/CVE-2026-34516.patch | 356 +++++++
> .../python3-aiohttp/CVE-2026-34517.patch | 65 ++
> .../python3-aiohttp/CVE-2026-34519.patch | 638 ++++++++++++
> .../python3-aiohttp/CVE-2026-34520.patch | 121 +++
> .../python3-aiohttp/CVE-2026-34525_p1.patch | 128 +++
> .../python3-aiohttp/CVE-2026-34525_p2.patch | 331 +++++++
> .../python3-aiohttp/CVE-2026-47265.patch | 61 ++
> .../python3-aiohttp/CVE-2026-50269.patch | 166 ++++
> .../python3-aiohttp/CVE-2026-54273_p1.patch | 798 ++++++++++++++++
> .../python3-aiohttp/CVE-2026-54273_p2.patch | 181 ++++
> .../python3-aiohttp/CVE-2026-54274.patch | 195 ++++
> .../python3-aiohttp/CVE-2026-54275.patch | 124 +++
> .../python3-aiohttp/CVE-2026-54277.patch | 96 ++
> .../python3-aiohttp/CVE-2026-54278.patch | 209 ++++
> .../python3-aiohttp/CVE-2026-54279.patch | 317 ++++++
> .../python/python3-aiohttp_3.9.5.bb | 15 +
> 16 files changed, 3801 insertions(+)
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34517.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34519.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34520.patch
> 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
> 
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-47265.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-50269.patch
> 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
> 
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54274.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54275.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54277.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54278.patch
> create mode 100644
> meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-54279.patch
> 
> --
> 2.35.6

[-- Attachment #2: Type: text/html, Size: 4191 bytes --]

^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2026-10-20  5:56 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-28 17:43 [oe][meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 01/13] python3-aiohttp: fix CVE-2026-34519 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 02/13] python3-aiohttp: fix CVE-2026-34516 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 03/13] python3-aiohttp: fix CVE-2026-34517 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 04/13] python3-aiohttp: fix CVE-2026-34520 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 05/13] python3-aiohttp: fix CVE-2026-34525 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 06/13] python3-aiohttp: fix CVE-2026-47265 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 07/13] python3-aiohttp: fix CVE-2026-50269 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 08/13] python3-aiohttp: fix CVE-2026-54274 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 09/13] python3-aiohttp: fix CVE-2026-54275 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 10/13] python3-aiohttp: fix CVE-2026-54277 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 11/13] python3-aiohttp: fix CVE-2026-54278 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 12/13] python3-aiohttp: fix CVE-2026-54279 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-09-28 17:43 ` [oe][meta-python][scarthgap][PATCH 13/13] python3-aiohttp: fix CVE-2026-54273 Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-20  5:56 ` [meta-python][scarthgap][PATCH 00/13] python3-aiohttp: fix multiple CVEs Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.