summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
Diffstat (limited to 'django')
-rw-r--r--django/conf/global_settings.py4
-rw-r--r--django/core/exceptions.py9
-rw-r--r--django/core/handlers/exception.py3
-rw-r--r--django/http/multipartparser.py64
-rw-r--r--django/http/request.py8
5 files changed, 72 insertions, 16 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index ba0f391f8a..44de0c9aa5 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -309,6 +309,10 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
# SuspiciousOperation (TooManyFieldsSent) is raised.
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
+# Maximum number of files encoded in a multipart upload that will be read
+# before a SuspiciousOperation (TooManyFilesSent) is raised.
+DATA_UPLOAD_MAX_NUMBER_FILES = 100
+
# Directory in which upload streamed files will be temporarily saved. A value of
# `None` will make Django use the operating system's default temporary directory
# (i.e. "/tmp" on *nix systems).
diff --git a/django/core/exceptions.py b/django/core/exceptions.py
index 7be4e16bc5..e06b33e7bc 100644
--- a/django/core/exceptions.py
+++ b/django/core/exceptions.py
@@ -67,6 +67,15 @@ class TooManyFieldsSent(SuspiciousOperation):
pass
+class TooManyFilesSent(SuspiciousOperation):
+ """
+ The number of fields in a GET or POST request exceeded
+ settings.DATA_UPLOAD_MAX_NUMBER_FILES.
+ """
+
+ pass
+
+
class RequestDataTooBig(SuspiciousOperation):
"""
The size of the request (excluding any file uploads) exceeded
diff --git a/django/core/handlers/exception.py b/django/core/handlers/exception.py
index 622c53134b..865a02aaee 100644
--- a/django/core/handlers/exception.py
+++ b/django/core/handlers/exception.py
@@ -13,6 +13,7 @@ from django.core.exceptions import (
RequestDataTooBig,
SuspiciousOperation,
TooManyFieldsSent,
+ TooManyFilesSent,
)
from django.http import Http404
from django.http.multipartparser import MultiPartParserError
@@ -111,7 +112,7 @@ def response_for_exception(request, exc):
exc_info=sys.exc_info(),
)
elif isinstance(exc, SuspiciousOperation):
- if isinstance(exc, (RequestDataTooBig, TooManyFieldsSent)):
+ if isinstance(exc, (RequestDataTooBig, TooManyFieldsSent, TooManyFilesSent)):
# POST data can't be accessed again, otherwise the original
# exception would be raised.
request._mark_post_parse_error()
diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py
index 77a4b1f140..64f8c6d4cf 100644
--- a/django/http/multipartparser.py
+++ b/django/http/multipartparser.py
@@ -16,6 +16,7 @@ from django.core.exceptions import (
RequestDataTooBig,
SuspiciousMultipartForm,
TooManyFieldsSent,
+ TooManyFilesSent,
)
from django.core.files.uploadhandler import SkipFile, StopFutureHandlers, StopUpload
from django.utils.datastructures import MultiValueDict
@@ -39,6 +40,7 @@ class InputStreamExhausted(Exception):
RAW = "raw"
FILE = "file"
FIELD = "field"
+FIELD_TYPES = frozenset([FIELD, RAW])
class MultiPartParser:
@@ -109,6 +111,22 @@ class MultiPartParser:
self._upload_handlers = upload_handlers
def parse(self):
+ # Call the actual parse routine and close all open files in case of
+ # errors. This is needed because if exceptions are thrown the
+ # MultiPartParser will not be garbage collected immediately and
+ # resources would be kept alive. This is only needed for errors because
+ # the Request object closes all uploaded files at the end of the
+ # request.
+ try:
+ return self._parse()
+ except Exception:
+ if hasattr(self, "_files"):
+ for _, files in self._files.lists():
+ for fileobj in files:
+ fileobj.close()
+ raise
+
+ def _parse(self):
"""
Parse the POST data and break it into a FILES MultiValueDict and a POST
MultiValueDict.
@@ -154,6 +172,8 @@ class MultiPartParser:
num_bytes_read = 0
# To count the number of keys in the request.
num_post_keys = 0
+ # To count the number of files in the request.
+ num_files = 0
# To limit the amount of data read from the request.
read_size = None
# Whether a file upload is finished.
@@ -169,6 +189,20 @@ class MultiPartParser:
old_field_name = None
uploaded_file = True
+ if (
+ item_type in FIELD_TYPES
+ and settings.DATA_UPLOAD_MAX_NUMBER_FIELDS is not None
+ ):
+ # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FIELDS.
+ num_post_keys += 1
+ # 2 accounts for empty raw fields before and after the
+ # last boundary.
+ if settings.DATA_UPLOAD_MAX_NUMBER_FIELDS + 2 < num_post_keys:
+ raise TooManyFieldsSent(
+ "The number of GET/POST parameters exceeded "
+ "settings.DATA_UPLOAD_MAX_NUMBER_FIELDS."
+ )
+
try:
disposition = meta_data["content-disposition"][1]
field_name = disposition["name"].strip()
@@ -181,17 +215,6 @@ class MultiPartParser:
field_name = force_str(field_name, encoding, errors="replace")
if item_type == FIELD:
- # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FIELDS.
- num_post_keys += 1
- if (
- settings.DATA_UPLOAD_MAX_NUMBER_FIELDS is not None
- and settings.DATA_UPLOAD_MAX_NUMBER_FIELDS < num_post_keys
- ):
- raise TooManyFieldsSent(
- "The number of GET/POST parameters exceeded "
- "settings.DATA_UPLOAD_MAX_NUMBER_FIELDS."
- )
-
# Avoid reading more than DATA_UPLOAD_MAX_MEMORY_SIZE.
if settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None:
read_size = (
@@ -226,6 +249,16 @@ class MultiPartParser:
field_name, force_str(data, encoding, errors="replace")
)
elif item_type == FILE:
+ # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FILES.
+ num_files += 1
+ if (
+ settings.DATA_UPLOAD_MAX_NUMBER_FILES is not None
+ and num_files > settings.DATA_UPLOAD_MAX_NUMBER_FILES
+ ):
+ raise TooManyFilesSent(
+ "The number of files exceeded "
+ "settings.DATA_UPLOAD_MAX_NUMBER_FILES."
+ )
# This is a file, use the handler...
file_name = disposition.get("filename")
if file_name:
@@ -303,8 +336,13 @@ class MultiPartParser:
# Handle file upload completions on next iteration.
old_field_name = field_name
else:
- # If this is neither a FIELD or a FILE, just exhaust the stream.
- exhaust(stream)
+ # If this is neither a FIELD nor a FILE, exhaust the field
+ # stream. Note: There could be an error here at some point,
+ # but there will be at least two RAW types (before and
+ # after the other boundaries). This branch is usually not
+ # reached at all, because a missing content-disposition
+ # header will skip the whole boundary.
+ exhaust(field_stream)
except StopUpload as e:
self._close_files()
if not e.connection_reset:
diff --git a/django/http/request.py b/django/http/request.py
index f32d57ba67..a32953e86e 100644
--- a/django/http/request.py
+++ b/django/http/request.py
@@ -14,7 +14,11 @@ from django.core.exceptions import (
TooManyFieldsSent,
)
from django.core.files import uploadhandler
-from django.http.multipartparser import MultiPartParser, MultiPartParserError
+from django.http.multipartparser import (
+ MultiPartParser,
+ MultiPartParserError,
+ TooManyFilesSent,
+)
from django.utils.datastructures import (
CaseInsensitiveMapping,
ImmutableList,
@@ -367,7 +371,7 @@ class HttpRequest:
data = self
try:
self._post, self._files = self.parse_file_upload(self.META, data)
- except MultiPartParserError:
+ except (MultiPartParserError, TooManyFilesSent):
# An error occurred while parsing POST data. Since when
# formatting the error the request handler might access
# self.POST, set self._post and self._file to prevent