mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-07 13:44:50 +00:00
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> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
357 lines
13 KiB
Diff
357 lines
13 KiB
Diff
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
|