VYPR
Low severityOSV Advisory· Published Jan 5, 2026· Updated Jan 6, 2026

AIOHTTP's Unicode processing of header values could cause parsing discrepancies

CVE-2025-69224

Description

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Versions 3.13.2 and below of the Python HTTP parser may allow a request smuggling attack with the presence of non-ASCII characters. If a pure Python version of AIOHTTP is installed (i.e. without the usual C extensions) or AIOHTTP_NO_EXTENSIONS is enabled, then an attacker may be able to execute a request smuggling attack to bypass certain firewalls or proxy protections. This issue is fixed in version 3.13.3.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
aiohttpPyPI
< 3.13.33.13.3

Affected products

1

Patches

1
32677f2adfd9

Reject non-ascii characters in some headers (#11886) (#11902)

https://github.com/aio-libs/aiohttpSam BullJan 3, 2026via ghsa
3 files changed · +42 11
  • aiohttp/http_parser.py+9 7 modified
    @@ -238,7 +238,9 @@ def parse_headers(
     
     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]):
    @@ -549,11 +551,9 @@ def parse_headers(
                     upgrade = True
     
             # encoding
    -        enc = headers.get(hdrs.CONTENT_ENCODING)
    -        if enc:
    -            enc = enc.lower()
    -            if enc in ("gzip", "deflate", "br", "zstd"):
    -                encoding = enc
    +        enc = headers.get(hdrs.CONTENT_ENCODING, "")
    +        if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
    +            encoding = enc
     
             # chunking
             te = headers.get(hdrs.TRANSFER_ENCODING)
    @@ -670,7 +670,9 @@ def parse_message(self, lines: List[bytes]) -> 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`")
    
  • aiohttp/_http_parser.pyx+3 3 modified
    @@ -419,7 +419,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:
    @@ -434,8 +435,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', 'zstd'):
    +            if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
                     encoding = enc
     
             if self._cparser.type == cparser.HTTP_REQUEST:
    
  • tests/test_http_parser.py+30 1 modified
    @@ -476,7 +476,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"
    @@ -574,6 +588,21 @@ def test_compression_zstd(parser: HttpRequestParser) -> None:
         assert msg.compression == "zstd"
     
     
    +@pytest.mark.parametrize(
    +    "enc",
    +    (
    +        "zstd".encode(),  # "st".upper() == "ST"
    +        "deflate".encode(),  # "fl".upper() == "FL"
    +    ),
    +)
    +def test_compression_non_ascii(parser: HttpRequestParser, 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: HttpRequestParser) -> None:
         text = b"GET /test HTTP/1.1\r\ncontent-encoding: compress\r\n\r\n"
         messages, upgrade, tail = parser.feed_data(text)
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.