summaryrefslogtreecommitdiff
path: root/django/http/multipartparser.py
diff options
context:
space:
mode:
Diffstat (limited to 'django/http/multipartparser.py')
-rw-r--r--django/http/multipartparser.py62
1 files changed, 51 insertions, 11 deletions
diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py
index 35a54f4ca1..d8a304d4ba 100644
--- a/django/http/multipartparser.py
+++ b/django/http/multipartparser.py
@@ -14,6 +14,7 @@ from urllib.parse import unquote
from django.conf import settings
from django.core.exceptions import (
RequestDataTooBig, SuspiciousMultipartForm, TooManyFieldsSent,
+ TooManyFilesSent,
)
from django.core.files.uploadhandler import (
SkipFile, StopFutureHandlers, StopUpload,
@@ -38,6 +39,7 @@ class InputStreamExhausted(Exception):
RAW = "raw"
FILE = "file"
FIELD = "field"
+FIELD_TYPES = frozenset([FIELD, RAW])
class MultiPartParser:
@@ -102,6 +104,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.
@@ -147,6 +165,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.
@@ -162,6 +182,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()
@@ -174,15 +208,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 = settings.DATA_UPLOAD_MAX_MEMORY_SIZE - num_bytes_read
@@ -208,6 +233,16 @@ class MultiPartParser:
self._post.appendlist(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:
@@ -276,8 +311,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: