!118 [sync] PR-117: Update to 4.2.15 for fix cves
From: @openeuler-sync-bot Reviewed-by: @cherry530 Signed-off-by: @cherry530
This commit is contained in:
commit
c5111f106e
@ -1,109 +0,0 @@
|
||||
From a9010fe5555e6086a9d9ae50069579400ef0685e Mon Sep 17 00:00:00 2001
|
||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
Date: Wed, 22 Jun 2022 12:44:04 +0200
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2022-34265 -- Protected
|
||||
Trunc(kind)/Extract(lookup_name) against SQL injection.
|
||||
|
||||
Thanks Takuto Yoshikai (Aeye Security Lab) for the report.
|
||||
---
|
||||
django/db/backends/base/operations.py | 3 ++
|
||||
django/db/models/functions/datetime.py | 4 +++
|
||||
docs/releases/3.2.14.txt | 11 ++++++
|
||||
.../datetime/test_extract_trunc.py | 34 +++++++++++++++++++
|
||||
4 files changed, 52 insertions(+)
|
||||
|
||||
diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
|
||||
index 0fcc607bcfb0..cdcd9885ba27 100644
|
||||
--- a/django/db/backends/base/operations.py
|
||||
+++ b/django/db/backends/base/operations.py
|
||||
@@ -9,6 +9,7 @@
|
||||
from django.db.backends import utils
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_str
|
||||
+from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
|
||||
class BaseDatabaseOperations:
|
||||
@@ -53,6 +54,8 @@ class BaseDatabaseOperations:
|
||||
# Prefix for EXPLAIN queries, or None EXPLAIN isn't supported.
|
||||
explain_prefix = None
|
||||
|
||||
+ extract_trunc_lookup_pattern = _lazy_re_compile(r"[\w\-_()]+")
|
||||
+
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self._cache = None
|
||||
diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py
|
||||
index 90e6f41be057..47651d281f19 100644
|
||||
--- a/django/db/models/functions/datetime.py
|
||||
+++ b/django/db/models/functions/datetime.py
|
||||
@@ -41,6 +41,8 @@ def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
|
||||
super().__init__(expression, **extra)
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
+ if not connection.ops.extract_trunc_lookup_pattern.fullmatch(self.lookup_name):
|
||||
+ raise ValueError("Invalid lookup_name: %s" % self.lookup_name)
|
||||
sql, params = compiler.compile(self.lhs)
|
||||
lhs_output_field = self.lhs.output_field
|
||||
if isinstance(lhs_output_field, DateTimeField):
|
||||
@@ -192,6 +194,8 @@ def __init__(self, expression, output_field=None, tzinfo=None, is_dst=None, **ex
|
||||
super().__init__(expression, output_field=output_field, **extra)
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
+ if not connection.ops.extract_trunc_lookup_pattern.fullmatch(self.kind):
|
||||
+ raise ValueError("Invalid kind: %s" % self.kind)
|
||||
inner_sql, inner_params = compiler.compile(self.lhs)
|
||||
tzname = None
|
||||
if isinstance(self.lhs.output_field, DateTimeField):
|
||||
diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py
|
||||
index 258600127f93..27ed3ae63ee5 100644
|
||||
--- a/tests/db_functions/datetime/test_extract_trunc.py
|
||||
+++ b/tests/db_functions/datetime/test_extract_trunc.py
|
||||
@@ -177,6 +177,23 @@ def test_extract_year_lessthan_lookup(self):
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.assertGreaterEqual(str(qs.query).lower().count('extract'), 2)
|
||||
|
||||
+ def test_extract_lookup_name_sql_injection(self):
|
||||
+ start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||
+ end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||
+ if settings.USE_TZ:
|
||||
+ start_datetime = timezone.make_aware(start_datetime)
|
||||
+ end_datetime = timezone.make_aware(end_datetime)
|
||||
+ self.create_model(start_datetime, end_datetime)
|
||||
+ self.create_model(end_datetime, start_datetime)
|
||||
+
|
||||
+ msg = "Invalid lookup_name: "
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ DTModel.objects.filter(
|
||||
+ start_datetime__year=Extract(
|
||||
+ "start_datetime", "day' FROM start_datetime)) OR 1=1;--"
|
||||
+ )
|
||||
+ ).exists()
|
||||
+
|
||||
def test_extract_func(self):
|
||||
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||
@@ -620,6 +637,23 @@ def test_extract_second_func(self):
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__second=ExtractSecond('start_datetime')).count(), 2)
|
||||
|
||||
+ def test_trunc_lookup_name_sql_injection(self):
|
||||
+ start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||
+ end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||
+ if settings.USE_TZ:
|
||||
+ start_datetime = timezone.make_aware(start_datetime)
|
||||
+ end_datetime = timezone.make_aware(end_datetime)
|
||||
+ self.create_model(start_datetime, end_datetime)
|
||||
+ self.create_model(end_datetime, start_datetime)
|
||||
+ msg = "Invalid kind: "
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ DTModel.objects.filter(
|
||||
+ start_datetime__date=Trunc(
|
||||
+ "start_datetime",
|
||||
+ "year', start_datetime)) OR 1=1;--",
|
||||
+ )
|
||||
+ ).exists()
|
||||
+
|
||||
def test_trunc_func(self):
|
||||
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||
@ -1,100 +0,0 @@
|
||||
From 80353a42e41fd22933184a30f2e2c04d0c274c83 Mon Sep 17 00:00:00 2001
|
||||
From: starlet-dx <15929766099@163.com>
|
||||
Date: Mon, 13 Feb 2023 19:31:46 +0800
|
||||
Subject: [PATCH 1/1] [3.2.x] Fixed CVE-2023-23969 -- Prevented DoS with pathological values for Accept-Language.
|
||||
|
||||
The parsed values of Accept-Language headers are cached in order to avoid repetitive parsing. This leads to a potential denial-of-service vector via excessive memory usage if the raw value of Accept-Language headers is very large.
|
||||
|
||||
Accept-Language headers are now limited to a maximum length in order to avoid this issue.
|
||||
---
|
||||
django/utils/translation/trans_real.py | 32 +++++++++++++++++++++++++-
|
||||
tests/i18n/tests.py | 12 ++++++++++
|
||||
2 files changed, 43 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
|
||||
index 8042f6f..b262a50 100644
|
||||
--- a/django/utils/translation/trans_real.py
|
||||
+++ b/django/utils/translation/trans_real.py
|
||||
@@ -30,6 +30,11 @@ _default = None
|
||||
# magic gettext number to separate context from message
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
+# Maximum number of characters that will be parsed from the Accept-Language
|
||||
+# header to prevent possible denial of service or memory exhaustion attacks.
|
||||
+# About 10x longer than the longest value shown on MDN’s Accept-Language page.
|
||||
+ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500
|
||||
+
|
||||
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
|
||||
# and RFC 3066, section 2.1
|
||||
accept_language_re = _lazy_re_compile(r'''
|
||||
@@ -556,7 +561,7 @@ def get_language_from_request(request, check_path=False):
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
-def parse_accept_lang_header(lang_string):
|
||||
+def _parse_accept_lang_header(lang_string):
|
||||
"""
|
||||
Parse the lang_string, which is the body of an HTTP Accept-Language
|
||||
header, and return a tuple of (lang, q-value), ordered by 'q' values.
|
||||
@@ -578,3 +583,28 @@ def parse_accept_lang_header(lang_string):
|
||||
result.append((lang, priority))
|
||||
result.sort(key=lambda k: k[1], reverse=True)
|
||||
return tuple(result)
|
||||
+
|
||||
+
|
||||
+def parse_accept_lang_header(lang_string):
|
||||
+ """
|
||||
+ Parse the value of the Accept-Language header up to a maximum length.
|
||||
+
|
||||
+ The value of the header is truncated to a maximum length to avoid potential
|
||||
+ denial of service and memory exhaustion attacks. Excessive memory could be
|
||||
+ used if the raw value is very large as it would be cached due to the use of
|
||||
+ functools.lru_cache() to avoid repetitive parsing of common header values.
|
||||
+ """
|
||||
+ # If the header value doesn't exceed the maximum allowed length, parse it.
|
||||
+ if len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH:
|
||||
+ return _parse_accept_lang_header(lang_string)
|
||||
+
|
||||
+ # If there is at least one comma in the value, parse up to the last comma
|
||||
+ # before the max length, skipping any truncated parts at the end of the
|
||||
+ # header value.
|
||||
+ index = lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)
|
||||
+ if index > 0:
|
||||
+ return _parse_accept_lang_header(lang_string[:index])
|
||||
+
|
||||
+ # Don't attempt to parse if there is only one language-range value which is
|
||||
+ # longer than the maximum allowed length and so truncated.
|
||||
+ return ()
|
||||
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
|
||||
index 7edceb1..f379f8f 100644
|
||||
--- a/tests/i18n/tests.py
|
||||
+++ b/tests/i18n/tests.py
|
||||
@@ -1352,6 +1352,14 @@ class MiscTests(SimpleTestCase):
|
||||
('de;q=0.', [('de', 0.0)]),
|
||||
('en; q=1,', [('en', 1.0)]),
|
||||
('en; q=1.0, * ; q=0.5', [('en', 1.0), ('*', 0.5)]),
|
||||
+ (
|
||||
+ 'en' + '-x' * 20,
|
||||
+ [('en-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x', 1.0)],
|
||||
+ ),
|
||||
+ (
|
||||
+ ', '.join(['en; q=1.0'] * 20),
|
||||
+ [('en', 1.0)] * 20,
|
||||
+ ),
|
||||
# Bad headers
|
||||
('en-gb;q=1.0000', []),
|
||||
('en;q=0.1234', []),
|
||||
@@ -1367,6 +1375,10 @@ class MiscTests(SimpleTestCase):
|
||||
('12-345', []),
|
||||
('', []),
|
||||
('en;q=1e0', []),
|
||||
+ # Invalid as language-range value too long.
|
||||
+ ('xxxxxxxx' + '-xxxxxxxx' * 500, []),
|
||||
+ # Header value too long, only parse up to limit.
|
||||
+ (', '.join(['en; q=1.0'] * 500), [('en', 1.0)] * 45),
|
||||
]
|
||||
for value, expected in tests:
|
||||
with self.subTest(value=value):
|
||||
--
|
||||
2.30.0
|
||||
|
||||
@ -1,401 +0,0 @@
|
||||
From a665ed5179f5bbd3db95ce67286d0192eff041d8 Mon Sep 17 00:00:00 2001
|
||||
From: Markus Holtermann <info@markusholtermann.eu>
|
||||
Date: Tue, 13 Dec 2022 10:27:39 +0100
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2023-24580 -- Prevented DoS with too many
|
||||
uploaded files.
|
||||
|
||||
Thanks to Jakob Ackermann for the report.
|
||||
---
|
||||
django/conf/global_settings.py | 4 ++
|
||||
django/core/exceptions.py | 9 +++
|
||||
django/core/handlers/exception.py | 4 +-
|
||||
django/http/multipartparser.py | 62 +++++++++++++++++----
|
||||
django/http/request.py | 6 +-
|
||||
docs/ref/exceptions.txt | 5 ++
|
||||
docs/ref/settings.txt | 23 ++++++++
|
||||
docs/releases/3.2.18.txt | 10 +++-
|
||||
tests/handlers/test_exception.py | 28 +++++++++-
|
||||
tests/requests/test_data_upload_settings.py | 51 ++++++++++++++++-
|
||||
10 files changed, 184 insertions(+), 18 deletions(-)
|
||||
|
||||
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
|
||||
index cf9fae496e3a..4a27887a8f04 100644
|
||||
--- a/django/conf/global_settings.py
|
||||
+++ b/django/conf/global_settings.py
|
||||
@@ -303,6 +303,10 @@ def gettext_noop(s):
|
||||
# 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 673d004d5756..83161a58cd66 100644
|
||||
--- a/django/core/exceptions.py
|
||||
+++ b/django/core/exceptions.py
|
||||
@@ -58,6 +58,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 3005a5eccb11..2ecc2a0fd697 100644
|
||||
--- a/django/core/handlers/exception.py
|
||||
+++ b/django/core/handlers/exception.py
|
||||
@@ -9,7 +9,7 @@
|
||||
from django.core import signals
|
||||
from django.core.exceptions import (
|
||||
BadRequest, PermissionDenied, RequestDataTooBig, SuspiciousOperation,
|
||||
- TooManyFieldsSent,
|
||||
+ TooManyFieldsSent, TooManyFilesSent,
|
||||
)
|
||||
from django.http import Http404
|
||||
from django.http.multipartparser import MultiPartParserError
|
||||
@@ -88,7 +88,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 35a54f4ca12e..d8a304d4babe 100644
|
||||
--- a/django/http/multipartparser.py
|
||||
+++ b/django/http/multipartparser.py
|
||||
@@ -14,6 +14,7 @@
|
||||
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 @@ def __init__(self, META, input_data, upload_handlers, encoding=None):
|
||||
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 @@ def parse(self):
|
||||
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 @@ def parse(self):
|
||||
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 @@ def parse(self):
|
||||
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 @@ def parse(self):
|
||||
|
||||
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 @@ def parse(self):
|
||||
# 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 195341ec4b69..b6cd7a372f14 100644
|
||||
--- a/django/http/request.py
|
||||
+++ b/django/http/request.py
|
||||
@@ -12,7 +12,9 @@
|
||||
DisallowedHost, ImproperlyConfigured, RequestDataTooBig, 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, MultiValueDict,
|
||||
)
|
||||
@@ -360,7 +362,7 @@ def _load_post_and_files(self):
|
||||
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
|
||||
diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt
|
||||
index 2f5aa64b9d9d..7d34025cd65c 100644
|
||||
--- a/docs/ref/exceptions.txt
|
||||
+++ b/docs/ref/exceptions.txt
|
||||
@@ -84,12 +84,17 @@ Django core exception classes are defined in ``django.core.exceptions``.
|
||||
* ``SuspiciousMultipartForm``
|
||||
* ``SuspiciousSession``
|
||||
* ``TooManyFieldsSent``
|
||||
+ * ``TooManyFilesSent``
|
||||
|
||||
If a ``SuspiciousOperation`` exception reaches the ASGI/WSGI handler level
|
||||
it is logged at the ``Error`` level and results in
|
||||
a :class:`~django.http.HttpResponseBadRequest`. See the :doc:`logging
|
||||
documentation </topics/logging/>` for more information.
|
||||
|
||||
+.. versionchanged:: 3.2.18
|
||||
+
|
||||
+ ``SuspiciousOperation`` is raised when too many files are submitted.
|
||||
+
|
||||
``PermissionDenied``
|
||||
--------------------
|
||||
|
||||
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
|
||||
index 9bfadbc89bd2..9173009c94d5 100644
|
||||
--- a/docs/ref/settings.txt
|
||||
+++ b/docs/ref/settings.txt
|
||||
@@ -1063,6 +1063,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
|
||||
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
|
||||
|
||||
``DATABASE_ROUTERS``
|
||||
@@ -3671,6 +3693,7 @@ HTTP
|
||||
----
|
||||
* :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE`
|
||||
* :setting:`DATA_UPLOAD_MAX_NUMBER_FIELDS`
|
||||
+* :setting:`DATA_UPLOAD_MAX_NUMBER_FILES`
|
||||
* :setting:`DEFAULT_CHARSET`
|
||||
* :setting:`DISALLOWED_USER_AGENTS`
|
||||
* :setting:`FORCE_SCRIPT_NAME`
|
||||
diff --git a/tests/handlers/test_exception.py b/tests/handlers/test_exception.py
|
||||
index 0c1e76399045..7de2edaeea34 100644
|
||||
--- a/tests/handlers/test_exception.py
|
||||
+++ b/tests/handlers/test_exception.py
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.core.handlers.wsgi import WSGIHandler
|
||||
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):
|
||||
@@ -25,3 +27,27 @@ def test_data_upload_max_memory_size_exceeded(self):
|
||||
def test_data_upload_max_number_fields_exceeded(self):
|
||||
response = WSGIHandler()(self.get_suspicious_environ(), lambda *a, **k: None)
|
||||
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)
|
||||
diff --git a/tests/requests/test_data_upload_settings.py b/tests/requests/test_data_upload_settings.py
|
||||
index 44897cc9fa97..ded778b42286 100644
|
||||
--- a/tests/requests/test_data_upload_settings.py
|
||||
+++ b/tests/requests/test_data_upload_settings.py
|
||||
@@ -1,11 +1,14 @@
|
||||
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.test import SimpleTestCase
|
||||
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_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.'
|
||||
|
||||
|
||||
@@ -166,6 +169,52 @@ def test_no_limit(self):
|
||||
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):
|
||||
def setUp(self):
|
||||
payload = FakePayload("\r\n".join(['a=1&a=2&a=3', '']))
|
||||
@ -1,322 +0,0 @@
|
||||
From 6bb2e1ac607b1a399e1d7bd3650c04a586e6746e Mon Sep 17 00:00:00 2001
|
||||
From: starlet-dx <15929766099@163.com>
|
||||
Date: Tue, 16 May 2023 10:00:42 +0800
|
||||
Subject: [PATCH 1/1] [3.2.x] Fixed CVE-2023-31047, Fixed #31710 -- Prevented
|
||||
potential bypass of validation when uploading multiple files using one form
|
||||
field.
|
||||
|
||||
Thanks Moataz Al-Sharida and nawaik for reports.
|
||||
|
||||
Co-authored-by: Shai Berger <shai@platonix.com>
|
||||
Co-authored-by: nessita <124304+nessita@users.noreply.github.com>
|
||||
|
||||
Origin:
|
||||
https://github.com/django/django/commit/eed53d0011622e70b936e203005f0e6f4ac48965
|
||||
---
|
||||
django/forms/widgets.py | 26 ++++++-
|
||||
docs/topics/http/file-uploads.txt | 65 ++++++++++++++++--
|
||||
.../forms_tests/field_tests/test_filefield.py | 68 ++++++++++++++++++-
|
||||
.../widget_tests/test_clearablefileinput.py | 5 ++
|
||||
.../widget_tests/test_fileinput.py | 44 ++++++++++++
|
||||
5 files changed, 200 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
|
||||
index 1b1c143..8ef8255 100644
|
||||
--- a/django/forms/widgets.py
|
||||
+++ b/django/forms/widgets.py
|
||||
@@ -378,16 +378,40 @@ class MultipleHiddenInput(HiddenInput):
|
||||
|
||||
class FileInput(Input):
|
||||
input_type = 'file'
|
||||
+ allow_multiple_selected = False
|
||||
needs_multipart_form = True
|
||||
template_name = 'django/forms/widgets/file.html'
|
||||
|
||||
+ def __init__(self, attrs=None):
|
||||
+ if (
|
||||
+ attrs is not None and
|
||||
+ not self.allow_multiple_selected and
|
||||
+ attrs.get("multiple", False)
|
||||
+ ):
|
||||
+ raise ValueError(
|
||||
+ "%s doesn't support uploading multiple files."
|
||||
+ % self.__class__.__qualname__
|
||||
+ )
|
||||
+ if self.allow_multiple_selected:
|
||||
+ if attrs is None:
|
||||
+ attrs = {"multiple": True}
|
||||
+ else:
|
||||
+ attrs.setdefault("multiple", True)
|
||||
+ super().__init__(attrs)
|
||||
+
|
||||
def format_value(self, value):
|
||||
"""File input never renders a value."""
|
||||
return
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"File widgets take data from FILES, not POST"
|
||||
- return files.get(name)
|
||||
+ getter = files.get
|
||||
+ if self.allow_multiple_selected:
|
||||
+ try:
|
||||
+ getter = files.getlist
|
||||
+ except AttributeError:
|
||||
+ pass
|
||||
+ return getter(name)
|
||||
|
||||
def value_omitted_from_data(self, data, files, name):
|
||||
return name not in files
|
||||
diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt
|
||||
index ca272d7..4388594 100644
|
||||
--- a/docs/topics/http/file-uploads.txt
|
||||
+++ b/docs/topics/http/file-uploads.txt
|
||||
@@ -126,19 +126,54 @@ model::
|
||||
form = UploadFileForm()
|
||||
return render(request, 'upload.html', {'form': form})
|
||||
|
||||
+.. _uploading_multiple_files:
|
||||
+
|
||||
Uploading multiple files
|
||||
------------------------
|
||||
|
||||
-If you want to upload multiple files using one form field, set the ``multiple``
|
||||
-HTML attribute of field's widget:
|
||||
+..
|
||||
+ Tests in tests.forms_tests.field_tests.test_filefield.MultipleFileFieldTest
|
||||
+ should be updated after any changes in the following snippets.
|
||||
+
|
||||
+If you want to upload multiple files using one form field, create a subclass
|
||||
+of the field's widget and set the ``allow_multiple_selected`` attribute on it
|
||||
+to ``True``.
|
||||
+
|
||||
+In order for such files to be all validated by your form (and have the value of
|
||||
+the field include them all), you will also have to subclass ``FileField``. See
|
||||
+below for an example.
|
||||
+
|
||||
+.. admonition:: Multiple file field
|
||||
+
|
||||
+ Django is likely to have a proper multiple file field support at some point
|
||||
+ in the future.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: forms.py
|
||||
|
||||
from django import forms
|
||||
|
||||
+
|
||||
+ class MultipleFileInput(forms.ClearableFileInput):
|
||||
+ allow_multiple_selected = True
|
||||
+
|
||||
+
|
||||
+ class MultipleFileField(forms.FileField):
|
||||
+ def __init__(self, *args, **kwargs):
|
||||
+ kwargs.setdefault("widget", MultipleFileInput())
|
||||
+ super().__init__(*args, **kwargs)
|
||||
+
|
||||
+ def clean(self, data, initial=None):
|
||||
+ single_file_clean = super().clean
|
||||
+ if isinstance(data, (list, tuple)):
|
||||
+ result = [single_file_clean(d, initial) for d in data]
|
||||
+ else:
|
||||
+ result = single_file_clean(data, initial)
|
||||
+ return result
|
||||
+
|
||||
+
|
||||
class FileFieldForm(forms.Form):
|
||||
- file_field = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))
|
||||
+ file_field = MultipleFileField()
|
||||
|
||||
Then override the ``post`` method of your
|
||||
:class:`~django.views.generic.edit.FormView` subclass to handle multiple file
|
||||
@@ -158,14 +193,32 @@ uploads:
|
||||
def post(self, request, *args, **kwargs):
|
||||
form_class = self.get_form_class()
|
||||
form = self.get_form(form_class)
|
||||
- files = request.FILES.getlist('file_field')
|
||||
if form.is_valid():
|
||||
- for f in files:
|
||||
- ... # Do something with each file.
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
+ def form_valid(self, form):
|
||||
+ files = form.cleaned_data["file_field"]
|
||||
+ for f in files:
|
||||
+ ... # Do something with each file.
|
||||
+ return super().form_valid()
|
||||
+
|
||||
+.. warning::
|
||||
+
|
||||
+ This will allow you to handle multiple files at the form level only. Be
|
||||
+ aware that you cannot use it to put multiple files on a single model
|
||||
+ instance (in a single field), for example, even if the custom widget is used
|
||||
+ with a form field related to a model ``FileField``.
|
||||
+
|
||||
+.. versionchanged:: 3.2.19
|
||||
+
|
||||
+ In previous versions, there was no support for the ``allow_multiple_selected``
|
||||
+ class attribute, and users were advised to create the widget with the HTML
|
||||
+ attribute ``multiple`` set through the ``attrs`` argument. However, this
|
||||
+ caused validation of the form field to be applied only to the last file
|
||||
+ submitted, which could have adverse security implications.
|
||||
+
|
||||
Upload Handlers
|
||||
===============
|
||||
|
||||
diff --git a/tests/forms_tests/field_tests/test_filefield.py b/tests/forms_tests/field_tests/test_filefield.py
|
||||
index 2db106e..b54febd 100644
|
||||
--- a/tests/forms_tests/field_tests/test_filefield.py
|
||||
+++ b/tests/forms_tests/field_tests/test_filefield.py
|
||||
@@ -2,7 +2,8 @@ import pickle
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
-from django.forms import FileField
|
||||
+from django.core.validators import validate_image_file_extension
|
||||
+from django.forms import FileField, FileInput
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
|
||||
@@ -83,3 +84,68 @@ class FileFieldTest(SimpleTestCase):
|
||||
|
||||
def test_file_picklable(self):
|
||||
self.assertIsInstance(pickle.loads(pickle.dumps(FileField())), FileField)
|
||||
+
|
||||
+
|
||||
+class MultipleFileInput(FileInput):
|
||||
+ allow_multiple_selected = True
|
||||
+
|
||||
+
|
||||
+class MultipleFileField(FileField):
|
||||
+ def __init__(self, *args, **kwargs):
|
||||
+ kwargs.setdefault("widget", MultipleFileInput())
|
||||
+ super().__init__(*args, **kwargs)
|
||||
+
|
||||
+ def clean(self, data, initial=None):
|
||||
+ single_file_clean = super().clean
|
||||
+ if isinstance(data, (list, tuple)):
|
||||
+ result = [single_file_clean(d, initial) for d in data]
|
||||
+ else:
|
||||
+ result = single_file_clean(data, initial)
|
||||
+ return result
|
||||
+
|
||||
+
|
||||
+class MultipleFileFieldTest(SimpleTestCase):
|
||||
+ def test_file_multiple(self):
|
||||
+ f = MultipleFileField()
|
||||
+ files = [
|
||||
+ SimpleUploadedFile("name1", b"Content 1"),
|
||||
+ SimpleUploadedFile("name2", b"Content 2"),
|
||||
+ ]
|
||||
+ self.assertEqual(f.clean(files), files)
|
||||
+
|
||||
+ def test_file_multiple_empty(self):
|
||||
+ f = MultipleFileField()
|
||||
+ files = [
|
||||
+ SimpleUploadedFile("empty", b""),
|
||||
+ SimpleUploadedFile("nonempty", b"Some Content"),
|
||||
+ ]
|
||||
+ msg = "'The submitted file is empty.'"
|
||||
+ with self.assertRaisesMessage(ValidationError, msg):
|
||||
+ f.clean(files)
|
||||
+ with self.assertRaisesMessage(ValidationError, msg):
|
||||
+ f.clean(files[::-1])
|
||||
+
|
||||
+ def test_file_multiple_validation(self):
|
||||
+ f = MultipleFileField(validators=[validate_image_file_extension])
|
||||
+
|
||||
+ good_files = [
|
||||
+ SimpleUploadedFile("image1.jpg", b"fake JPEG"),
|
||||
+ SimpleUploadedFile("image2.png", b"faux image"),
|
||||
+ SimpleUploadedFile("image3.bmp", b"fraudulent bitmap"),
|
||||
+ ]
|
||||
+ self.assertEqual(f.clean(good_files), good_files)
|
||||
+
|
||||
+ evil_files = [
|
||||
+ SimpleUploadedFile("image1.sh", b"#!/bin/bash -c 'echo pwned!'\n"),
|
||||
+ SimpleUploadedFile("image2.png", b"faux image"),
|
||||
+ SimpleUploadedFile("image3.jpg", b"fake JPEG"),
|
||||
+ ]
|
||||
+
|
||||
+ evil_rotations = (
|
||||
+ evil_files[i:] + evil_files[:i] # Rotate by i.
|
||||
+ for i in range(len(evil_files))
|
||||
+ )
|
||||
+ msg = "File extension “sh” is not allowed. Allowed extensions are: "
|
||||
+ for rotated_evil_files in evil_rotations:
|
||||
+ with self.assertRaisesMessage(ValidationError, msg):
|
||||
+ f.clean(rotated_evil_files)
|
||||
diff --git a/tests/forms_tests/widget_tests/test_clearablefileinput.py b/tests/forms_tests/widget_tests/test_clearablefileinput.py
|
||||
index dee44c4..6cf1476 100644
|
||||
--- a/tests/forms_tests/widget_tests/test_clearablefileinput.py
|
||||
+++ b/tests/forms_tests/widget_tests/test_clearablefileinput.py
|
||||
@@ -176,3 +176,8 @@ class ClearableFileInputTest(WidgetTest):
|
||||
self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True)
|
||||
self.assertIs(widget.value_omitted_from_data({}, {'field': 'x'}, 'field'), False)
|
||||
self.assertIs(widget.value_omitted_from_data({'field-clear': 'y'}, {}, 'field'), False)
|
||||
+
|
||||
+ def test_multiple_error(self):
|
||||
+ msg = "ClearableFileInput doesn't support uploading multiple files."
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ ClearableFileInput(attrs={"multiple": True})
|
||||
diff --git a/tests/forms_tests/widget_tests/test_fileinput.py b/tests/forms_tests/widget_tests/test_fileinput.py
|
||||
index 8eec262..8068f70 100644
|
||||
--- a/tests/forms_tests/widget_tests/test_fileinput.py
|
||||
+++ b/tests/forms_tests/widget_tests/test_fileinput.py
|
||||
@@ -1,4 +1,6 @@
|
||||
+from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.forms import FileInput
|
||||
+from django.utils.datastructures import MultiValueDict
|
||||
|
||||
from .base import WidgetTest
|
||||
|
||||
@@ -24,3 +26,45 @@ class FileInputTest(WidgetTest):
|
||||
# user to keep the existing, initial value.
|
||||
self.assertIs(self.widget.use_required_attribute(None), True)
|
||||
self.assertIs(self.widget.use_required_attribute('resume.txt'), False)
|
||||
+
|
||||
+ def test_multiple_error(self):
|
||||
+ msg = "FileInput doesn't support uploading multiple files."
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ FileInput(attrs={"multiple": True})
|
||||
+
|
||||
+ def test_value_from_datadict_multiple(self):
|
||||
+ class MultipleFileInput(FileInput):
|
||||
+ allow_multiple_selected = True
|
||||
+
|
||||
+ file_1 = SimpleUploadedFile("something1.txt", b"content 1")
|
||||
+ file_2 = SimpleUploadedFile("something2.txt", b"content 2")
|
||||
+ # Uploading multiple files is allowed.
|
||||
+ widget = MultipleFileInput(attrs={"multiple": True})
|
||||
+ value = widget.value_from_datadict(
|
||||
+ data={"name": "Test name"},
|
||||
+ files=MultiValueDict({"myfile": [file_1, file_2]}),
|
||||
+ name="myfile",
|
||||
+ )
|
||||
+ self.assertEqual(value, [file_1, file_2])
|
||||
+ # Uploading multiple files is not allowed.
|
||||
+ widget = FileInput()
|
||||
+ value = widget.value_from_datadict(
|
||||
+ data={"name": "Test name"},
|
||||
+ files=MultiValueDict({"myfile": [file_1, file_2]}),
|
||||
+ name="myfile",
|
||||
+ )
|
||||
+ self.assertEqual(value, file_2)
|
||||
+
|
||||
+ def test_multiple_default(self):
|
||||
+ class MultipleFileInput(FileInput):
|
||||
+ allow_multiple_selected = True
|
||||
+
|
||||
+ tests = [
|
||||
+ (None, True),
|
||||
+ ({"class": "myclass"}, True),
|
||||
+ ({"multiple": False}, False),
|
||||
+ ]
|
||||
+ for attrs, expected in tests:
|
||||
+ with self.subTest(attrs=attrs):
|
||||
+ widget = MultipleFileInput(attrs=attrs)
|
||||
+ self.assertIs(widget.attrs["multiple"], expected)
|
||||
--
|
||||
2.30.0
|
||||
|
||||
@ -1,244 +0,0 @@
|
||||
From 454f2fb93437f98917283336201b4048293f7582 Mon Sep 17 00:00:00 2001
|
||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
Date: Wed, 14 Jun 2023 12:23:06 +0200
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2023-36053 -- Prevented potential ReDoS in
|
||||
EmailValidator and URLValidator.
|
||||
|
||||
Thanks Seokchan Yoon for reports.
|
||||
---
|
||||
django/core/validators.py | 7 ++++--
|
||||
django/forms/fields.py | 3 +++
|
||||
docs/ref/forms/fields.txt | 7 +++++-
|
||||
docs/ref/validators.txt | 25 ++++++++++++++++++-
|
||||
docs/releases/3.2.20.txt | 7 +++++-
|
||||
.../field_tests/test_emailfield.py | 5 +++-
|
||||
tests/forms_tests/tests/test_forms.py | 19 +++++++++-----
|
||||
tests/validators/tests.py | 11 ++++++++
|
||||
8 files changed, 72 insertions(+), 12 deletions(-)
|
||||
|
||||
diff --git a/django/core/validators.py b/django/core/validators.py
|
||||
index 731ccf2d4690..b9b58dfa6176 100644
|
||||
--- a/django/core/validators.py
|
||||
+++ b/django/core/validators.py
|
||||
@@ -93,6 +93,7 @@ class URLValidator(RegexValidator):
|
||||
message = _('Enter a valid URL.')
|
||||
schemes = ['http', 'https', 'ftp', 'ftps']
|
||||
unsafe_chars = frozenset('\t\r\n')
|
||||
+ max_length = 2048
|
||||
|
||||
def __init__(self, schemes=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -100,7 +101,7 @@ def __init__(self, schemes=None, **kwargs):
|
||||
self.schemes = schemes
|
||||
|
||||
def __call__(self, value):
|
||||
- if not isinstance(value, str):
|
||||
+ 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, params={'value': value})
|
||||
@@ -210,7 +211,9 @@ def __init__(self, message=None, code=None, allowlist=None, *, whitelist=None):
|
||||
self.domain_allowlist = allowlist
|
||||
|
||||
def __call__(self, value):
|
||||
- if not value or '@' not in value:
|
||||
+ # The maximum length of an email is 320 characters per RFC 3696
|
||||
+ # section 3.
|
||||
+ if not value or '@' not in value or len(value) > 320:
|
||||
raise ValidationError(self.message, code=self.code, params={'value': value})
|
||||
|
||||
user_part, domain_part = value.rsplit('@', 1)
|
||||
diff --git a/django/forms/fields.py b/django/forms/fields.py
|
||||
index 0214d60c1cf1..8adb09e38294 100644
|
||||
--- a/django/forms/fields.py
|
||||
+++ b/django/forms/fields.py
|
||||
@@ -540,6 +540,9 @@ class EmailField(CharField):
|
||||
default_validators = [validators.validate_email]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
+ # The default maximum length of an email is 320 characters per RFC 3696
|
||||
+ # section 3.
|
||||
+ kwargs.setdefault("max_length", 320)
|
||||
super().__init__(strip=True, **kwargs)
|
||||
|
||||
|
||||
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
|
||||
index 9438214a28ce..5b485f215384 100644
|
||||
--- a/docs/ref/forms/fields.txt
|
||||
+++ b/docs/ref/forms/fields.txt
|
||||
@@ -592,7 +592,12 @@ For each field, we describe the default widget used if you don't specify
|
||||
* Error message keys: ``required``, ``invalid``
|
||||
|
||||
Has three optional arguments ``max_length``, ``min_length``, and
|
||||
- ``empty_value`` which work just as they do for :class:`CharField`.
|
||||
+ ``empty_value`` which work just as they do for :class:`CharField`. The
|
||||
+ ``max_length`` argument defaults to 320 (see :rfc:`3696#section-3`).
|
||||
+
|
||||
+ .. versionchanged:: 3.2.20
|
||||
+
|
||||
+ The default value for ``max_length`` was changed to 320 characters.
|
||||
|
||||
``FileField``
|
||||
-------------
|
||||
diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt
|
||||
index 50761e5a425c..b22762b17b93 100644
|
||||
--- a/docs/ref/validators.txt
|
||||
+++ b/docs/ref/validators.txt
|
||||
@@ -130,6 +130,11 @@ to, or in lieu of custom ``field.clean()`` methods.
|
||||
:param code: If not ``None``, overrides :attr:`code`.
|
||||
:param allowlist: If not ``None``, overrides :attr:`allowlist`.
|
||||
|
||||
+ An :class:`EmailValidator` ensures that a value looks like an email, and
|
||||
+ raises a :exc:`~django.core.exceptions.ValidationError` with
|
||||
+ :attr:`message` and :attr:`code` if it doesn't. Values longer than 320
|
||||
+ characters are always considered invalid.
|
||||
+
|
||||
.. attribute:: message
|
||||
|
||||
The error message used by
|
||||
@@ -158,13 +163,19 @@ to, or in lieu of custom ``field.clean()`` methods.
|
||||
The undocumented ``domain_whitelist`` attribute is deprecated. Use
|
||||
``domain_allowlist`` instead.
|
||||
|
||||
+ .. versionchanged:: 3.2.20
|
||||
+
|
||||
+ In older versions, values longer than 320 characters could be
|
||||
+ considered valid.
|
||||
+
|
||||
``URLValidator``
|
||||
----------------
|
||||
|
||||
.. class:: URLValidator(schemes=None, regex=None, message=None, code=None)
|
||||
|
||||
A :class:`RegexValidator` subclass that ensures a value looks like a URL,
|
||||
- and raises an error code of ``'invalid'`` if it doesn't.
|
||||
+ and raises an error code of ``'invalid'`` if it doesn't. Values longer than
|
||||
+ :attr:`max_length` characters are always considered invalid.
|
||||
|
||||
Loopback addresses and reserved IP spaces are considered valid. Literal
|
||||
IPv6 addresses (:rfc:`3986#section-3.2.2`) and Unicode domains are both
|
||||
@@ -181,6 +192,18 @@ to, or in lieu of custom ``field.clean()`` methods.
|
||||
|
||||
.. _valid URI schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
|
||||
|
||||
+ .. attribute:: max_length
|
||||
+
|
||||
+ .. versionadded:: 3.2.20
|
||||
+
|
||||
+ The maximum length of values that could be considered valid. Defaults
|
||||
+ to 2048 characters.
|
||||
+
|
||||
+ .. versionchanged:: 3.2.20
|
||||
+
|
||||
+ In older versions, values longer than 2048 characters could be
|
||||
+ considered valid.
|
||||
+
|
||||
``validate_email``
|
||||
------------------
|
||||
|
||||
diff --git a/tests/forms_tests/field_tests/test_emailfield.py b/tests/forms_tests/field_tests/test_emailfield.py
|
||||
index 8b85e4dcc144..19d315205d7e 100644
|
||||
--- a/tests/forms_tests/field_tests/test_emailfield.py
|
||||
+++ b/tests/forms_tests/field_tests/test_emailfield.py
|
||||
@@ -9,7 +9,10 @@ class EmailFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
|
||||
|
||||
def test_emailfield_1(self):
|
||||
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.'"):
|
||||
f.clean('')
|
||||
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
|
||||
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
|
||||
index 26f8ecafea44..82a32af403a0 100644
|
||||
--- a/tests/forms_tests/tests/test_forms.py
|
||||
+++ b/tests/forms_tests/tests/test_forms.py
|
||||
@@ -422,11 +422,18 @@ class SignupForm(Form):
|
||||
get_spam = BooleanField()
|
||||
|
||||
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>')
|
||||
|
||||
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(
|
||||
str(f['get_spam']),
|
||||
'<input checked type="checkbox" name="get_spam" required>',
|
||||
@@ -2824,7 +2831,7 @@ class Person(Form):
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</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>
|
||||
<label class="required" for="id_age">Age:</label> <input type="number" name="age" id="id_age" required></li>"""
|
||||
)
|
||||
@@ -2840,7 +2847,7 @@ class Person(Form):
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</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>
|
||||
<p class="required error"><label class="required" for="id_age">Age:</label>
|
||||
<input type="number" name="age" id="id_age" required></p>"""
|
||||
@@ -2859,7 +2866,7 @@ class Person(Form):
|
||||
<option value="false">No</option>
|
||||
</select></td></tr>
|
||||
<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>
|
||||
<td><ul class="errorlist"><li>This field is required.</li></ul>
|
||||
<input type="number" name="age" id="id_age" required></td></tr>"""
|
||||
@@ -3489,7 +3496,7 @@ class CommentForm(Form):
|
||||
f = CommentForm(data, auto_id=False, error_class=DivErrorList)
|
||||
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>
|
||||
-<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>
|
||||
<p>Comment: <input type="text" name="comment" required></p>""")
|
||||
|
||||
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
|
||||
index e39d0e3a1cef..1065727a974e 100644
|
||||
--- a/tests/validators/tests.py
|
||||
+++ b/tests/validators/tests.py
|
||||
@@ -59,6 +59,7 @@
|
||||
|
||||
(validate_email, 'example@atm.%s' % ('a' * 64), 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, '', ValidationError),
|
||||
(validate_email, 'abc', ValidationError),
|
||||
@@ -246,6 +247,16 @@
|
||||
(URLValidator(), None, ValidationError),
|
||||
(URLValidator(), 56, ValidationError),
|
||||
(URLValidator(), 'no_scheme', ValidationError),
|
||||
+ (
|
||||
+ URLValidator(),
|
||||
+ "http://example." + ("a" * 63 + ".") * 1000 + "com",
|
||||
+ ValidationError,
|
||||
+ ),
|
||||
+ (
|
||||
+ URLValidator(),
|
||||
+ "http://userid:password" + "d" * 2000 + "@example.aaaaaaaaaaaaa.com",
|
||||
+ None,
|
||||
+ ),
|
||||
# Newlines and tabs are not accepted.
|
||||
(URLValidator(), 'http://www.djangoproject.com/\n', ValidationError),
|
||||
(URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError),
|
||||
@ -1,83 +0,0 @@
|
||||
From 6f030b1149bd8fa4ba90452e77cb3edc095ce54e Mon Sep 17 00:00:00 2001
|
||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
Date: Tue, 22 Aug 2023 08:53:03 +0200
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2023-41164 -- Fixed potential DoS in
|
||||
django.utils.encoding.uri_to_iri().
|
||||
|
||||
Thanks MProgrammer (https://hackerone.com/mprogrammer) for the report.
|
||||
|
||||
Origin: https://github.com/django/django/commit/6f030b1149bd8fa4ba90452e77cb3edc095ce54e
|
||||
|
||||
Co-authored-by: nessita <124304+nessita@users.noreply.github.com>
|
||||
---
|
||||
django/utils/encoding.py | 6 ++++--
|
||||
docs/releases/3.2.21.txt | 7 ++++++-
|
||||
tests/utils_tests/test_encoding.py | 21 ++++++++++++++++++++-
|
||||
3 files changed, 30 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/django/utils/encoding.py b/django/utils/encoding.py
|
||||
index e1ebacef4705..c5c4463b1c22 100644
|
||||
--- a/django/utils/encoding.py
|
||||
+++ b/django/utils/encoding.py
|
||||
@@ -229,6 +229,7 @@ def repercent_broken_unicode(path):
|
||||
repercent-encode any octet produced that is not part of a strictly legal
|
||||
UTF-8 octet sequence.
|
||||
"""
|
||||
+ changed_parts = []
|
||||
while True:
|
||||
try:
|
||||
path.decode()
|
||||
@@ -236,9 +237,10 @@ def repercent_broken_unicode(path):
|
||||
# CVE-2019-14235: A recursion shouldn't be used since the exception
|
||||
# handling uses massive amounts of memory
|
||||
repercent = quote(path[e.start:e.end], safe=b"/#%[]=:;$&()+,!?*@'~")
|
||||
- path = path[:e.start] + repercent.encode() + path[e.end:]
|
||||
+ changed_parts.append(path[:e.start] + repercent.encode())
|
||||
+ path = path[e.end:]
|
||||
else:
|
||||
- return path
|
||||
+ return b"".join(changed_parts) + path
|
||||
|
||||
|
||||
def filepath_to_uri(path):
|
||||
diff --git a/tests/utils_tests/test_encoding.py b/tests/utils_tests/test_encoding.py
|
||||
index 36f2d8665f3c..42779050cb3a 100644
|
||||
--- a/tests/utils_tests/test_encoding.py
|
||||
+++ b/tests/utils_tests/test_encoding.py
|
||||
@@ -1,9 +1,10 @@
|
||||
import datetime
|
||||
+import inspect
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
-from urllib.parse import quote_plus
|
||||
+from urllib.parse import quote, quote_plus
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils.encoding import (
|
||||
@@ -101,6 +102,24 @@ def test_repercent_broken_unicode_recursion_error(self):
|
||||
except RecursionError:
|
||||
self.fail('Unexpected RecursionError raised.')
|
||||
|
||||
+ def test_repercent_broken_unicode_small_fragments(self):
|
||||
+ data = b"test\xfctest\xfctest\xfc"
|
||||
+ decoded_paths = []
|
||||
+
|
||||
+ def mock_quote(*args, **kwargs):
|
||||
+ # The second frame is the call to repercent_broken_unicode().
|
||||
+ decoded_paths.append(inspect.currentframe().f_back.f_locals["path"])
|
||||
+ return quote(*args, **kwargs)
|
||||
+
|
||||
+ with mock.patch("django.utils.encoding.quote", mock_quote):
|
||||
+ self.assertEqual(repercent_broken_unicode(data), b"test%FCtest%FCtest%FC")
|
||||
+
|
||||
+ # decode() is called on smaller fragment of the path each time.
|
||||
+ self.assertEqual(
|
||||
+ decoded_paths,
|
||||
+ [b"test\xfctest\xfctest\xfc", b"test\xfctest\xfc", b"test\xfc"],
|
||||
+ )
|
||||
+
|
||||
|
||||
class TestRFC3987IEncodingUtils(unittest.TestCase):
|
||||
|
||||
@ -1,168 +0,0 @@
|
||||
From ccdade1a0262537868d7ca64374de3d957ca50c5 Mon Sep 17 00:00:00 2001
|
||||
From: Natalia <124304+nessita@users.noreply.github.com>
|
||||
Date: Tue, 19 Sep 2023 09:51:48 -0300
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2023-43665 -- Mitigated potential DoS in
|
||||
django.utils.text.Truncator when truncating HTML text.
|
||||
|
||||
Thanks Wenchao Li of Alibaba Group for the report.
|
||||
|
||||
Origin:
|
||||
https://github.com/django/django/commit/ccdade1a0262537868d7ca64374de3d957ca50c5
|
||||
---
|
||||
django/utils/text.py | 18 ++++++++++++++++-
|
||||
docs/ref/templates/builtins.txt | 20 +++++++++++++++++++
|
||||
tests/utils_tests/test_text.py | 35 ++++++++++++++++++++++++---------
|
||||
3 files changed, 63 insertions(+), 10 deletions(-)
|
||||
|
||||
diff --git a/django/utils/text.py b/django/utils/text.py
|
||||
index baa44f2..83e258f 100644
|
||||
--- a/django/utils/text.py
|
||||
+++ b/django/utils/text.py
|
||||
@@ -60,7 +60,14 @@ def wrap(text, width):
|
||||
class Truncator(SimpleLazyObject):
|
||||
"""
|
||||
An object used to truncate text, either by characters or words.
|
||||
+
|
||||
+ When truncating HTML text (either chars or words), input will be limited to
|
||||
+ at most `MAX_LENGTH_HTML` characters.
|
||||
"""
|
||||
+
|
||||
+ # 5 million characters are approximately 4000 text pages or 3 web pages.
|
||||
+ MAX_LENGTH_HTML = 5_000_000
|
||||
+
|
||||
def __init__(self, text):
|
||||
super().__init__(lambda: str(text))
|
||||
|
||||
@@ -157,6 +164,11 @@ class Truncator(SimpleLazyObject):
|
||||
if words and length <= 0:
|
||||
return ''
|
||||
|
||||
+ size_limited = False
|
||||
+ if len(text) > self.MAX_LENGTH_HTML:
|
||||
+ text = text[: self.MAX_LENGTH_HTML]
|
||||
+ size_limited = True
|
||||
+
|
||||
html4_singlets = (
|
||||
'br', 'col', 'link', 'base', 'img',
|
||||
'param', 'area', 'hr', 'input'
|
||||
@@ -206,10 +218,14 @@ class Truncator(SimpleLazyObject):
|
||||
# Add it to the start of the open tags list
|
||||
open_tags.insert(0, tagname)
|
||||
|
||||
+ truncate_text = self.add_truncation_text("", truncate)
|
||||
+
|
||||
if current_len <= length:
|
||||
+ if size_limited and truncate_text:
|
||||
+ text += truncate_text
|
||||
return text
|
||||
+
|
||||
out = text[:end_text_pos]
|
||||
- truncate_text = self.add_truncation_text('', truncate)
|
||||
if truncate_text:
|
||||
out += truncate_text
|
||||
# Close any tags still open
|
||||
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
|
||||
index 22509a2..a6fd971 100644
|
||||
--- a/docs/ref/templates/builtins.txt
|
||||
+++ b/docs/ref/templates/builtins.txt
|
||||
@@ -2348,6 +2348,16 @@ If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
|
||||
|
||||
Newlines in the HTML content will be preserved.
|
||||
|
||||
+.. admonition:: Size of input string
|
||||
+
|
||||
+ Processing large, potentially malformed HTML strings can be
|
||||
+ resource-intensive and impact service performance. ``truncatechars_html``
|
||||
+ limits input to the first five million characters.
|
||||
+
|
||||
+.. versionchanged:: 3.2.22
|
||||
+
|
||||
+ In older versions, strings over five million characters were processed.
|
||||
+
|
||||
.. templatefilter:: truncatewords
|
||||
|
||||
``truncatewords``
|
||||
@@ -2386,6 +2396,16 @@ If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
|
||||
|
||||
Newlines in the HTML content will be preserved.
|
||||
|
||||
+.. admonition:: Size of input string
|
||||
+
|
||||
+ Processing large, potentially malformed HTML strings can be
|
||||
+ resource-intensive and impact service performance. ``truncatewords_html``
|
||||
+ limits input to the first five million characters.
|
||||
+
|
||||
+.. versionchanged:: 3.2.22
|
||||
+
|
||||
+ In older versions, strings over five million characters were processed.
|
||||
+
|
||||
.. templatefilter:: unordered_list
|
||||
|
||||
``unordered_list``
|
||||
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
|
||||
index d2a94fc..0a6f0bc 100644
|
||||
--- a/tests/utils_tests/test_text.py
|
||||
+++ b/tests/utils_tests/test_text.py
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import sys
|
||||
+from unittest.mock import patch
|
||||
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.test import SimpleTestCase, ignore_warnings
|
||||
@@ -90,11 +91,17 @@ class TestUtilsText(SimpleTestCase):
|
||||
# lazy strings are handled correctly
|
||||
self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(10), 'The quick…')
|
||||
|
||||
- def test_truncate_chars_html(self):
|
||||
+ @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
|
||||
+ def test_truncate_chars_html_size_limit(self):
|
||||
+ max_len = text.Truncator.MAX_LENGTH_HTML
|
||||
+ bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
|
||||
+ valid_html = "<p>Joel is a slug</p>" # 14 chars
|
||||
perf_test_values = [
|
||||
- (('</a' + '\t' * 50000) + '//>', None),
|
||||
- ('&' * 50000, '&' * 9 + '…'),
|
||||
- ('_X<<<<<<<<<<<>', None),
|
||||
+ ("</a" + "\t" * (max_len - 6) + "//>", None),
|
||||
+ ("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * 6 + "…"),
|
||||
+ ("&" * bigger_len, "&" * 9 + "…"),
|
||||
+ ("_X<<<<<<<<<<<>", None),
|
||||
+ (valid_html * bigger_len, "<p>Joel is a…</p>"), # 10 chars
|
||||
]
|
||||
for value, expected in perf_test_values:
|
||||
with self.subTest(value=value):
|
||||
@@ -152,15 +159,25 @@ class TestUtilsText(SimpleTestCase):
|
||||
truncator = text.Truncator('<p>I <3 python, what about you?</p>')
|
||||
self.assertEqual('<p>I <3 python,…</p>', truncator.words(3, html=True))
|
||||
|
||||
+ @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
|
||||
+ def test_truncate_words_html_size_limit(self):
|
||||
+ max_len = text.Truncator.MAX_LENGTH_HTML
|
||||
+ bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
|
||||
+ valid_html = "<p>Joel is a slug</p>" # 4 words
|
||||
perf_test_values = [
|
||||
- ('</a' + '\t' * 50000) + '//>',
|
||||
- '&' * 50000,
|
||||
- '_X<<<<<<<<<<<>',
|
||||
+ ("</a" + "\t" * (max_len - 6) + "//>", None),
|
||||
+ ("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * (max_len - 3) + "…"),
|
||||
+ ("&" * max_len, None), # no change
|
||||
+ ("&" * bigger_len, "&" * max_len + "…"),
|
||||
+ ("_X<<<<<<<<<<<>", None),
|
||||
+ (valid_html * bigger_len, valid_html * 12 + "<p>Joel is…</p>"), # 50 words
|
||||
]
|
||||
- for value in perf_test_values:
|
||||
+ for value, expected in perf_test_values:
|
||||
with self.subTest(value=value):
|
||||
truncator = text.Truncator(value)
|
||||
- self.assertEqual(value, truncator.words(50, html=True))
|
||||
+ self.assertEqual(
|
||||
+ expected if expected else value, truncator.words(50, html=True)
|
||||
+ )
|
||||
|
||||
def test_wrap(self):
|
||||
digits = '1234 67 9'
|
||||
--
|
||||
2.30.0
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
From f9a7fb8466a7ba4857eaf930099b5258f3eafb2b Mon Sep 17 00:00:00 2001
|
||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
Date: Tue, 17 Oct 2023 11:48:32 +0200
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2023-46695 -- Fixed potential DoS in
|
||||
UsernameField on Windows.
|
||||
|
||||
Thanks MProgrammer (https://hackerone.com/mprogrammer) for the report.
|
||||
---
|
||||
django/contrib/auth/forms.py | 10 +++++++++-
|
||||
tests/auth_tests/test_forms.py | 8 +++++++-
|
||||
2 files changed, 16 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
|
||||
index 20d8922..fb7cfda 100644
|
||||
--- a/django/contrib/auth/forms.py
|
||||
+++ b/django/contrib/auth/forms.py
|
||||
@@ -62,7 +62,15 @@ class ReadOnlyPasswordHashField(forms.Field):
|
||||
|
||||
class UsernameField(forms.CharField):
|
||||
def to_python(self, value):
|
||||
- return unicodedata.normalize('NFKC', super().to_python(value))
|
||||
+ value = super().to_python(value)
|
||||
+ if self.max_length is not None and len(value) > self.max_length:
|
||||
+ # Normalization can increase the string length (e.g.
|
||||
+ # "ff" -> "ff", "½" -> "1⁄2") but cannot reduce it, so there is no
|
||||
+ # point in normalizing invalid data. Moreover, Unicode
|
||||
+ # normalization is very slow on Windows and can be a DoS attack
|
||||
+ # vector.
|
||||
+ return value
|
||||
+ return unicodedata.normalize("NFKC", value)
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
return {
|
||||
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
|
||||
index 7a731be..c0e1975 100644
|
||||
--- a/tests/auth_tests/test_forms.py
|
||||
+++ b/tests/auth_tests/test_forms.py
|
||||
@@ -5,7 +5,7 @@ from unittest import mock
|
||||
from django.contrib.auth.forms import (
|
||||
AdminPasswordChangeForm, AuthenticationForm, PasswordChangeForm,
|
||||
PasswordResetForm, ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget,
|
||||
- SetPasswordForm, UserChangeForm, UserCreationForm,
|
||||
+ SetPasswordForm, UserChangeForm, UserCreationForm, UsernameField,
|
||||
)
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.signals import user_login_failed
|
||||
@@ -132,6 +132,12 @@ class UserCreationFormTest(TestDataMixin, TestCase):
|
||||
self.assertNotEqual(user.username, ohm_username)
|
||||
self.assertEqual(user.username, 'testΩ') # U+03A9 GREEK CAPITAL LETTER OMEGA
|
||||
|
||||
+ def test_invalid_username_no_normalize(self):
|
||||
+ field = UsernameField(max_length=254)
|
||||
+ # Usernames are not normalized if they are too long.
|
||||
+ self.assertEqual(field.to_python("½" * 255), "½" * 255)
|
||||
+ self.assertEqual(field.to_python("ff" * 254), "ff" * 254)
|
||||
+
|
||||
def test_duplicate_normalized_unicode(self):
|
||||
"""
|
||||
To prevent almost identical usernames, visually identical but differing
|
||||
--
|
||||
2.30.0
|
||||
|
||||
@ -1,204 +0,0 @@
|
||||
From c1171ffbd570db90ca206c30f8e2b9f691243820 Mon Sep 17 00:00:00 2001
|
||||
From: Adam Johnson <me@adamj.eu>
|
||||
Date: Mon, 22 Jan 2024 13:21:13 +0000
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2024-24680 -- Mitigated potential DoS in
|
||||
intcomma template filter.
|
||||
|
||||
Thanks Seokchan Yoon for the report.
|
||||
|
||||
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
|
||||
Co-authored-by: Shai Berger <shai@platonix.com>
|
||||
---
|
||||
.../contrib/humanize/templatetags/humanize.py | 13 +-
|
||||
tests/humanize_tests/tests.py | 140 ++++++++++++++++--
|
||||
2 files changed, 135 insertions(+), 18 deletions(-)
|
||||
|
||||
diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py
|
||||
index 753a0d9..238aaf2 100644
|
||||
--- a/django/contrib/humanize/templatetags/humanize.py
|
||||
+++ b/django/contrib/humanize/templatetags/humanize.py
|
||||
@@ -70,12 +70,13 @@ def intcomma(value, use_l10n=True):
|
||||
return intcomma(value, False)
|
||||
else:
|
||||
return number_format(value, use_l10n=True, force_grouping=True)
|
||||
- orig = str(value)
|
||||
- new = re.sub(r"^(-?\d+)(\d{3})", r'\g<1>,\g<2>', orig)
|
||||
- if orig == new:
|
||||
- return new
|
||||
- else:
|
||||
- return intcomma(new, use_l10n)
|
||||
+ result = str(value)
|
||||
+ match = re.match(r"-?\d+", result)
|
||||
+ if match:
|
||||
+ prefix = match[0]
|
||||
+ prefix_with_commas = re.sub(r"\d{3}", r"\g<0>,", prefix[::-1])[::-1]
|
||||
+ result = prefix_with_commas + result[len(prefix) :]
|
||||
+ return result
|
||||
|
||||
|
||||
# A tuple of standard large number to their converters
|
||||
diff --git a/tests/humanize_tests/tests.py b/tests/humanize_tests/tests.py
|
||||
index a0d16bb..3c22787 100644
|
||||
--- a/tests/humanize_tests/tests.py
|
||||
+++ b/tests/humanize_tests/tests.py
|
||||
@@ -66,28 +66,144 @@ class HumanizeTests(SimpleTestCase):
|
||||
|
||||
def test_intcomma(self):
|
||||
test_list = (
|
||||
- 100, 1000, 10123, 10311, 1000000, 1234567.25, '100', '1000',
|
||||
- '10123', '10311', '1000000', '1234567.1234567',
|
||||
- Decimal('1234567.1234567'), None,
|
||||
+ 100,
|
||||
+ -100,
|
||||
+ 1000,
|
||||
+ -1000,
|
||||
+ 10123,
|
||||
+ -10123,
|
||||
+ 10311,
|
||||
+ -10311,
|
||||
+ 1000000,
|
||||
+ -1000000,
|
||||
+ 1234567.25,
|
||||
+ -1234567.25,
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1000",
|
||||
+ "-1000",
|
||||
+ "10123",
|
||||
+ "-10123",
|
||||
+ "10311",
|
||||
+ "-10311",
|
||||
+ "1000000",
|
||||
+ "-1000000",
|
||||
+ "1234567.1234567",
|
||||
+ "-1234567.1234567",
|
||||
+ Decimal("1234567.1234567"),
|
||||
+ Decimal("-1234567.1234567"),
|
||||
+ None,
|
||||
+ "1234567",
|
||||
+ "-1234567",
|
||||
+ "1234567.12",
|
||||
+ "-1234567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
result_list = (
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
|
||||
- '1,234,567.1234567', None,
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.25",
|
||||
+ "-1,234,567.25",
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ None,
|
||||
+ "1,234,567",
|
||||
+ "-1,234,567",
|
||||
+ "1,234,567.12",
|
||||
+ "-1,234,567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
with translation.override('en'):
|
||||
self.humanize_tester(test_list, result_list, 'intcomma')
|
||||
|
||||
def test_l10n_intcomma(self):
|
||||
test_list = (
|
||||
- 100, 1000, 10123, 10311, 1000000, 1234567.25, '100', '1000',
|
||||
- '10123', '10311', '1000000', '1234567.1234567',
|
||||
- Decimal('1234567.1234567'), None,
|
||||
+ 100,
|
||||
+ -100,
|
||||
+ 1000,
|
||||
+ -1000,
|
||||
+ 10123,
|
||||
+ -10123,
|
||||
+ 10311,
|
||||
+ -10311,
|
||||
+ 1000000,
|
||||
+ -1000000,
|
||||
+ 1234567.25,
|
||||
+ -1234567.25,
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1000",
|
||||
+ "-1000",
|
||||
+ "10123",
|
||||
+ "-10123",
|
||||
+ "10311",
|
||||
+ "-10311",
|
||||
+ "1000000",
|
||||
+ "-1000000",
|
||||
+ "1234567.1234567",
|
||||
+ "-1234567.1234567",
|
||||
+ Decimal("1234567.1234567"),
|
||||
+ -Decimal("1234567.1234567"),
|
||||
+ None,
|
||||
+ "1234567",
|
||||
+ "-1234567",
|
||||
+ "1234567.12",
|
||||
+ "-1234567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
result_list = (
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
|
||||
- '1,234,567.1234567', None,
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.25",
|
||||
+ "-1,234,567.25",
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ None,
|
||||
+ "1,234,567",
|
||||
+ "-1,234,567",
|
||||
+ "1,234,567.12",
|
||||
+ "-1,234,567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=False):
|
||||
with translation.override('en'):
|
||||
--
|
||||
2.33.0
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
From 072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521 Mon Sep 17 00:00:00 2001
|
||||
From: Shai Berger <shai@platonix.com>
|
||||
Date: Mon, 19 Feb 2024 13:56:37 +0100
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2024-27351 -- Prevented potential ReDoS in
|
||||
Truncator.words().
|
||||
|
||||
Thanks Seokchan Yoon for the report.
|
||||
|
||||
Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
---
|
||||
django/utils/text.py | 57 ++++++++++++++++++++++++++++++++--
|
||||
tests/utils_tests/test_text.py | 26 ++++++++++++++++
|
||||
2 files changed, 81 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/django/utils/text.py b/django/utils/text.py
|
||||
index 83e258f..88da9a2 100644
|
||||
--- a/django/utils/text.py
|
||||
+++ b/django/utils/text.py
|
||||
@@ -18,8 +18,61 @@ def capfirst(x):
|
||||
return x and str(x)[0].upper() + str(x)[1:]
|
||||
|
||||
|
||||
-# Set up regular expressions
|
||||
-re_words = _lazy_re_compile(r'<[^>]+?>|([^<>\s]+)', re.S)
|
||||
+# ----- Begin security-related performance workaround -----
|
||||
+
|
||||
+# We used to have, below
|
||||
+#
|
||||
+# re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S)
|
||||
+#
|
||||
+# But it was shown that this regex, in the way we use it here, has some
|
||||
+# catastrophic edge-case performance features. Namely, when it is applied to
|
||||
+# text with only open brackets "<<<...". The class below provides the services
|
||||
+# and correct answers for the use cases, but in these edge cases does it much
|
||||
+# faster.
|
||||
+re_notag = _lazy_re_compile(r"([^<>\s]+)", re.S)
|
||||
+re_prt = _lazy_re_compile(r"<|([^<>\s]+)", re.S)
|
||||
+
|
||||
+
|
||||
+class WordsRegex:
|
||||
+ @staticmethod
|
||||
+ def search(text, pos):
|
||||
+ # Look for "<" or a non-tag word.
|
||||
+ partial = re_prt.search(text, pos)
|
||||
+ if partial is None or partial[1] is not None:
|
||||
+ return partial
|
||||
+
|
||||
+ # "<" was found, look for a closing ">".
|
||||
+ end = text.find(">", partial.end(0))
|
||||
+ if end < 0:
|
||||
+ # ">" cannot be found, look for a word.
|
||||
+ return re_notag.search(text, pos + 1)
|
||||
+ else:
|
||||
+ # "<" followed by a ">" was found -- fake a match.
|
||||
+ end += 1
|
||||
+ return FakeMatch(text[partial.start(0): end], end)
|
||||
+
|
||||
+
|
||||
+class FakeMatch:
|
||||
+ __slots__ = ["_text", "_end"]
|
||||
+
|
||||
+ def end(self, group=0):
|
||||
+ assert group == 0, "This specific object takes only group=0"
|
||||
+ return self._end
|
||||
+
|
||||
+ def __getitem__(self, group):
|
||||
+ if group == 1:
|
||||
+ return None
|
||||
+ assert group == 0, "This specific object takes only group in {0,1}"
|
||||
+ return self._text
|
||||
+
|
||||
+ def __init__(self, text, end):
|
||||
+ self._text, self._end = text, end
|
||||
+
|
||||
+
|
||||
+# ----- End security-related performance workaround -----
|
||||
+
|
||||
+# Set up regular expressions.
|
||||
+re_words = WordsRegex
|
||||
re_chars = _lazy_re_compile(r'<[^>]+?>|(.)', re.S)
|
||||
re_tag = _lazy_re_compile(r'<(/)?(\S+?)(?:(\s*/)|\s.*?)?>', re.S)
|
||||
re_newlines = _lazy_re_compile(r'\r\n|\r') # Used in normalize_newlines
|
||||
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
|
||||
index 0a6f0bc..758919c 100644
|
||||
--- a/tests/utils_tests/test_text.py
|
||||
+++ b/tests/utils_tests/test_text.py
|
||||
@@ -159,6 +159,32 @@ class TestUtilsText(SimpleTestCase):
|
||||
truncator = text.Truncator('<p>I <3 python, what about you?</p>')
|
||||
self.assertEqual('<p>I <3 python,…</p>', truncator.words(3, html=True))
|
||||
|
||||
+ # Only open brackets.
|
||||
+ test = "<" * 60_000
|
||||
+ truncator = text.Truncator(test)
|
||||
+ self.assertEqual(truncator.words(1, html=True), test)
|
||||
+
|
||||
+ # Tags with special chars in attrs.
|
||||
+ truncator = text.Truncator(
|
||||
+ """<i style="margin: 5%; font: *;">Hello, my dear lady!</i>"""
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ """<i style="margin: 5%; font: *;">Hello, my dear…</i>""",
|
||||
+ truncator.words(3, html=True),
|
||||
+ )
|
||||
+
|
||||
+ # Tags with special non-latin chars in attrs.
|
||||
+ truncator = text.Truncator("""<p data-x="א">Hello, my dear lady!</p>""")
|
||||
+ self.assertEqual(
|
||||
+ """<p data-x="א">Hello, my dear…</p>""",
|
||||
+ truncator.words(3, html=True),
|
||||
+ )
|
||||
+
|
||||
+ # Misplaced brackets.
|
||||
+ truncator = text.Truncator("hello >< world")
|
||||
+ self.assertEqual(truncator.words(1, html=True), "hello…")
|
||||
+ self.assertEqual(truncator.words(2, html=True), "hello >< world")
|
||||
+
|
||||
@patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
|
||||
def test_truncate_words_html_size_limit(self):
|
||||
max_len = text.Truncator.MAX_LENGTH_HTML
|
||||
--
|
||||
2.33.0
|
||||
|
||||
Binary file not shown.
@ -1,74 +0,0 @@
|
||||
From 8c5a1dfe34ea52cc2af21064a8654bfaa8b7a012 Mon Sep 17 00:00:00 2001
|
||||
From: Carlton Gibson <carlton.gibson@noumenal.es>
|
||||
Date: Wed, 27 Jul 2022 10:27:42 +0200
|
||||
Subject: [PATCH] [3.2.x] Fixed CVE-2022-36359: Escaped filename in
|
||||
Content-Disposition header.
|
||||
|
||||
Thanks to Motoyasu Saburi for the report.
|
||||
---
|
||||
django/http/response.py | 4 +++-
|
||||
docs/releases/3.2.15.txt | 8 ++++++-
|
||||
tests/responses/test_fileresponse.py | 35 ++++++++++++++++++++++++++++
|
||||
3 files changed, 45 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/django/http/response.py b/django/http/response.py
|
||||
index 1c22edaff3..73f87d7bda 100644
|
||||
--- a/django/http/response.py
|
||||
+++ b/django/http/response.py
|
||||
@@ -485,7 +485,9 @@ class FileResponse(StreamingHttpResponse):
|
||||
disposition = 'attachment' if self.as_attachment else 'inline'
|
||||
try:
|
||||
filename.encode('ascii')
|
||||
- file_expr = 'filename="{}"'.format(filename)
|
||||
+ file_expr = 'filename="{}"'.format(
|
||||
+ filename.replace('\\', '\\\\').replace('"', r'\"')
|
||||
+ )
|
||||
except UnicodeEncodeError:
|
||||
file_expr = "filename*=utf-8''{}".format(quote(filename))
|
||||
self.headers['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
|
||||
diff --git a/tests/responses/test_fileresponse.py b/tests/responses/test_fileresponse.py
|
||||
index 46d407bdf5..b4ef82ef3e 100644
|
||||
--- a/tests/responses/test_fileresponse.py
|
||||
+++ b/tests/responses/test_fileresponse.py
|
||||
@@ -89,3 +89,38 @@ class FileResponseTests(SimpleTestCase):
|
||||
response.headers['Content-Disposition'],
|
||||
"attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt"
|
||||
)
|
||||
+
|
||||
+ def test_content_disposition_escaping(self):
|
||||
+ # fmt: off
|
||||
+ tests = [
|
||||
+ (
|
||||
+ 'multi-part-one";\" dummy".txt',
|
||||
+ r"multi-part-one\";\" dummy\".txt"
|
||||
+ ),
|
||||
+ ]
|
||||
+ # fmt: on
|
||||
+ # Non-escape sequence backslashes are path segments on Windows, and are
|
||||
+ # eliminated by an os.path.basename() check in FileResponse.
|
||||
+ if sys.platform != "win32":
|
||||
+ # fmt: off
|
||||
+ tests += [
|
||||
+ (
|
||||
+ 'multi-part-one\\";\" dummy".txt',
|
||||
+ r"multi-part-one\\\";\" dummy\".txt"
|
||||
+ ),
|
||||
+ (
|
||||
+ 'multi-part-one\\";\\\" dummy".txt',
|
||||
+ r"multi-part-one\\\";\\\" dummy\".txt"
|
||||
+ )
|
||||
+ ]
|
||||
+ # fmt: on
|
||||
+ for filename, escaped in tests:
|
||||
+ with self.subTest(filename=filename, escaped=escaped):
|
||||
+ response = FileResponse(
|
||||
+ io.BytesIO(b"binary content"), filename=filename, as_attachment=True
|
||||
+ )
|
||||
+ response.close()
|
||||
+ self.assertEqual(
|
||||
+ response.headers["Content-Disposition"],
|
||||
+ f'attachment; filename="{escaped}"',
|
||||
+ )
|
||||
--
|
||||
2.36.1
|
||||
|
||||
@ -1,28 +1,11 @@
|
||||
%global _empty_manifest_terminate_build 0
|
||||
Name: python-django
|
||||
Version: 3.2.12
|
||||
Release: 10
|
||||
Version: 4.2.15
|
||||
Release: 1
|
||||
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
|
||||
License: Apache-2.0 and Python-2.0 and BSD-3-Clause
|
||||
URL: https://www.djangoproject.com/
|
||||
Source0: https://github.com/django/django/archive/refs/tags/%{version}.tar.gz
|
||||
|
||||
#https://github.com/django/django/commit/a9010fe5555e6086a9d9ae50069579400ef0685e
|
||||
Patch0: CVE-2022-34265.patch
|
||||
Patch1: backport-CVE-2022-36359.patch
|
||||
Patch2: CVE-2023-23969.patch
|
||||
Patch3: CVE-2023-24580.patch
|
||||
Patch4: CVE-2023-31047.patch
|
||||
Patch5: CVE-2023-36053.patch
|
||||
Patch6: CVE-2023-41164.patch
|
||||
# https://github.com/django/django/commit/ccdade1a0262537868d7ca64374de3d957ca50c5
|
||||
Patch7: CVE-2023-43665.patch
|
||||
# https://github.com/django/django/commit/f9a7fb8466a7ba4857eaf930099b5258f3eafb2b
|
||||
Patch8: CVE-2023-46695.patch
|
||||
# https://github.com/django/django/commit/c1171ffbd570db90ca206c30f8e2b9f691243820
|
||||
Patch9: CVE-2024-24680.patch
|
||||
# https://github.com/django/django/commit/072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521
|
||||
Patch10: CVE-2024-27351.patch
|
||||
Source0: https://files.pythonhosted.org/packages/source/d/Django/Django-%{version}.tar.gz
|
||||
|
||||
BuildArch: noarch
|
||||
%description
|
||||
@ -49,7 +32,7 @@ Provides: python3-Django-doc
|
||||
Development documents and examples for Django
|
||||
|
||||
%prep
|
||||
%autosetup -n django-%{version} -p1
|
||||
%autosetup -n Django-%{version} -p1
|
||||
|
||||
%build
|
||||
%py3_build
|
||||
@ -89,6 +72,19 @@ mv %{buildroot}/doclist.lst .
|
||||
%{_docdir}/*
|
||||
|
||||
%changelog
|
||||
* Thu Aug 08 2024 yaoxin <yao_xin001@hoperun.com> - 4.2.15-1
|
||||
- Update to 4.2.15
|
||||
* CVE-2024-41989: Memory exhaustion in ``django.utils.numberformat.floatformat()``
|
||||
* CVE-2024-41990: Potential denial-of-service vulnerability in ``django.utils.html.urlize()``
|
||||
* CVE-2024-41991: Potential denial-of-service vulnerability in ``django.utils.html.urlize()`` and ``AdminURLFieldWidget``
|
||||
* CVE-2024-42005: Potential SQL injection in ``QuerySet.values()`` and ``values_list()``
|
||||
* Fixed a regression in Django 4.2.14 that caused a crash in ``LocaleMiddleware`` when processing a language code over 500 characters
|
||||
* CVE-2024-38875: Potential denial-of-service vulnerability in django.utils.html.urlize()
|
||||
* CVE-2024-39329: Username enumeration through timing difference for users with unusable passwords
|
||||
* CVE-2024-39330: Potential directory-traversal via Storage.save()
|
||||
* CVE-2024-39614: Potential denial-of-service vulnerability in get_supported_language_variant()
|
||||
* Fixed a crash in Django 4.2 when validating email max line lengths with content decoded using the surrogateescape error handling scheme
|
||||
|
||||
* Tue Mar 05 2024 yaoxin <yao_xin001@hoperun.com> - 3.2.12-10
|
||||
- Fix CVE-2024-27351
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user