new file mode 100644
@@ -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
@@ -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"