VYPR
Medium severity5.3NVD Advisory· Published Apr 1, 2026· Updated Apr 16, 2026

CVE-2026-34519

CVE-2026-34519

Description

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, an attacker who controls the reason parameter when creating a Response may be able to inject extra headers or similar exploits. This issue has been patched in version 3.13.4.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
aiohttpPyPI
< 3.13.43.13.4

Affected products

1
  • cpe:2.3:a:aiohttp:aiohttp:*:*:*:*:*:*:*:*
    Range: <3.13.4

Patches

1
53b35a2f8869

Restrict reason (#12209) (#12212)

https://github.com/aio-libs/aiohttpSam BullMar 7, 2026via ghsa
6 files changed · +43 8
  • aiohttp/http_writer.py+1 0 modified
    @@ -361,6 +361,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")
    
  • aiohttp/_http_writer.pyx+1 1 modified
    @@ -131,7 +131,7 @@ def _serialize_headers(str status_line, headers):
         _init_writer(&writer, buf)
     
         try:
    -        if _write_str(&writer, status_line) < 0:
    +        if _write_str_raise_on_nlcr(&writer, status_line) < 0:
                 raise
             if _write_byte(&writer, b'\r') < 0:
                 raise
    
  • aiohttp/web_exceptions.py+2 0 modified
    @@ -101,6 +101,8 @@ def __init__(
                     "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,
    
  • aiohttp/web_response.py+2 2 modified
    @@ -158,8 +158,8 @@ def _set_status(self, status: int, reason: Optional[str]) -> None:
             self._status = int(status)
             if reason is None:
                 reason = REASON_PHRASES.get(self._status, "")
    -        elif "\n" in reason:
    -            raise ValueError("Reason cannot contain \\n")
    +        elif "\r" in reason or "\n" in reason:
    +            raise ValueError("Reason cannot contain \\r or \\n")
             self._reason = reason
     
         @property
    
  • tests/test_web_exceptions.py+11 1 modified
    @@ -273,5 +273,15 @@ def test_unicode_text_body_unauthorized() -> None:
     
     
     def test_multiline_reason() -> None:
    -    with pytest.raises(ValueError, match=r"Reason cannot contain \\n"):
    +    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")
    
  • tests/test_web_response.py+26 4 modified
    @@ -13,7 +13,8 @@
     from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
     from re_assert import Matches
     
    -from aiohttp import HttpVersion, HttpVersion10, HttpVersion11, hdrs
    +from aiohttp import HttpVersion, HttpVersion10, HttpVersion11, hdrs, web
    +from aiohttp.abc import AbstractStreamWriter
     from aiohttp.helpers import ETag
     from aiohttp.http_writer import StreamWriter, _serialize_headers
     from aiohttp.multipart import BodyPartReader, MultipartWriter
    @@ -1008,6 +1009,27 @@ def test_set_status_with_empty_reason() -> None:
         assert resp.reason == ""
     
     
    +def test_set_status_reason_with_cr() -> None:
    +    resp = web.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 = web.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 = web.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()
    @@ -1308,9 +1330,9 @@ 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 \\n"):
    -        Response(reason="Bad\r\nInjected-header: foo")
    +async def test_multiline_reason(buf: bytearray, writer: AbstractStreamWriter) -> None:
    +    with pytest.raises(ValueError, match=r"Reason cannot contain \\r or \\n"):
    +        web.Response(reason="Bad\r\nInjected-header: foo")
     
     
     async def test_send_set_cookie_header(buf, writer) -> None:
    

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

5

News mentions

0

No linked articles in our index yet.