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