Apply all patches up to CVE-2023-36053
This commit is contained in:
parent
c669cf279a
commit
11bb365c7b
@ -304,6 +304,10 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
|
|||||||
# SuspiciousOperation (TooManyFieldsSent) is raised.
|
# SuspiciousOperation (TooManyFieldsSent) is raised.
|
||||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
|
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
|
# 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
|
# `None` will make Django use the operating system's default temporary directory
|
||||||
# (i.e. "/tmp" on *nix systems).
|
# (i.e. "/tmp" on *nix systems).
|
||||||
|
@ -61,6 +61,15 @@ class TooManyFieldsSent(SuspiciousOperation):
|
|||||||
pass
|
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):
|
class RequestDataTooBig(SuspiciousOperation):
|
||||||
"""
|
"""
|
||||||
The size of the request (excluding any file uploads) exceeded
|
The size of the request (excluding any file uploads) exceeded
|
||||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
|||||||
from django.core import signals
|
from django.core import signals
|
||||||
from django.core.exceptions import (
|
from django.core.exceptions import (
|
||||||
PermissionDenied, RequestDataTooBig, SuspiciousOperation,
|
PermissionDenied, RequestDataTooBig, SuspiciousOperation,
|
||||||
TooManyFieldsSent,
|
TooManyFieldsSent, TooManyFilesSent,
|
||||||
)
|
)
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.http.multipartparser import MultiPartParserError
|
from django.http.multipartparser import MultiPartParserError
|
||||||
@ -67,7 +67,7 @@ def response_for_exception(request, exc):
|
|||||||
response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc)
|
response = get_exception_response(request, get_resolver(get_urlconf()), 400, exc)
|
||||||
|
|
||||||
elif isinstance(exc, SuspiciousOperation):
|
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
|
# POST data can't be accessed again, otherwise the original
|
||||||
# exception would be raised.
|
# exception would be raised.
|
||||||
request._mark_post_parse_error()
|
request._mark_post_parse_error()
|
||||||
|
@ -106,6 +106,8 @@ class URLValidator(RegexValidator):
|
|||||||
r'\Z', re.IGNORECASE)
|
r'\Z', re.IGNORECASE)
|
||||||
message = _('Enter a valid URL.')
|
message = _('Enter a valid URL.')
|
||||||
schemes = ['http', 'https', 'ftp', 'ftps']
|
schemes = ['http', 'https', 'ftp', 'ftps']
|
||||||
|
unsafe_chars = frozenset('\t\r\n')
|
||||||
|
max_length = 2048
|
||||||
|
|
||||||
def __init__(self, schemes=None, **kwargs):
|
def __init__(self, schemes=None, **kwargs):
|
||||||
super(URLValidator, self).__init__(**kwargs)
|
super(URLValidator, self).__init__(**kwargs)
|
||||||
@ -114,6 +116,10 @@ class URLValidator(RegexValidator):
|
|||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
value = force_text(value)
|
value = force_text(value)
|
||||||
|
if not isinstance(value, str) or len(value) > self.max_length:
|
||||||
|
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||||
|
if self.unsafe_chars.intersection(value):
|
||||||
|
raise ValidationError(self.message, code=self.code)
|
||||||
# Check first if the scheme is valid
|
# Check first if the scheme is valid
|
||||||
scheme = value.split('://')[0].lower()
|
scheme = value.split('://')[0].lower()
|
||||||
if scheme not in self.schemes:
|
if scheme not in self.schemes:
|
||||||
@ -194,8 +200,9 @@ class EmailValidator(object):
|
|||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
value = force_text(value)
|
value = force_text(value)
|
||||||
|
# The maximum length of an email is 320 characters per RFC 3696
|
||||||
if not value or '@' not in value:
|
# section 3.
|
||||||
|
if not value or '@' not in value or len(value) > 320:
|
||||||
raise ValidationError(self.message, code=self.code)
|
raise ValidationError(self.message, code=self.code)
|
||||||
|
|
||||||
user_part, domain_part = value.rsplit('@', 1)
|
user_part, domain_part = value.rsplit('@', 1)
|
||||||
|
@ -24,10 +24,12 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
|||||||
c.execute('PRAGMA foreign_keys')
|
c.execute('PRAGMA foreign_keys')
|
||||||
self._initial_pragma_fk = c.fetchone()[0]
|
self._initial_pragma_fk = c.fetchone()[0]
|
||||||
c.execute('PRAGMA foreign_keys = 0')
|
c.execute('PRAGMA foreign_keys = 0')
|
||||||
|
self.connection.cursor().execute('PRAGMA legacy_alter_table = ON')
|
||||||
return super(DatabaseSchemaEditor, self).__enter__()
|
return super(DatabaseSchemaEditor, self).__enter__()
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
super(DatabaseSchemaEditor, self).__exit__(exc_type, exc_value, traceback)
|
super(DatabaseSchemaEditor, self).__exit__(exc_type, exc_value, traceback)
|
||||||
|
self.connection.cursor().execute('PRAGMA legacy_alter_table = OFF')
|
||||||
with self.connection.cursor() as c:
|
with self.connection.cursor() as c:
|
||||||
# Restore initial FK setting - PRAGMA values can't be parametrized
|
# Restore initial FK setting - PRAGMA values can't be parametrized
|
||||||
c.execute('PRAGMA foreign_keys = %s' % int(self._initial_pragma_fk))
|
c.execute('PRAGMA foreign_keys = %s' % int(self._initial_pragma_fk))
|
||||||
|
@ -538,6 +538,9 @@ class EmailField(CharField):
|
|||||||
default_validators = [validators.validate_email]
|
default_validators = [validators.validate_email]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
# The default maximum length of an email is 320 characters per RFC 3696
|
||||||
|
# section 3.
|
||||||
|
kwargs.setdefault("max_length", 320)
|
||||||
super(EmailField, self).__init__(*args, strip=True, **kwargs)
|
super(EmailField, self).__init__(*args, strip=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import sys
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import (
|
from django.core.exceptions import (
|
||||||
RequestDataTooBig, SuspiciousMultipartForm, TooManyFieldsSent,
|
RequestDataTooBig, SuspiciousMultipartForm, TooManyFieldsSent,
|
||||||
|
TooManyFilesSent,
|
||||||
)
|
)
|
||||||
from django.core.files.uploadhandler import (
|
from django.core.files.uploadhandler import (
|
||||||
SkipFile, StopFutureHandlers, StopUpload,
|
SkipFile, StopFutureHandlers, StopUpload,
|
||||||
@ -41,6 +42,7 @@ class InputStreamExhausted(Exception):
|
|||||||
RAW = "raw"
|
RAW = "raw"
|
||||||
FILE = "file"
|
FILE = "file"
|
||||||
FIELD = "field"
|
FIELD = "field"
|
||||||
|
FIELD_TYPES = frozenset([FIELD, RAW])
|
||||||
|
|
||||||
_BASE64_DECODE_ERROR = TypeError if six.PY2 else binascii.Error
|
_BASE64_DECODE_ERROR = TypeError if six.PY2 else binascii.Error
|
||||||
|
|
||||||
@ -104,6 +106,22 @@ class MultiPartParser(object):
|
|||||||
self._upload_handlers = upload_handlers
|
self._upload_handlers = upload_handlers
|
||||||
|
|
||||||
def parse(self):
|
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
|
Parse the POST data and break it into a FILES MultiValueDict and a POST
|
||||||
MultiValueDict.
|
MultiValueDict.
|
||||||
@ -149,6 +167,8 @@ class MultiPartParser(object):
|
|||||||
num_bytes_read = 0
|
num_bytes_read = 0
|
||||||
# To count the number of keys in the request.
|
# To count the number of keys in the request.
|
||||||
num_post_keys = 0
|
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.
|
# To limit the amount of data read from the request.
|
||||||
read_size = None
|
read_size = None
|
||||||
|
|
||||||
@ -161,6 +181,20 @@ class MultiPartParser(object):
|
|||||||
self.handle_file_complete(old_field_name, counters)
|
self.handle_file_complete(old_field_name, counters)
|
||||||
old_field_name = None
|
old_field_name = None
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
disposition = meta_data['content-disposition'][1]
|
disposition = meta_data['content-disposition'][1]
|
||||||
field_name = disposition['name'].strip()
|
field_name = disposition['name'].strip()
|
||||||
@ -173,15 +207,6 @@ class MultiPartParser(object):
|
|||||||
field_name = force_text(field_name, encoding, errors='replace')
|
field_name = force_text(field_name, encoding, errors='replace')
|
||||||
|
|
||||||
if item_type == FIELD:
|
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.
|
# Avoid reading more than DATA_UPLOAD_MAX_MEMORY_SIZE.
|
||||||
if settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None:
|
if settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None:
|
||||||
read_size = settings.DATA_UPLOAD_MAX_MEMORY_SIZE - num_bytes_read
|
read_size = settings.DATA_UPLOAD_MAX_MEMORY_SIZE - num_bytes_read
|
||||||
@ -207,6 +232,16 @@ class MultiPartParser(object):
|
|||||||
|
|
||||||
self._post.appendlist(field_name, force_text(data, encoding, errors='replace'))
|
self._post.appendlist(field_name, force_text(data, encoding, errors='replace'))
|
||||||
elif item_type == FILE:
|
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...
|
# This is a file, use the handler...
|
||||||
file_name = disposition.get('filename')
|
file_name = disposition.get('filename')
|
||||||
if file_name:
|
if file_name:
|
||||||
@ -273,8 +308,13 @@ class MultiPartParser(object):
|
|||||||
# Handle file upload completions on next iteration.
|
# Handle file upload completions on next iteration.
|
||||||
old_field_name = field_name
|
old_field_name = field_name
|
||||||
else:
|
else:
|
||||||
# If this is neither a FIELD or a FILE, just exhaust the stream.
|
# If this is neither a FIELD nor a FILE, exhaust the field
|
||||||
exhaust(stream)
|
# 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:
|
except StopUpload as e:
|
||||||
self._close_files()
|
self._close_files()
|
||||||
if not e.connection_reset:
|
if not e.connection_reset:
|
||||||
|
@ -12,7 +12,9 @@ from django.core.exceptions import (
|
|||||||
DisallowedHost, ImproperlyConfigured, RequestDataTooBig,
|
DisallowedHost, ImproperlyConfigured, RequestDataTooBig,
|
||||||
)
|
)
|
||||||
from django.core.files import uploadhandler
|
from django.core.files import uploadhandler
|
||||||
from django.http.multipartparser import MultiPartParser, MultiPartParserError
|
from django.http.multipartparser import (
|
||||||
|
MultiPartParser, MultiPartParserError, TooManyFilesSent,
|
||||||
|
)
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.datastructures import ImmutableList, MultiValueDict
|
from django.utils.datastructures import ImmutableList, MultiValueDict
|
||||||
from django.utils.encoding import (
|
from django.utils.encoding import (
|
||||||
@ -298,7 +300,7 @@ class HttpRequest(object):
|
|||||||
data = self
|
data = self
|
||||||
try:
|
try:
|
||||||
self._post, self._files = self.parse_file_upload(self.META, data)
|
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
|
# An error occurred while parsing POST data. Since when
|
||||||
# formatting the error the request handler might access
|
# formatting the error the request handler might access
|
||||||
# self.POST, set self._post and self._file to prevent
|
# self.POST, set self._post and self._file to prevent
|
||||||
|
@ -88,12 +88,17 @@ Django core exception classes are defined in ``django.core.exceptions``.
|
|||||||
* ``SuspiciousMultipartForm``
|
* ``SuspiciousMultipartForm``
|
||||||
* ``SuspiciousSession``
|
* ``SuspiciousSession``
|
||||||
* ``TooManyFieldsSent``
|
* ``TooManyFieldsSent``
|
||||||
|
* ``TooManyFilesSent``
|
||||||
|
|
||||||
If a ``SuspiciousOperation`` exception reaches the WSGI handler level it is
|
If a ``SuspiciousOperation`` exception reaches the WSGI handler level it is
|
||||||
logged at the ``Error`` level and results in
|
logged at the ``Error`` level and results in
|
||||||
a :class:`~django.http.HttpResponseBadRequest`. See the :doc:`logging
|
a :class:`~django.http.HttpResponseBadRequest`. See the :doc:`logging
|
||||||
documentation </topics/logging/>` for more information.
|
documentation </topics/logging/>` for more information.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2.18
|
||||||
|
|
||||||
|
``SuspiciousOperation`` is raised when too many files are submitted.
|
||||||
|
|
||||||
``PermissionDenied``
|
``PermissionDenied``
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -961,6 +961,28 @@ could be used as a denial-of-service attack vector if left unchecked. Since web
|
|||||||
servers don't typically perform deep request inspection, it's not possible to
|
servers don't typically perform deep request inspection, it's not possible to
|
||||||
perform a similar check at that level.
|
perform a similar check at that level.
|
||||||
|
|
||||||
|
.. setting:: DATA_UPLOAD_MAX_NUMBER_FILES
|
||||||
|
|
||||||
|
``DATA_UPLOAD_MAX_NUMBER_FILES``
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 3.2.18
|
||||||
|
|
||||||
|
Default: ``100``
|
||||||
|
|
||||||
|
The maximum number of files that may be received via POST in a
|
||||||
|
``multipart/form-data`` encoded request before a
|
||||||
|
:exc:`~django.core.exceptions.SuspiciousOperation` (``TooManyFiles``) is
|
||||||
|
raised. You can set this to ``None`` to disable the check. Applications that
|
||||||
|
are expected to receive an unusually large number of file fields should tune
|
||||||
|
this setting.
|
||||||
|
|
||||||
|
The number of accepted files is correlated to the amount of time and memory
|
||||||
|
needed to process the request. Large requests could be used as a
|
||||||
|
denial-of-service attack vector if left unchecked. Since web servers don't
|
||||||
|
typically perform deep request inspection, it's not possible to perform a
|
||||||
|
similar check at that level.
|
||||||
|
|
||||||
.. setting:: DATABASE_ROUTERS
|
.. setting:: DATABASE_ROUTERS
|
||||||
|
|
||||||
``DATABASE_ROUTERS``
|
``DATABASE_ROUTERS``
|
||||||
@ -3423,6 +3445,7 @@ HTTP
|
|||||||
----
|
----
|
||||||
* :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE`
|
* :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE`
|
||||||
* :setting:`DATA_UPLOAD_MAX_NUMBER_FIELDS`
|
* :setting:`DATA_UPLOAD_MAX_NUMBER_FIELDS`
|
||||||
|
* :setting:`DATA_UPLOAD_MAX_NUMBER_FILES`
|
||||||
* :setting:`DEFAULT_CHARSET`
|
* :setting:`DEFAULT_CHARSET`
|
||||||
* :setting:`DEFAULT_CONTENT_TYPE`
|
* :setting:`DEFAULT_CONTENT_TYPE`
|
||||||
* :setting:`DISALLOWED_USER_AGENTS`
|
* :setting:`DISALLOWED_USER_AGENTS`
|
||||||
|
@ -11,7 +11,10 @@ class EmailFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
|
|||||||
|
|
||||||
def test_emailfield_1(self):
|
def test_emailfield_1(self):
|
||||||
f = EmailField()
|
f = EmailField()
|
||||||
self.assertWidgetRendersTo(f, '<input type="email" name="f" id="id_f" required />')
|
self.assertEqual(f.max_length, 320)
|
||||||
|
self.assertWidgetRendersTo(
|
||||||
|
f, '<input type="email" name="f" id="id_f" maxlength="320" required>'
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
|
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
|
||||||
f.clean('')
|
f.clean('')
|
||||||
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
|
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
|
||||||
|
@ -421,11 +421,18 @@ class FormsTestCase(SimpleTestCase):
|
|||||||
get_spam = BooleanField()
|
get_spam = BooleanField()
|
||||||
|
|
||||||
f = SignupForm(auto_id=False)
|
f = SignupForm(auto_id=False)
|
||||||
self.assertHTMLEqual(str(f['email']), '<input type="email" name="email" required />')
|
self.assertHTMLEqual(
|
||||||
|
str(f["email"]),
|
||||||
|
'<input type="email" name="email" maxlength="320" required />',
|
||||||
|
)
|
||||||
self.assertHTMLEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" required />')
|
self.assertHTMLEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" required />')
|
||||||
|
|
||||||
f = SignupForm({'email': 'test@example.com', 'get_spam': True}, auto_id=False)
|
f = SignupForm({'email': 'test@example.com', 'get_spam': True}, auto_id=False)
|
||||||
self.assertHTMLEqual(str(f['email']), '<input type="email" name="email" value="test@example.com" required />')
|
self.assertHTMLEqual(
|
||||||
|
str(f["email"]),
|
||||||
|
'<input type="email" name="email" maxlength="320" value="test@example.com" '
|
||||||
|
"required />",
|
||||||
|
)
|
||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
str(f['get_spam']),
|
str(f['get_spam']),
|
||||||
'<input checked type="checkbox" name="get_spam" required />',
|
'<input checked type="checkbox" name="get_spam" required />',
|
||||||
@ -2771,7 +2778,7 @@ Good luck picking a username that doesn't already exist.</p>
|
|||||||
<option value="2">Yes</option>
|
<option value="2">Yes</option>
|
||||||
<option value="3">No</option>
|
<option value="3">No</option>
|
||||||
</select></li>
|
</select></li>
|
||||||
<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" /></li>
|
<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320" /></li>
|
||||||
<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul>
|
<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul>
|
||||||
<label class="required" for="id_age">Age:</label> <input type="number" name="age" id="id_age" required /></li>"""
|
<label class="required" for="id_age">Age:</label> <input type="number" name="age" id="id_age" required /></li>"""
|
||||||
)
|
)
|
||||||
@ -2787,7 +2794,7 @@ Good luck picking a username that doesn't already exist.</p>
|
|||||||
<option value="2">Yes</option>
|
<option value="2">Yes</option>
|
||||||
<option value="3">No</option>
|
<option value="3">No</option>
|
||||||
</select></p>
|
</select></p>
|
||||||
<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" /></p>
|
<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320" /></p>
|
||||||
<ul class="errorlist"><li>This field is required.</li></ul>
|
<ul class="errorlist"><li>This field is required.</li></ul>
|
||||||
<p class="required error"><label class="required" for="id_age">Age:</label>
|
<p class="required error"><label class="required" for="id_age">Age:</label>
|
||||||
<input type="number" name="age" id="id_age" required /></p>"""
|
<input type="number" name="age" id="id_age" required /></p>"""
|
||||||
@ -2806,7 +2813,7 @@ Good luck picking a username that doesn't already exist.</p>
|
|||||||
<option value="3">No</option>
|
<option value="3">No</option>
|
||||||
</select></td></tr>
|
</select></td></tr>
|
||||||
<tr><th><label for="id_email">Email:</label></th><td>
|
<tr><th><label for="id_email">Email:</label></th><td>
|
||||||
<input type="email" name="email" id="id_email" /></td></tr>
|
<input type="email" name="email" id="id_email" maxlength="320" /></td></tr>
|
||||||
<tr class="required error"><th><label class="required" for="id_age">Age:</label></th>
|
<tr class="required error"><th><label class="required" for="id_age">Age:</label></th>
|
||||||
<td><ul class="errorlist"><li>This field is required.</li></ul>
|
<td><ul class="errorlist"><li>This field is required.</li></ul>
|
||||||
<input type="number" name="age" id="id_age" required /></td></tr>"""
|
<input type="number" name="age" id="id_age" required /></td></tr>"""
|
||||||
@ -3405,7 +3412,7 @@ Good luck picking a username that doesn't already exist.</p>
|
|||||||
f = CommentForm(data, auto_id=False, error_class=DivErrorList)
|
f = CommentForm(data, auto_id=False, error_class=DivErrorList)
|
||||||
self.assertHTMLEqual(f.as_p(), """<p>Name: <input type="text" name="name" maxlength="50" /></p>
|
self.assertHTMLEqual(f.as_p(), """<p>Name: <input type="text" name="name" maxlength="50" /></p>
|
||||||
<div class="errorlist"><div class="error">Enter a valid email address.</div></div>
|
<div class="errorlist"><div class="error">Enter a valid email address.</div></div>
|
||||||
<p>Email: <input type="email" name="email" value="invalid" required /></p>
|
<p>Email: <input type="email" name="email" value="invalid" maxlength="320" required /></p>
|
||||||
<div class="errorlist"><div class="error">This field is required.</div></div>
|
<div class="errorlist"><div class="error">This field is required.</div></div>
|
||||||
<p>Comment: <input type="text" name="comment" required /></p>""")
|
<p>Comment: <input type="text" name="comment" required /></p>""")
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.core.handlers.wsgi import WSGIHandler
|
from django.core.handlers.wsgi import WSGIHandler
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
from django.test.client import FakePayload
|
from django.test.client import (
|
||||||
|
BOUNDARY, MULTIPART_CONTENT, FakePayload, encode_multipart,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExceptionHandlerTests(SimpleTestCase):
|
class ExceptionHandlerTests(SimpleTestCase):
|
||||||
@ -25,3 +27,27 @@ class ExceptionHandlerTests(SimpleTestCase):
|
|||||||
def test_data_upload_max_number_fields_exceeded(self):
|
def test_data_upload_max_number_fields_exceeded(self):
|
||||||
response = WSGIHandler()(self.get_suspicious_environ(), lambda *a, **k: None)
|
response = WSGIHandler()(self.get_suspicious_environ(), lambda *a, **k: None)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
@override_settings(DATA_UPLOAD_MAX_NUMBER_FILES=2)
|
||||||
|
def test_data_upload_max_number_files_exceeded(self):
|
||||||
|
payload = FakePayload(
|
||||||
|
encode_multipart(
|
||||||
|
BOUNDARY,
|
||||||
|
{
|
||||||
|
"a.txt": "Hello World!",
|
||||||
|
"b.txt": "Hello Django!",
|
||||||
|
"c.txt": "Hello Python!",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
environ = {
|
||||||
|
"REQUEST_METHOD": "POST",
|
||||||
|
"CONTENT_TYPE": MULTIPART_CONTENT,
|
||||||
|
"CONTENT_LENGTH": len(payload),
|
||||||
|
"wsgi.input": payload,
|
||||||
|
"SERVER_NAME": "test",
|
||||||
|
"SERVER_PORT": "8000",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = WSGIHandler()(environ, lambda *a, **k: None)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
@ -258,7 +258,7 @@ class InspectDBTestCase(TestCase):
|
|||||||
}
|
}
|
||||||
call_command('inspectdb', 'inspectdb_columntypes', stdout=out)
|
call_command('inspectdb', 'inspectdb_columntypes', stdout=out)
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
self.assertIn("text_field = myfields.TextField()", output)
|
self.assertIn("text_field = models.TextField()", output)
|
||||||
self.assertIn("big_int_field = models.BigIntegerField()", output)
|
self.assertIn("big_int_field = models.BigIntegerField()", output)
|
||||||
finally:
|
finally:
|
||||||
connection.introspection.data_types_reverse = orig_data_types_reverse
|
connection.introspection.data_types_reverse = orig_data_types_reverse
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.core.exceptions import RequestDataTooBig, TooManyFieldsSent
|
from django.core.exceptions import (
|
||||||
|
RequestDataTooBig, TooManyFieldsSent, TooManyFilesSent,
|
||||||
|
)
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.test.client import FakePayload
|
from django.test.client import FakePayload
|
||||||
|
|
||||||
TOO_MANY_FIELDS_MSG = 'The number of GET/POST parameters exceeded settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.'
|
TOO_MANY_FIELDS_MSG = 'The number of GET/POST parameters exceeded settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.'
|
||||||
|
TOO_MANY_FILES_MSG = 'The number of files exceeded settings.DATA_UPLOAD_MAX_NUMBER_FILES.'
|
||||||
TOO_MUCH_DATA_MSG = 'Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE.'
|
TOO_MUCH_DATA_MSG = 'Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE.'
|
||||||
|
|
||||||
|
|
||||||
@ -166,6 +169,52 @@ class DataUploadMaxNumberOfFieldsMultipartPost(SimpleTestCase):
|
|||||||
self.request._load_post_and_files()
|
self.request._load_post_and_files()
|
||||||
|
|
||||||
|
|
||||||
|
class DataUploadMaxNumberOfFilesMultipartPost(SimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
payload = FakePayload(
|
||||||
|
"\r\n".join(
|
||||||
|
[
|
||||||
|
"--boundary",
|
||||||
|
(
|
||||||
|
'Content-Disposition: form-data; name="name1"; '
|
||||||
|
'filename="name1.txt"'
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"value1",
|
||||||
|
"--boundary",
|
||||||
|
(
|
||||||
|
'Content-Disposition: form-data; name="name2"; '
|
||||||
|
'filename="name2.txt"'
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"value2",
|
||||||
|
"--boundary--",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.request = WSGIRequest(
|
||||||
|
{
|
||||||
|
"REQUEST_METHOD": "POST",
|
||||||
|
"CONTENT_TYPE": "multipart/form-data; boundary=boundary",
|
||||||
|
"CONTENT_LENGTH": len(payload),
|
||||||
|
"wsgi.input": payload,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_number_exceeded(self):
|
||||||
|
with self.settings(DATA_UPLOAD_MAX_NUMBER_FILES=1):
|
||||||
|
with self.assertRaisesMessage(TooManyFilesSent, TOO_MANY_FILES_MSG):
|
||||||
|
self.request._load_post_and_files()
|
||||||
|
|
||||||
|
def test_number_not_exceeded(self):
|
||||||
|
with self.settings(DATA_UPLOAD_MAX_NUMBER_FILES=2):
|
||||||
|
self.request._load_post_and_files()
|
||||||
|
|
||||||
|
def test_no_limit(self):
|
||||||
|
with self.settings(DATA_UPLOAD_MAX_NUMBER_FILES=None):
|
||||||
|
self.request._load_post_and_files()
|
||||||
|
|
||||||
|
|
||||||
class DataUploadMaxNumberOfFieldsFormPost(SimpleTestCase):
|
class DataUploadMaxNumberOfFieldsFormPost(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
payload = FakePayload("\r\n".join(['a=1&a=2;a=3', '']))
|
payload = FakePayload("\r\n".join(['a=1&a=2;a=3', '']))
|
||||||
|
@ -243,12 +243,12 @@ class RequestsTests(SimpleTestCase):
|
|||||||
def test_far_expiration(self):
|
def test_far_expiration(self):
|
||||||
"Cookie will expire when an distant expiration time is provided"
|
"Cookie will expire when an distant expiration time is provided"
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
response.set_cookie('datetime', expires=datetime(2028, 1, 1, 4, 5, 6))
|
response.set_cookie('datetime', expires=datetime(2038, 1, 1, 4, 5, 6))
|
||||||
datetime_cookie = response.cookies['datetime']
|
datetime_cookie = response.cookies['datetime']
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
datetime_cookie['expires'],
|
datetime_cookie['expires'],
|
||||||
# assertIn accounts for slight time dependency (#23450)
|
# assertIn accounts for slight time dependency (#23450)
|
||||||
('Sat, 01-Jan-2028 04:05:06 GMT', 'Sat, 01-Jan-2028 04:05:07 GMT')
|
('Fri, 01-Jan-2038 04:05:06 GMT', 'Fri, 01-Jan-2038 04:05:07 GMT')
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_max_age_expiration(self):
|
def test_max_age_expiration(self):
|
||||||
|
@ -63,6 +63,7 @@ TEST_DATA = [
|
|||||||
|
|
||||||
(validate_email, 'example@atm.%s' % ('a' * 64), ValidationError),
|
(validate_email, 'example@atm.%s' % ('a' * 64), ValidationError),
|
||||||
(validate_email, 'example@%s.atm.%s' % ('b' * 64, 'a' * 63), ValidationError),
|
(validate_email, 'example@%s.atm.%s' % ('b' * 64, 'a' * 63), ValidationError),
|
||||||
|
(validate_email, "example@%scom" % (("a" * 63 + ".") * 100), ValidationError),
|
||||||
(validate_email, None, ValidationError),
|
(validate_email, None, ValidationError),
|
||||||
(validate_email, '', ValidationError),
|
(validate_email, '', ValidationError),
|
||||||
(validate_email, 'abc', ValidationError),
|
(validate_email, 'abc', ValidationError),
|
||||||
@ -223,6 +224,11 @@ TEST_DATA = [
|
|||||||
(URLValidator(EXTENDED_SCHEMES), 'git+ssh://git@github.com/example/hg-git.git', None),
|
(URLValidator(EXTENDED_SCHEMES), 'git+ssh://git@github.com/example/hg-git.git', None),
|
||||||
|
|
||||||
(URLValidator(EXTENDED_SCHEMES), 'git://-invalid.com', ValidationError),
|
(URLValidator(EXTENDED_SCHEMES), 'git://-invalid.com', ValidationError),
|
||||||
|
(
|
||||||
|
URLValidator(),
|
||||||
|
"http://example." + ("a" * 63 + ".") * 1000 + "com",
|
||||||
|
ValidationError,
|
||||||
|
),
|
||||||
# Trailing newlines not accepted
|
# Trailing newlines not accepted
|
||||||
(URLValidator(), 'http://www.djangoproject.com/\n', ValidationError),
|
(URLValidator(), 'http://www.djangoproject.com/\n', ValidationError),
|
||||||
(URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError),
|
(URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user