Apply all patches up to CVE-2023-36053

This commit is contained in:
Alan Cheung 2023-07-13 14:15:15 -07:00
parent c669cf279a
commit 11bb365c7b
17 changed files with 215 additions and 29 deletions

View File

@ -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).

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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``
-------------------- --------------------

View File

@ -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`

View File

@ -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.'"):

View File

@ -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&#39;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&#39;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&#39;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&#39;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>""")

View File

@ -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)

View File

@ -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

View File

@ -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', '']))

View File

@ -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):

View File

@ -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),