Skip to content

Commit c7a5388

Browse files
committed
add config and docs for limits
1 parent 62c56e0 commit c7a5388

File tree

7 files changed

+231
-33
lines changed

7 files changed

+231
-33
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Unreleased
1111
- ``Flask.open_resource``/``open_instance_resource`` and
1212
``Blueprint.open_resource`` take an ``encoding`` parameter to use when
1313
opening in text mode. It defaults to ``utf-8``. :issue:`5504`
14+
- ``Request.max_content_length`` can be customized per-request instead of only
15+
through the ``MAX_CONTENT_LENGTH`` config. Added
16+
``MAX_FORM_MEMORY_SIZE`` and ``MAX_FORM_PARTS`` config. Added documentation
17+
about resource limits to the security page. :issue:`5625`
1418

1519

1620
Version 3.0.3

docs/config.rst

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,54 @@ The following configuration values are used internally by Flask:
259259

260260
.. py:data:: MAX_CONTENT_LENGTH
261261
262-
Don't read more than this many bytes from the incoming request data. If not
263-
set and the request does not specify a ``CONTENT_LENGTH``, no data will be
264-
read for security.
262+
The maximum number of bytes that will be read during this request. If
263+
this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge`
264+
error is raised. If it is set to ``None``, no limit is enforced at the
265+
Flask application level. However, if it is ``None`` and the request has no
266+
``Content-Length`` header and the WSGI server does not indicate that it
267+
terminates the stream, then no data is read to avoid an infinite stream.
268+
269+
Each request defaults to this config. It can be set on a specific
270+
:attr:`.Request.max_content_length` to apply the limit to that specific
271+
view. This should be set appropriately based on an application's or view's
272+
specific needs.
265273

266274
Default: ``None``
267275

276+
.. versionadded:: 0.6
277+
278+
.. py:data:: MAX_FORM_MEMORY_SIZE
279+
280+
The maximum size in bytes any non-file form field may be in a
281+
``multipart/form-data`` body. If this limit is exceeded, a 413
282+
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is
283+
set to ``None``, no limit is enforced at the Flask application level.
284+
285+
Each request defaults to this config. It can be set on a specific
286+
:attr:`.Request.max_form_memory_parts` to apply the limit to that specific
287+
view. This should be set appropriately based on an application's or view's
288+
specific needs.
289+
290+
Default: ``500_000``
291+
292+
.. versionadded:: 3.1
293+
294+
.. py:data:: MAX_FORM_PARTS
295+
296+
The maximum number of fields that may be present in a
297+
``multipart/form-data`` body. If this limit is exceeded, a 413
298+
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
299+
is set to ``None``, no limit is enforced at the Flask application level.
300+
301+
Each request defaults to this config. It can be set on a specific
302+
:attr:`.Request.max_form_parts` to apply the limit to that specific view.
303+
This should be set appropriately based on an application's or view's
304+
specific needs.
305+
306+
Default: ``1_000``
307+
308+
.. versionadded:: 3.1
309+
268310
.. py:data:: TEMPLATES_AUTO_RELOAD
269311
270312
Reload templates when they are changed. If not set, it will be enabled in

docs/web-security.rst

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,43 @@
11
Security Considerations
22
=======================
33

4-
Web applications usually face all kinds of security problems and it's very
5-
hard to get everything right. Flask tries to solve a few of these things
6-
for you, but there are a couple more you have to take care of yourself.
4+
Web applications face many types of potential security problems, and it can be
5+
hard to get everything right, or even to know what "right" is in general. Flask
6+
tries to solve a few of these things by default, but there are other parts you
7+
may have to take care of yourself. Many of these solutions are tradeoffs, and
8+
will depend on each application's specific needs and threat model. Many hosting
9+
platforms may take care of certain types of problems without the need for the
10+
Flask application to handle them.
11+
12+
Resource Use
13+
------------
14+
15+
A common category of attacks is "Denial of Service" (DoS or DDoS). This is a
16+
very broad category, and different variants target different layers in a
17+
deployed application. In general, something is done to increase how much
18+
processing time or memory is used to handle each request, to the point where
19+
there are not enough resources to handle legitimate requests.
20+
21+
Flask provides a few configuration options to handle resource use. They can
22+
also be set on individual requests to customize only that request. The
23+
documentation for each goes into more detail.
24+
25+
- :data:`MAX_CONTENT_LENGTH` or :attr:`.Request.max_content_length` controls
26+
how much data will be read from a request. It is not set by default,
27+
although it will still block truly unlimited streams unless the WSGI server
28+
indicates support.
29+
- :data:`MAX_FORM_MEMORY_SIZE` or :attr:`.Request.max_form_memory_size`
30+
controls how large any non-file ``multipart/form-data`` field can be. It is
31+
set to 500kB by default.
32+
- :data:`MAX_FORM_PARTS` or :attr:`.Request.max_form_parts` controls how many
33+
``multipart/form-data`` fields can be parsed. It is set to 1000 by default.
34+
Combined with the default `max_form_memory_size`, this means that a form
35+
will occupy at most 500MB of memory.
36+
37+
Regardless of these settings, you should also review what settings are available
38+
from your operating system, container deployment (Docker etc), WSGI server, HTTP
39+
server, and hosting platform. They typically have ways to set process resource
40+
limits, timeouts, and other checks regardless of how Flask is configured.
741

842
.. _security-xss:
943

src/flask/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ class Flask(App):
192192
"SESSION_COOKIE_SAMESITE": None,
193193
"SESSION_REFRESH_EACH_REQUEST": True,
194194
"MAX_CONTENT_LENGTH": None,
195+
"MAX_FORM_MEMORY_SIZE": 500_000,
196+
"MAX_FORM_PARTS": 1_000,
195197
"SEND_FILE_MAX_AGE_DEFAULT": None,
196198
"TRAP_BAD_REQUEST_ERRORS": None,
197199
"TRAP_HTTP_EXCEPTIONS": False,

src/flask/wrappers.py

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,96 @@ class Request(RequestBase):
5252
#: something similar.
5353
routing_exception: HTTPException | None = None
5454

55+
_max_content_length: int | None = None
56+
_max_form_memory_size: int | None = None
57+
_max_form_parts: int | None = None
58+
5559
@property
56-
def max_content_length(self) -> int | None: # type: ignore[override]
57-
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
58-
if current_app:
59-
return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return]
60-
else:
61-
return None
60+
def max_content_length(self) -> int | None:
61+
"""The maximum number of bytes that will be read during this request. If
62+
this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge`
63+
error is raised. If it is set to ``None``, no limit is enforced at the
64+
Flask application level. However, if it is ``None`` and the request has
65+
no ``Content-Length`` header and the WSGI server does not indicate that
66+
it terminates the stream, then no data is read to avoid an infinite
67+
stream.
68+
69+
Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which
70+
defaults to ``None``. It can be set on a specific ``request`` to apply
71+
the limit to that specific view. This should be set appropriately based
72+
on an application's or view's specific needs.
73+
74+
.. versionchanged:: 3.1
75+
This can be set per-request.
76+
77+
.. versionchanged:: 0.6
78+
This is configurable through Flask config.
79+
"""
80+
if self._max_content_length is not None:
81+
return self._max_content_length
82+
83+
if not current_app:
84+
return super().max_content_length
85+
86+
return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return]
87+
88+
@max_content_length.setter
89+
def max_content_length(self, value: int | None) -> None:
90+
self._max_content_length = value
91+
92+
@property
93+
def max_form_memory_size(self) -> int | None:
94+
"""The maximum size in bytes any non-file form field may be in a
95+
``multipart/form-data`` body. If this limit is exceeded, a 413
96+
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
97+
is set to ``None``, no limit is enforced at the Flask application level.
98+
99+
Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which
100+
defaults to ``500_000``. It can be set on a specific ``request`` to
101+
apply the limit to that specific view. This should be set appropriately
102+
based on an application's or view's specific needs.
103+
104+
.. versionchanged:: 3.1
105+
This is configurable through Flask config.
106+
"""
107+
if self._max_form_memory_size is not None:
108+
return self._max_form_memory_size
109+
110+
if not current_app:
111+
return super().max_form_memory_size
112+
113+
return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return]
114+
115+
@max_form_memory_size.setter
116+
def max_form_memory_size(self, value: int | None) -> None:
117+
self._max_form_memory_size = value
118+
119+
@property # type: ignore[override]
120+
def max_form_parts(self) -> int | None:
121+
"""The maximum number of fields that may be present in a
122+
``multipart/form-data`` body. If this limit is exceeded, a 413
123+
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
124+
is set to ``None``, no limit is enforced at the Flask application level.
125+
126+
Each request defaults to the :data:`MAX_FORM_PARTS` config, which
127+
defaults to ``1_000``. It can be set on a specific ``request`` to apply
128+
the limit to that specific view. This should be set appropriately based
129+
on an application's or view's specific needs.
130+
131+
.. versionchanged:: 3.1
132+
This is configurable through Flask config.
133+
"""
134+
if self._max_form_parts is not None:
135+
return self._max_form_parts
136+
137+
if not current_app:
138+
return super().max_form_parts
139+
140+
return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return]
141+
142+
@max_form_parts.setter
143+
def max_form_parts(self, value: int | None) -> None:
144+
self._max_form_parts = value
62145

63146
@property
64147
def endpoint(self) -> str | None:

tests/test_basic.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,27 +1538,6 @@ def run_simple_mock(*args, **kwargs):
15381538
app.run(debug=debug, use_debugger=use_debugger, use_reloader=use_reloader)
15391539

15401540

1541-
def test_max_content_length(app, client):
1542-
app.config["MAX_CONTENT_LENGTH"] = 64
1543-
1544-
@app.before_request
1545-
def always_first():
1546-
flask.request.form["myfile"]
1547-
AssertionError()
1548-
1549-
@app.route("/accept", methods=["POST"])
1550-
def accept_file():
1551-
flask.request.form["myfile"]
1552-
AssertionError()
1553-
1554-
@app.errorhandler(413)
1555-
def catcher(error):
1556-
return "42"
1557-
1558-
rv = client.post("/accept", data={"myfile": "foo" * 100})
1559-
assert rv.data == b"42"
1560-
1561-
15621541
def test_url_processors(app, client):
15631542
@app.url_defaults
15641543
def add_language_code(endpoint, values):

tests/test_request.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
from flask import Flask
4+
from flask import Request
5+
from flask import request
6+
from flask.testing import FlaskClient
7+
8+
9+
def test_max_content_length(app: Flask, client: FlaskClient) -> None:
10+
app.config["MAX_CONTENT_LENGTH"] = 50
11+
12+
@app.post("/")
13+
def index():
14+
request.form["myfile"]
15+
AssertionError()
16+
17+
@app.errorhandler(413)
18+
def catcher(error):
19+
return "42"
20+
21+
rv = client.post("/", data={"myfile": "foo" * 50})
22+
assert rv.data == b"42"
23+
24+
25+
def test_limit_config(app: Flask):
26+
app.config["MAX_CONTENT_LENGTH"] = 100
27+
app.config["MAX_FORM_MEMORY_SIZE"] = 50
28+
app.config["MAX_FORM_PARTS"] = 3
29+
r = Request({})
30+
31+
# no app context, use Werkzeug defaults
32+
assert r.max_content_length is None
33+
assert r.max_form_memory_size == 500_000
34+
assert r.max_form_parts == 1_000
35+
36+
# in app context, use config
37+
with app.app_context():
38+
assert r.max_content_length == 100
39+
assert r.max_form_memory_size == 50
40+
assert r.max_form_parts == 3
41+
42+
# regardless of app context, use override
43+
r.max_content_length = 90
44+
r.max_form_memory_size = 30
45+
r.max_form_parts = 4
46+
47+
assert r.max_content_length == 90
48+
assert r.max_form_memory_size == 30
49+
assert r.max_form_parts == 4
50+
51+
with app.app_context():
52+
assert r.max_content_length == 90
53+
assert r.max_form_memory_size == 30
54+
assert r.max_form_parts == 4

0 commit comments

Comments
 (0)