diff mbox series

[meta-python,scarthgap,02/13] python3-aiohttp: fix CVE-2026-34516

Message ID 20260928174323.1810308-3-dkelaiya@cisco.com
State New
Headers show
Series python3-aiohttp: fix multiple CVEs | expand

Commit Message

From: Darsh Kelaiya <dkelaiya@cisco.com>

This patch applies the upstream stable-branch fix in [1], which
backports the original upstream commit in [2]. The advisory
identifying the fix is referenced in [3].

[1] https://github.com/aio-libs/aiohttp/commit/8a74257b3804c9aac0bf644af93070f68f6c5a6f
[2] https://github.com/aio-libs/aiohttp/commit/5fe9dfb64400a574f5ba3f2d3b49b7db47567c29
[3] https://nvd.nist.gov/vuln/detail/CVE-2026-34516

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
 .../python3-aiohttp/CVE-2026-34516.patch      | 356 ++++++++++++++++++
 .../python/python3-aiohttp_3.9.5.bb           |   1 +
 2 files changed, 357 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch
diff mbox series

Patch

diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch
new file mode 100644
index 0000000000..47956fe975
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-34516.patch
@@ -0,0 +1,356 @@ 
+From b119720e4e9c19b23f8589dae05b9a39b26995e1 Mon Sep 17 00:00:00 2001
+From: Sam Bull <git@sambull.org>
+Date: Tue, 10 Mar 2026 20:36:38 +0000
+Subject: [PATCH] Restrict multipart header sizes (#12208) (#12228)
+
+CVE: CVE-2026-34516
+Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/8a74257b3804c9aac0bf644af93070f68f6c5a6f]
+
+Backport Changes:
+- Retained aiohttp 3.9.5's Optional/Union annotations in
+  MultipartReader because this version supports Python 3.8 and cannot
+  use the upstream PEP 604 union syntax.
+- Omitted the test_read_boundary_across_chunks() readline() signature
+  update because that test is not present in aiohttp 3.9.5; retained
+  the applicable Stream test-helper update.
+- Applied the multipart header trimming change using rstrip(b"\r\n")
+  for compatibility with the aiohttp 3.9.5 codebase.
+
+(cherry picked from commit 5fe9dfb64400a574f5ba3f2d3b49b7db47567c29)
+(cherry picked from commit 8a74257b3804c9aac0bf644af93070f68f6c5a6f)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ aiohttp/multipart.py      | 27 +++++++++++++++++++---
+ aiohttp/streams.py        | 16 +++++++-----
+ aiohttp/test_utils.py     |  3 +++
+ aiohttp/web_protocol.py   |  7 ++++++
+ aiohttp/web_request.py    |  7 +++++-
+ tests/test_multipart.py   |  3 ++-
+ tests/test_streams.py     |  9 ++++---
+ tests/test_web_request.py | 53 ++++++++++++++++++++++++++++++++++++++-
+ 8 files changed, 106 insertions(+), 17 deletions(-)
+
+diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
+index 9e5ff9b41..d7b993218 100644
+--- a/aiohttp/multipart.py
++++ b/aiohttp/multipart.py
+@@ -37,6 +37,7 @@ from .hdrs import (
+ )
+ from .helpers import CHAR, TOKEN, parse_mimetype, reify
+ from .http import HeadersParser
++from .http_exceptions import BadHttpMessage
+ from .payload import (
+     JsonPayload,
+     LookupError,
+@@ -547,7 +548,14 @@ class MultipartReader:
+     #: Body part reader class for non multipart/* content types.
+     part_reader_cls = BodyPartReader
+ 
+-    def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None:
++    def __init__(
++        self,
++        headers: Mapping[str, str],
++        content: StreamReader,
++        *,
++        max_field_size: int = 8190,
++        max_headers: int = 128,
++    ) -> None:
+         self._mimetype = parse_mimetype(headers[CONTENT_TYPE])
+         assert self._mimetype.type == "multipart", "multipart/* content type expected"
+         if "boundary" not in self._mimetype.parameters:
+@@ -560,6 +568,8 @@ class MultipartReader:
+         self._content = content
+         self._default_charset: Optional[str] = None
+         self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None
++        self._max_field_size = max_field_size
++        self._max_headers = max_headers
+         self._at_eof = False
+         self._at_bof = True
+         self._unread: List[bytes] = []
+@@ -661,7 +671,12 @@ class MultipartReader:
+         if mimetype.type == "multipart":
+             if self.multipart_reader_cls is None:
+                 return type(self)(headers, self._content)
+-            return self.multipart_reader_cls(headers, self._content)
++            return self.multipart_reader_cls(
++                headers,
++                self._content,
++                max_field_size=self._max_field_size,
++                max_headers=self._max_headers,
++            )
+         else:
+             return self.part_reader_cls(
+                 self._boundary,
+@@ -723,12 +738,14 @@ class MultipartReader:
+     async def _read_headers(self) -> "CIMultiDictProxy[str]":
+         lines = []
+         while True:
+-            chunk = await self._content.readline()
++            chunk = await self._content.readline(max_line_length=self._max_field_size)
+-            chunk = chunk.strip()
++            chunk = chunk.rstrip(b"\r\n")
+             lines.append(chunk)
+             if not chunk:
+                 break
+-        parser = HeadersParser()
++            if len(lines) > self._max_headers:
++                raise BadHttpMessage("Too many headers received")
++        parser = HeadersParser(max_field_size=self._max_field_size)
+         headers, raw_headers = parser.parse_headers(lines)
+         return headers
+ 
+diff --git a/aiohttp/streams.py b/aiohttp/streams.py
+index 121528842..dffaf374b 100644
+--- a/aiohttp/streams.py
++++ b/aiohttp/streams.py
+@@ -21,6 +21,7 @@ from .helpers import (
+     set_exception,
+     set_result,
+ )
++from .http_exceptions import LineTooLong
+ from .log import internal_logger
+ 
+ __all__ = (
+@@ -327,10 +328,12 @@ class StreamReader(AsyncStreamReaderMixin):
+         finally:
+             self._waiter = None
+ 
+-    async def readline(self) -> bytes:
+-        return await self.readuntil()
++    async def readline(self, *, max_line_length: Optional[int] = None) -> bytes:
++        return await self.readuntil(max_size=max_line_length)
+ 
+-    async def readuntil(self, separator: bytes = b"\n") -> bytes:
++    async def readuntil(
++        self, separator: bytes = b"\n", *, max_size: Optional[int] = None
++    ) -> bytes:
+         seplen = len(separator)
+         if seplen == 0:
+             raise ValueError("Separator should be at least one-byte string")
+@@ -341,6 +344,7 @@ class StreamReader(AsyncStreamReaderMixin):
+         chunk = b""
+         chunk_size = 0
+         not_enough = True
++        max_size = max_size or self._high_water
+ 
+         while not_enough:
+             while self._buffer and not_enough:
+@@ -355,8 +359,8 @@ class StreamReader(AsyncStreamReaderMixin):
+                 if ichar:
+                     not_enough = False
+ 
+-                if chunk_size > self._high_water:
+-                    raise ValueError("Chunk too big")
++                if chunk_size > max_size:
++                    raise LineTooLong(chunk[:100] + b"...", max_size)
+ 
+             if self._eof:
+                 break
+@@ -572,7 +576,7 @@ class EmptyStreamReader(StreamReader):  # lgtm [py/missing-call-to-init]
+     def feed_data(self, data: bytes, n: int = 0) -> None:
+         pass
+ 
+-    async def readline(self) -> bytes:
++    async def readline(self, *, max_line_length: Optional[int] = None) -> bytes:
+         return b""
+ 
+     async def read(self, n: int = -1) -> bytes:
+diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py
+index a36e85996..a12bb0505 100644
+--- a/aiohttp/test_utils.py
++++ b/aiohttp/test_utils.py
+@@ -638,6 +638,9 @@ def make_mocked_request(
+ 
+     if protocol is sentinel:
+         protocol = mock.Mock()
++        protocol.max_field_size = 8190
++        protocol.max_line_length = 8190
++        protocol.max_headers = 128
+         protocol.transport = transport
+ 
+     if writer is sentinel:
+diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py
+index a06503e0c..a73bb4364 100644
+--- a/aiohttp/web_protocol.py
++++ b/aiohttp/web_protocol.py
+@@ -136,6 +136,9 @@ class RequestHandler(BaseProtocol):
+     KEEPALIVE_RESCHEDULE_DELAY = 1
+ 
+     __slots__ = (
++        "max_field_size",
++        "max_headers",
++        "max_line_size",
+         "_request_count",
+         "_keepalive",
+         "_manager",
+@@ -193,6 +196,10 @@ class RequestHandler(BaseProtocol):
+         self._request_handler: Optional[_RequestHandler] = manager.request_handler
+         self._request_factory: Optional[_RequestFactory] = manager.request_factory
+ 
++        self.max_line_size = max_line_size
++        self.max_headers = max_headers
++        self.max_field_size = max_field_size
++
+         self._tcp_keepalive = tcp_keepalive
+         # placeholder to be replaced on keepalive timeout setup
+         self._keepalive_time = 0.0
+diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
+index cd77b7bde..2ec09565c 100644
+--- a/aiohttp/web_request.py
++++ b/aiohttp/web_request.py
+@@ -687,7 +687,12 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin):
+ 
+     async def multipart(self) -> MultipartReader:
+         """Return async iterator to process BODY as multipart."""
+-        return MultipartReader(self._headers, self._payload)
++        return MultipartReader(
++            self._headers,
++            self._payload,
++            max_field_size=self._protocol.max_field_size,
++            max_headers=self._protocol.max_headers,
++        )
+ 
+     async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
+         """Return POST parameters."""
+diff --git a/tests/test_multipart.py b/tests/test_multipart.py
+index e4a2be1f3..3bc9efd71 100644
+--- a/tests/test_multipart.py
++++ b/tests/test_multipart.py
+@@ -3,6 +3,7 @@ import io
+ import json
+ import pathlib
+ import zlib
++from typing import Optional
+ from unittest import mock
+ 
+ import pytest
+@@ -63,7 +64,7 @@ class Stream:
+     def at_eof(self):
+         return self.content.tell() == len(self.content.getbuffer())
+ 
+-    async def readline(self):
++    async def readline(self, *, max_line_length: Optional[int] = None) -> bytes:
+         return self.content.readline()
+ 
+     def unread_data(self, data):
+diff --git a/tests/test_streams.py b/tests/test_streams.py
+index ed65b567a..2076bc9ba 100644
+--- a/tests/test_streams.py
++++ b/tests/test_streams.py
+@@ -12,6 +12,7 @@ import pytest
+ from re_assert import Matches
+ 
+ from aiohttp import streams
++from aiohttp.http_exceptions import LineTooLong
+ 
+ DATA = b"line1\nline2\nline3\n"
+ 
+@@ -325,7 +326,7 @@ class TestStreamReader:
+         stream.feed_data(b"li")
+         stream.feed_data(b"ne1\nline2\n")
+ 
+-        with pytest.raises(ValueError):
++        with pytest.raises(LineTooLong):
+             await stream.readline()
+         # The buffer should contain the remaining data after exception
+         stream.feed_eof()
+@@ -346,7 +347,7 @@ class TestStreamReader:
+ 
+         loop.call_soon(cb)
+ 
+-        with pytest.raises(ValueError):
++        with pytest.raises(LineTooLong):
+             await stream.readline()
+         data = await stream.read()
+         assert b"chunk3\n" == data
+@@ -436,7 +437,7 @@ class TestStreamReader:
+         stream.feed_data(b"li")
+         stream.feed_data(b"ne1" + separator + b"line2" + separator)
+ 
+-        with pytest.raises(ValueError):
++        with pytest.raises(LineTooLong):
+             await stream.readuntil(separator)
+         # The buffer should contain the remaining data after exception
+         stream.feed_eof()
+@@ -458,7 +459,7 @@ class TestStreamReader:
+ 
+         loop.call_soon(cb)
+ 
+-        with pytest.raises(ValueError, match="Chunk too big"):
++        with pytest.raises(LineTooLong):
+             await stream.readuntil(separator)
+         data = await stream.read()
+         assert b"chunk3#" == data
+diff --git a/tests/test_web_request.py b/tests/test_web_request.py
+index 962092999..c1d61f895 100644
+--- a/tests/test_web_request.py
++++ b/tests/test_web_request.py
+@@ -11,6 +11,7 @@ from yarl import URL
+ 
+ from aiohttp import HttpVersion
+ from aiohttp.base_protocol import BaseProtocol
++from aiohttp.http_exceptions import BadHttpMessage, LineTooLong
+ from aiohttp.http_parser import RawRequestMessage
+ from aiohttp.streams import StreamReader
+ from aiohttp.test_utils import make_mocked_request
+@@ -676,7 +677,57 @@ async def test_multipart_formdata_file(protocol: BaseProtocol) -> None:
+     result["a_file"].file.close()
+ 
+ 
+-async def test_make_too_big_request_limit_None(protocol) -> None:
++async def test_multipart_formdata_headers_too_many(protocol: BaseProtocol) -> None:
++    many = b"".join(f"X-{i}: a\r\n".encode() for i in range(130))
++    body = (
++        b"--b\r\n"
++        b'Content-Disposition: form-data; name="a"\r\n' + many + b"\r\n1\r\n"
++        b"--b--\r\n"
++    )
++    content_type = "multipart/form-data; boundary=b"
++    payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
++    payload.feed_data(body)
++    payload.feed_eof()
++    req = make_mocked_request(
++        "POST",
++        "/",
++        headers={"CONTENT-TYPE": content_type},
++        payload=payload,
++    )
++
++    with pytest.raises(BadHttpMessage, match="Too many headers received"):
++        await req.post()
++
++
++async def test_multipart_formdata_header_too_long(protocol: BaseProtocol) -> None:
++    k = b"t" * 4100
++    body = (
++        b"--b\r\n"
++        b'Content-Disposition: form-data; name="a"\r\n'
++        + k
++        + b":"
++        + k
++        + b"\r\n"
++        + b"\r\n1\r\n"
++        b"--b--\r\n"
++    )
++    content_type = "multipart/form-data; boundary=b"
++    payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
++    payload.feed_data(body)
++    payload.feed_eof()
++    req = make_mocked_request(
++        "POST",
++        "/",
++        headers={"CONTENT-TYPE": content_type},
++        payload=payload,
++    )
++
++    match = "400, message:\n  Got more than 8190 bytes when reading"
++    with pytest.raises(LineTooLong, match=match):
++        await req.post()
++
++
++async def test_make_too_big_request_limit_None(protocol: BaseProtocol) -> None:
+     payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
+     large_file = 1024**2 * b"x"
+     too_large_file = large_file + b"x"
+-- 
+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 400a4a838b..d7c7a5014a 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
@@ -22,6 +22,7 @@  SRC_URI += "file://CVE-2024-52304.patch \
            file://CVE-2026-34993.patch \
            file://CVE-2026-34518.patch \
            file://CVE-2026-34519.patch \
+           file://CVE-2026-34516.patch \
            "
 
 CVE_STATUS[CVE-2026-34515] = "not-applicable-platform: Vulnerability only affects applications running on Windows"