1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
From 635c8c03b609b1099d93fb8ea8c8691624237b0f Mon Sep 17 00:00:00 2001
From: Gyorgy Sarvari <skandigraun@gmail.com>
Date: Sat, 3 Jan 2026 04:53:29 +0000
Subject: [PATCH] Replace asserts with exceptions (#11897) (#11914)
From: Sam Bull <git@sambull.org>
(cherry picked from commit d5bf65f15c0c718b6b95e9bc9d0914a92c51e60f)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
CVE: CVE-2025-69227
Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/bc1319ec3cbff9438a758951a30907b072561259]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
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(
|