From 635c8c03b609b1099d93fb8ea8c8691624237b0f Mon Sep 17 00:00:00 2001 From: Gyorgy Sarvari Date: Sat, 3 Jan 2026 04:53:29 +0000 Subject: [PATCH] Replace asserts with exceptions (#11897) (#11914) From: Sam Bull (cherry picked from commit d5bf65f15c0c718b6b95e9bc9d0914a92c51e60f) Co-authored-by: J. Nick Koston CVE: CVE-2025-69227 Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/bc1319ec3cbff9438a758951a30907b072561259] Signed-off-by: Gyorgy Sarvari --- aiohttp/multipart.py | 10 ++++------ aiohttp/web_request.py | 8 +++----- tests/test_multipart.py | 12 +++++++++++- tests/test_web_request.py | 24 +++++++++++++++++++++++- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 54dfd48..7783ac5 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -357,11 +357,8 @@ class BodyPartReader: self._read_bytes += len(chunk) if self._read_bytes == self._length: self._at_eof = True - if self._at_eof: - clrf = await self._content.readline() - assert ( - b"\r\n" == clrf - ), "reader did not read all the data or it is malformed" + if self._at_eof and await self._content.readline() != b"\r\n": + raise ValueError("Reader did not read all the data or it is malformed") return chunk async def _read_chunk_from_length(self, size: int) -> bytes: @@ -390,7 +387,8 @@ class BodyPartReader: while len(chunk) < self._boundary_len: chunk += await self._content.read(size) self._content_eof += int(self._content.at_eof()) - assert self._content_eof < 3, "Reading after EOF" + if self._content_eof > 2: + raise ValueError("Reading after EOF") if self._content_eof: break if len(chunk) > size: diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 6e09027..96222b0 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -721,13 +721,13 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): multipart = await self.multipart() max_size = self._client_max_size - field = await multipart.next() - while field is not None: + while (field := await multipart.next()) is not None: size = 0 field_ct = field.headers.get(hdrs.CONTENT_TYPE) if isinstance(field, BodyPartReader): - assert field.name is not None + if field.name is None: + raise ValueError("Multipart field missing name.") # Note that according to RFC 7578, the Content-Type header # is optional, even for files, so we can't assume it's @@ -779,8 +779,6 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): raise ValueError( "To decode nested multipart you need to use custom reader", ) - - field = await multipart.next() else: data = await self.read() if data: diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 75b73a7..5351945 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -221,11 +221,21 @@ class TestPartReader: with Stream(data) as stream: obj = aiohttp.BodyPartReader(BOUNDARY, {}, stream) result = b"" - with pytest.raises(AssertionError): + with pytest.raises(ValueError): for _ in range(4): result += await obj.read_chunk(7) assert data == result + async def test_read_with_content_length_malformed_crlf(self) -> None: + # Content-Length is correct but data after content is not \r\n + content = b"Hello" + h = CIMultiDictProxy(CIMultiDict({"CONTENT-LENGTH": str(len(content))})) + # Malformed: "XX" instead of "\r\n" after content + with Stream(content + b"XX--:--") as stream: + obj = aiohttp.BodyPartReader(BOUNDARY, h, stream) + with pytest.raises(ValueError, match="malformed"): + await obj.read() + async def test_read_boundary_with_incomplete_chunk(self) -> None: with Stream(b"") as stream: diff --git a/tests/test_web_request.py b/tests/test_web_request.py index da80ca9..125b95e 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -10,6 +10,7 @@ from multidict import CIMultiDict, CIMultiDictProxy, MultiDict from yarl import URL from aiohttp import HttpVersion +from aiohttp.base_protocol import BaseProtocol from aiohttp.http_parser import RawRequestMessage from aiohttp.streams import StreamReader from aiohttp.test_utils import make_mocked_request @@ -815,7 +816,28 @@ async def test_multipart_formdata(protocol) -> None: assert dict(result) == {"a": "b", "c": "d"} -async def test_multipart_formdata_file(protocol) -> None: +async def test_multipart_formdata_field_missing_name(protocol: BaseProtocol) -> None: + # Ensure ValueError is raised when Content-Disposition has no name + payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) + payload.feed_data( + b"-----------------------------326931944431359\r\n" + b"Content-Disposition: form-data\r\n" # Missing name! + b"\r\n" + b"value\r\n" + b"-----------------------------326931944431359--\r\n" + ) + content_type = ( + "multipart/form-data; boundary=---------------------------326931944431359" + ) + payload.feed_eof() + req = make_mocked_request( + "POST", "/", headers={"CONTENT-TYPE": content_type}, payload=payload + ) + with pytest.raises(ValueError, match="Multipart field missing name"): + await req.post() + + +async def test_multipart_formdata_file(protocol: BaseProtocol) -> None: # Make sure file uploads work, even without a content type payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop()) payload.feed_data(