From 225412b13f66a76a7222d7719777e6162638faa3 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) (cherry picked from commit 5affd64f86d28a16a8f8e6fea2d217c99bf7831f) CVE: CVE-2025-69224 Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/32677f2adfd907420c078dda6b79225c6f4ebce0] Signed-off-by: Gyorgy Sarvari --- aiohttp/_http_parser.pyx | 6 +++--- aiohttp/http_parser.py | 8 ++++++-- tests/test_http_parser.py | 16 +++++++++++++++- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 16893f0..23f1dd1 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -421,7 +421,8 @@ cdef class HttpParser: headers = CIMultiDictProxy(CIMultiDict(self._headers)) if self._cparser.type == cparser.HTTP_REQUEST: - allowed = upgrade and headers.get("upgrade", "").lower() in ALLOWED_UPGRADES + 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: @@ -436,8 +437,7 @@ 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'): + if enc.isascii() and enc.lower() in {"gzip", "deflate", "br"}: encoding = enc if self._cparser.type == cparser.HTTP_REQUEST: diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 9f864b2..bc8f7da 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]): @@ -664,7 +666,9 @@ class HttpRequestParser(HttpParser[RawRequestMessage]): ) def _is_chunked_te(self, te: str) -> bool: - if te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked": + te = te.rsplit(",", maxsplit=1)[-1].strip(" \t") + # .lower() transforms some non-ascii chars, so must check first. + if te.isascii() and te.lower() == "chunked": return True # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3 raise BadHttpMessage("Request has invalid `Transfer-Encoding`") diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 385452c..d4c1768 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -468,7 +468,21 @@ def test_request_chunked(parser) -> None: assert isinstance(payload, streams.StreamReader) -def test_request_te_chunked_with_content_length(parser: Any) -> None: +def test_te_header_non_ascii(parser: HttpRequestParser) -> 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: HttpRequestParser) -> 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: HttpRequestParser) -> None: text = ( b"GET /test HTTP/1.1\r\n" b"content-length: 1234\r\n"