* [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