python3-aiohttp: fix CVE-2025-69227

This patch applies the reviewed upstream fix commits shown in
[1] and [2]. The advisory identifying the fix is referenced in
[3].

[1] https://github.com/aio-libs/aiohttp/commit/bc1319ec3cbff9438a758951a30907b072561259
[2] https://github.com/aio-libs/aiohttp/commit/d5bf65f15c0c718b6b95e9bc9d0914a92c51e60f
[3] https://nvd.nist.gov/vuln/detail/CVE-2025-69227

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Darsh Kelaiya
2026-09-01 10:18:03 +05:30
committed by Anuj Mittal
parent 5891d513a9
commit 11baed163d
2 changed files with 164 additions and 0 deletions
@@ -0,0 +1,163 @@
From b05e3498d7b7b48f7b3a512a638fabea4ca4cd9f Mon Sep 17 00:00:00 2001
From: Sam Bull <git@sambull.org>
Date: Sat, 3 Jan 2026 04:53:29 +0000
Subject: [PATCH] Replace asserts with exceptions (#11897) (#11914)
CVE: CVE-2025-69227
Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/bc1319ec3cbff9438a758951a30907b072561259]
Backport Changes:
- Adapted the _read_chunk_from_stream() EOF check to aiohttp 3.9.5,
where _content_eof is updated outside the newer upstream read loop.
- Imported CIMultiDict and CIMultiDictProxy explicitly because
aiohttp 3.9.5's test_multipart module does not import them.
(cherry picked from commit d5bf65f15c0c718b6b95e9bc9d0914a92c51e60f)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
(cherry picked from commit bc1319ec3cbff9438a758951a30907b072561259)
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
aiohttp/multipart.py | 10 ++++------
aiohttp/web_request.py | 8 +++-----
tests/test_multipart.py | 13 ++++++++++++-
tests/test_web_request.py | 24 +++++++++++++++++++++++-
4 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
index 520ee539e..9e5ff9b41 100644
--- a/aiohttp/multipart.py
+++ b/aiohttp/multipart.py
@@ -325,11 +325,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:
@@ -354,7 +351,8 @@ class BodyPartReader:
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")
assert self._prev_chunk is not None
window = self._prev_chunk + chunk
sub = b"\r\n" + self._boundary
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index b3d614186..cd77b7bde 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -713,12 +713,12 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin):
max_size = self._client_max_size
size = 0
- field = await multipart.next()
- while field is not None:
+ while (field := await multipart.next()) is not None:
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
@@ -770,8 +770,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 436b70957..e46eb29bd5 100644
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -6,6 +6,7 @@
from unittest import mock
import pytest
+from multidict import CIMultiDict, CIMultiDictProxy
import aiohttp
from aiohttp import payload
@@ -200,11 +200,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 704fc189a..962092999 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
@@ -629,7 +630,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(
--
2.44.4