Skip to content

Commit abb04a5

Browse files
committed
Support max_form_parts and max_form_memory_size
These allow greater control over safer form parsing with the former limiting the number of parts and the latter limiting any individual (data) parts maximum size in bytes. The default values are taken from Flask.
1 parent f33a15f commit abb04a5

File tree

5 files changed

+97
-27
lines changed

5 files changed

+97
-27
lines changed

src/quart/app.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ class Quart(App):
248248
"EXPLAIN_TEMPLATE_LOADING": False,
249249
"MAX_CONTENT_LENGTH": 16 * 1024 * 1024, # 16 MB Limit
250250
"MAX_COOKIE_SIZE": 4093,
251+
"MAX_FORM_MEMORY_SIZE": 500_000,
252+
"MAX_FORM_PARTS": 1_000,
251253
"PERMANENT_SESSION_LIFETIME": timedelta(days=31),
252254
# Replaces PREFERRED_URL_SCHEME to allow for WebSocket scheme
253255
"PREFER_SECURE_URLS": False,
@@ -1130,8 +1132,9 @@ async def handle_websocket_exception(
11301132

11311133
def log_exception(
11321134
self,
1133-
exception_info: tuple[type, BaseException, TracebackType]
1134-
| tuple[None, None, None],
1135+
exception_info: (
1136+
tuple[type, BaseException, TracebackType] | tuple[None, None, None]
1137+
),
11351138
) -> None:
11361139
"""Log a exception to the :attr:`logger`.
11371140

src/quart/formparser.py

+35-16
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,20 @@ class FormDataParser:
4343

4444
def __init__(
4545
self,
46-
stream_factory: StreamFactory = default_stream_factory,
47-
max_form_memory_size: int | None = None,
48-
max_content_length: int | None = None,
46+
*,
4947
cls: type[MultiDict] | None = MultiDict,
48+
max_content_length: int | None = None,
49+
max_form_memory_size: int | None = None,
50+
max_form_parts: int | None = None,
5051
silent: bool = True,
52+
stream_factory: StreamFactory = default_stream_factory,
5153
) -> None:
52-
self.stream_factory = stream_factory
5354
self.cls = cls
55+
self.max_content_length = max_content_length
56+
self.max_form_memory_size = max_form_memory_size
57+
self.max_form_parts = max_form_parts
5458
self.silent = silent
59+
self.stream_factory = stream_factory
5560

5661
def get_parse_func(
5762
self, mimetype: str, options: dict[str, str]
@@ -87,9 +92,12 @@ async def _parse_multipart(
8792
options: dict[str, str],
8893
) -> tuple[MultiDict, MultiDict]:
8994
parser = MultiPartParser(
90-
self.stream_factory,
9195
cls=self.cls,
9296
file_storage_cls=self.file_storage_class,
97+
max_content_length=self.max_content_length,
98+
max_form_memory_size=self.max_form_memory_size,
99+
max_form_parts=self.max_form_parts,
100+
stream_factory=self.stream_factory,
93101
)
94102
boundary = options.get("boundary", "").encode("ascii")
95103

@@ -105,10 +113,14 @@ async def _parse_urlencoded(
105113
content_length: int | None,
106114
options: dict[str, str],
107115
) -> tuple[MultiDict, MultiDict]:
108-
form = parse_qsl(
109-
(await body).decode(),
110-
keep_blank_values=True,
111-
)
116+
try:
117+
form = parse_qsl(
118+
(await body).decode(),
119+
keep_blank_values=True,
120+
max_num_fields=self.max_form_parts,
121+
)
122+
except ValueError:
123+
raise RequestEntityTooLarge() from None
112124
return self.cls(form), self.cls()
113125

114126
parse_functions: dict[str, ParserFunc] = {
@@ -121,17 +133,22 @@ async def _parse_urlencoded(
121133
class MultiPartParser:
122134
def __init__(
123135
self,
124-
stream_factory: StreamFactory = default_stream_factory,
125-
max_form_memory_size: int | None = None,
126-
cls: type[MultiDict] = MultiDict,
136+
*,
127137
buffer_size: int = 64 * 1024,
138+
cls: type[MultiDict] = MultiDict,
128139
file_storage_cls: type[FileStorage] = FileStorage,
140+
max_content_length: int | None = None,
141+
max_form_memory_size: int | None = None,
142+
max_form_parts: int | None = None,
143+
stream_factory: StreamFactory = default_stream_factory,
129144
) -> None:
130-
self.max_form_memory_size = max_form_memory_size
131-
self.stream_factory = stream_factory
132-
self.cls = cls
133145
self.buffer_size = buffer_size
146+
self.cls = cls
134147
self.file_storage_cls = file_storage_cls
148+
self.max_content_length = max_content_length
149+
self.max_form_memory_size = max_form_memory_size
150+
self.max_form_parts = max_form_parts
151+
self.stream_factory = stream_factory
135152

136153
def fail(self, message: str) -> NoReturn:
137154
raise ValueError(message)
@@ -172,7 +189,9 @@ async def parse(
172189
container: IO[bytes] | list[bytes]
173190
_write: Callable[[bytes], Any]
174191

175-
parser = MultipartDecoder(boundary, self.max_form_memory_size)
192+
parser = MultipartDecoder(
193+
boundary, self.max_content_length, max_parts=self.max_form_parts
194+
)
176195

177196
fields = []
178197
files = []

src/quart/wrappers/base.py

-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from werkzeug.sansio.request import Request as SansIORequest
99

1010
from .. import json
11-
from ..globals import current_app
1211

1312
if TYPE_CHECKING:
1413
from ..routing import QuartRule # noqa
@@ -73,14 +72,6 @@ def __init__(
7372
self.http_version = http_version
7473
self.scope = scope
7574

76-
@property
77-
def max_content_length(self) -> int | None:
78-
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
79-
if current_app:
80-
return current_app.config["MAX_CONTENT_LENGTH"]
81-
else:
82-
return None
83-
8475
@property
8576
def endpoint(self) -> str | None:
8677
"""Returns the corresponding endpoint matched for this request.

src/quart/wrappers/request.py

+47
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ class Request(BaseRequestWebsocket):
141141
body_class = Body
142142
form_data_parser_class = FormDataParser
143143
lock_class = asyncio.Lock
144+
_max_content_length: int | None = None
145+
_max_form_memory_size: int | None = None
146+
_max_form_parts: int | None = None
144147

145148
def __init__(
146149
self,
@@ -189,6 +192,48 @@ def __init__(
189192
self._parsing_lock = self.lock_class()
190193
self._send_push_promise = send_push_promise
191194

195+
@property
196+
def max_content_length(self) -> int | None:
197+
if self._max_content_length is not None:
198+
return self._max_content_length
199+
200+
if current_app:
201+
return current_app.config["MAX_CONTENT_LENGTH"]
202+
203+
return None
204+
205+
@max_content_length.setter
206+
def max_content_length(self, value: int | None) -> None:
207+
self._max_content_length = value
208+
209+
@property
210+
def max_form_memory_size(self) -> int | None:
211+
if self._max_form_memory_size is not None:
212+
return self._max_form_memory_size
213+
214+
if current_app:
215+
return current_app.config["MAX_FORM_MEMORY_SIZE"]
216+
217+
return None
218+
219+
@max_form_memory_size.setter
220+
def max_form_memory_size(self, value: int | None) -> None:
221+
self._max_form_memory_size = value
222+
223+
@property
224+
def max_form_parts(self) -> int | None:
225+
if self._max_form_parts is not None:
226+
return self._max_form_parts
227+
228+
if current_app:
229+
return current_app.config["MAX_FORM_PARTS"]
230+
231+
return None
232+
233+
@max_form_parts.setter
234+
def max_form_parts(self, value: int | None) -> None:
235+
self._max_form_parts = value
236+
192237
@property
193238
async def stream(self) -> NoReturn:
194239
raise NotImplementedError("Use body instead")
@@ -284,6 +329,8 @@ async def files(self) -> MultiDict:
284329
def make_form_data_parser(self) -> FormDataParser:
285330
return self.form_data_parser_class(
286331
max_content_length=self.max_content_length,
332+
max_form_memory_size=self.max_form_memory_size,
333+
max_form_parts=self.max_form_parts,
287334
cls=self.parameter_storage_class,
288335
)
289336

tests/test_formparser.py

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from werkzeug.exceptions import RequestEntityTooLarge
55

6+
from quart.formparser import FormDataParser
67
from quart.formparser import MultiPartParser
78
from quart.wrappers.request import Body
89

@@ -19,3 +20,12 @@ async def test_multipart_max_form_memory_size() -> None:
1920

2021
with pytest.raises(RequestEntityTooLarge):
2122
await parser.parse(body, b"bound", 0)
23+
24+
25+
async def test_formparser_max_num_parts() -> None:
26+
parser = FormDataParser(max_form_parts=1)
27+
body = Body(None, None)
28+
body.set_result(b"param1=data1&param2=data2&param3=data3")
29+
30+
with pytest.raises(RequestEntityTooLarge):
31+
await parser.parse(body, "application/x-url-encoded", None)

0 commit comments

Comments
 (0)