mirror of
https://github.com/django/django.git
synced 2025-10-31 09:41:08 +00:00
Fixed CVE-2023-24580 -- Prevented DoS with too many uploaded files.
Thanks to Jakob Ackermann for the report.
This commit is contained in:
committed by
Carlton Gibson
parent
1eb94bc8da
commit
85ac33591c
@@ -313,6 +313,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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.core.exceptions import (
|
||||
RequestDataTooBig,
|
||||
SuspiciousOperation,
|
||||
TooManyFieldsSent,
|
||||
TooManyFilesSent,
|
||||
)
|
||||
from django.http import Http404
|
||||
from django.http.multipartparser import MultiPartParserError
|
||||
@@ -110,7 +111,7 @@ def response_for_exception(request, exc):
|
||||
exception=exc,
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -14,6 +14,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:
|
||||
@@ -111,6 +113,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.
|
||||
@@ -156,6 +174,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.
|
||||
@@ -171,6 +191,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()
|
||||
@@ -183,17 +217,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 = (
|
||||
@@ -228,6 +251,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:
|
||||
@@ -305,8 +338,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:
|
||||
|
||||
@@ -13,7 +13,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,
|
||||
@@ -382,7 +386,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
|
||||
|
||||
Reference in New Issue
Block a user