new file mode 100644
@@ -0,0 +1,161 @@
+From 4c27b675ef3d1c5b3b0f379525be575ec0d8d15e Mon Sep 17 00:00:00 2001
+From: Sam Bull <git@sambull.org>
+Date: Sat, 3 Jan 2026 00:02:45 +0000
+Subject: [PATCH] Reject non-ascii characters in some headers (#11886) (#11902)
+
+CVE: CVE-2025-69224
+Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/32677f2adfd907420c078dda6b79225c6f4ebce0]
+
+Backport Changes:
+- Adapted the pure-Python `Transfer-Encoding` validation inline because
+ aiohttp 3.9.5 lacks the upstream `_is_chunked_te()` call path.
+- Backported the request/response upgrade distinction needed to apply the
+ non-ASCII request-header check while preserving CONNECT and HTTP 101
+ response handling.
+- Added the `ALLOWED_UPGRADES` definition from upstream prerequisite commit
+ c99a1e27375285149ea82cbdcc2f2c40e57596dc because aiohttp 3.9.5
+ predates it and the Cython parser otherwise fails to compile.
+- Retained aiohttp 3.9.5's supported `gzip`, `deflate`, and `br` content
+ encodings; omitted upstream zstd-specific code and test context because
+ this version lacks zstd decompression support.
+- Retained target-compatible `Any` test annotations because the older test
+ module does not import `HttpRequestParser`.
+
+(cherry picked from commit 5affd64f86d28a16a8f8e6fea2d217c99bf7831f)
+(cherry picked from commit 32677f2adfd907420c078dda6b79225c6f4ebce0)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ aiohttp/_http_parser.pyx | 18 +++++++++++++-----
+ aiohttp/http_parser.py | 8 +++++---
+ tests/test_http_parser.py | 32 ++++++++++++++++++++++++++++++--
+ 3 files changed, 48 insertions(+), 10 deletions(-)
+
+diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
+index 7ea9b32ca..f1c130395 100644
+--- a/aiohttp/_http_parser.pyx
++++ b/aiohttp/_http_parser.pyx
+@@ -47,6 +47,7 @@ include "_headers.pxi"
+
+ from aiohttp cimport _find_header
+
++ALLOWED_UPGRADES = frozenset({"websocket"})
+ DEF DEFAULT_FREELIST_SIZE = 250
+
+ cdef extern from "Python.h":
+@@ -425,8 +426,14 @@ cdef class HttpParser:
+ raw_headers = tuple(self._raw_headers)
+ headers = CIMultiDictProxy(self._headers)
+
+- if upgrade or self._cparser.method == cparser.HTTP_CONNECT:
+- self._upgraded = True
++ if self._cparser.type == cparser.HTTP_REQUEST:
++ h_upg = headers.get("upgrade", "")
++ allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES
++ if allowed or self._cparser.method == cparser.HTTP_CONNECT:
++ self._upgraded = True
++ else:
++ if upgrade and self._cparser.status_code == 101:
++ self._upgraded = True
+
+ # do not support old websocket spec
+ if SEC_WEBSOCKET_KEY1 in headers:
+@@ -436,9 +443,10 @@ cdef class HttpParser:
+ enc = self._content_encoding
+ if enc is not None:
+ self._content_encoding = None
+- enc = enc.lower()
+- if enc in ('gzip', 'deflate', 'br'):
+- encoding = enc
++ if enc.isascii():
++ enc = enc.lower()
++ if enc in ('gzip', 'deflate', 'br'):
++ encoding = enc
+
+ if self._cparser.type == cparser.HTTP_REQUEST:
+ msg = _new_request_message(
+diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
+index 0a80c5c6d..5768bd623 100644
+--- a/aiohttp/http_parser.py
++++ b/aiohttp/http_parser.py
+@@ -232,7 +232,9 @@ class HeadersParser:
+
+ def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool:
+ """Check if the upgrade header is supported."""
+- return headers.get(hdrs.UPGRADE, "").lower() in {"tcp", "websocket"}
++ u = headers.get(hdrs.UPGRADE, "")
++ # .lower() can transform non-ascii characters.
++ return u.isascii() and u.lower() in {"tcp", "websocket"}
+
+
+ class HttpParser(abc.ABC, Generic[_MsgT]):
+@@ -542,7 +544,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+
+ # encoding
+ enc = headers.get(hdrs.CONTENT_ENCODING)
+- if enc:
++ if enc and enc.isascii():
+ enc = enc.lower()
+ if enc in ("gzip", "deflate", "br"):
+ encoding = enc
+@@ -550,7 +552,7 @@ class HttpParser(abc.ABC, Generic[_MsgT]):
+ # chunking
+ te = headers.get(hdrs.TRANSFER_ENCODING)
+ if te is not None:
+- if "chunked" == te.lower():
++ if te.isascii() and "chunked" == te.lower():
+ chunked = True
+ else:
+ raise BadHttpMessage("Request has invalid `Transfer-Encoding`")
+diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
+index 2f34f0bc0..021b6e4ae 100644
+--- a/tests/test_http_parser.py
++++ b/tests/test_http_parser.py
+@@ -482,6 +482,20 @@ def test_request_chunked(parser) -> None:
+ assert isinstance(payload, streams.StreamReader)
+
+
++def test_te_header_non_ascii(parser: Any) -> None:
++ # K = Kelvin sign, not valid ascii.
++ text = "GET /test HTTP/1.1\r\nTransfer-Encoding: chunKed\r\n\r\n"
++ with pytest.raises(http_exceptions.BadHttpMessage):
++ parser.feed_data(text.encode())
++
++
++def test_upgrade_header_non_ascii(parser: Any) -> None:
++ # K = Kelvin sign, not valid ascii.
++ text = "GET /test HTTP/1.1\r\nUpgrade: websocKet\r\n\r\n"
++ messages, upgrade, tail = parser.feed_data(text.encode())
++ assert not upgrade
++
++
+ def test_request_te_chunked_with_content_length(parser: Any) -> None:
+ text = (
+ b"GET /test HTTP/1.1\r\n"
+@@ -555,8 +569,22 @@ def test_compression_brotli(parser) -> None:
+ assert msg.compression == "br"
+
+
+-def test_compression_unknown(parser) -> None:
+- text = b"GET /test HTTP/1.1\r\n" b"content-encoding: compress\r\n\r\n"
++@pytest.mark.parametrize(
++ "enc",
++ (
++ "deflate".encode(), # "fl".upper() == "FL"
++ ),
++)
++def test_compression_non_ascii(parser: Any, enc: bytes) -> None:
++ text = b"GET /test HTTP/1.1\r\ncontent-encoding: " + enc + b"\r\n\r\n"
++ messages, upgrade, tail = parser.feed_data(text)
++ msg = messages[0][0]
++ # Non-ascii input should not evaluate to a valid encoding scheme.
++ assert msg.compression is None
++
++
++def test_compression_unknown(parser: Any) -> None:
++ text = b"GET /test HTTP/1.1\r\ncontent-encoding: compress\r\n\r\n"
+ messages, upgrade, tail = parser.feed_data(text)
+ msg = messages[0][0]
+ assert msg.compression is None
+--
+2.35.6
+
@@ -8,6 +8,7 @@ SRC_URI[sha256sum] = "edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d
SRC_URI += "file://CVE-2024-52304.patch \
file://CVE-2025-53643.patch \
+ file://CVE-2025-69224.patch \
file://CVE-2025-69225.patch \
file://CVE-2025-69226.patch \
file://CVE-2025-69228.patch \
@@ -16,6 +17,15 @@ SRC_URI += "file://CVE-2024-52304.patch \
PYPI_PACKAGE = "aiohttp"
inherit python_setuptools_build_meta pypi
+DEPENDS += "python3-cython-native"
+
+do_configure:prepend() {
+ cython3 -3 -Werror \
+ -I ${S}/aiohttp \
+ -o ${S}/aiohttp/_http_parser.c \
+ ${S}/aiohttp/_http_parser.pyx
+}
+
RDEPENDS:${PN} = "\
python3-aiohappyeyeballs \
python3-aiosignal \