From 26a8ae419d664dc50e6de17d34bb9f24e462d248 Mon Sep 17 00:00:00 2001
From: starlet-dx <15929766099@163.com>
Date: Thu, 8 Aug 2024 14:39:18 +0800
Subject: [PATCH] Update to 4.2.15 for fix cves
(cherry picked from commit a99ad9eb675e702e84ae0e6e5378f4e89272b679)
---
CVE-2022-34265.patch | 109 -------
CVE-2023-23969.patch | 100 -------
CVE-2023-24580.patch | 401 --------------------------
CVE-2023-31047.patch | 322 ---------------------
CVE-2023-36053.patch | 244 ----------------
CVE-2023-41164.patch | 83 ------
CVE-2023-43665.patch | 168 -----------
CVE-2023-46695.patch | 62 ----
CVE-2024-24680.patch | 204 -------------
CVE-2024-27351.patch | 122 --------
3.2.12.tar.gz => Django-4.2.15.tar.gz | Bin 9554725 -> 10418066 bytes
backport-CVE-2022-36359.patch | 74 -----
python-django.spec | 38 ++-
13 files changed, 17 insertions(+), 1910 deletions(-)
delete mode 100644 CVE-2022-34265.patch
delete mode 100644 CVE-2023-23969.patch
delete mode 100644 CVE-2023-24580.patch
delete mode 100644 CVE-2023-31047.patch
delete mode 100644 CVE-2023-36053.patch
delete mode 100644 CVE-2023-41164.patch
delete mode 100644 CVE-2023-43665.patch
delete mode 100644 CVE-2023-46695.patch
delete mode 100644 CVE-2024-24680.patch
delete mode 100644 CVE-2024-27351.patch
rename 3.2.12.tar.gz => Django-4.2.15.tar.gz (53%)
delete mode 100644 backport-CVE-2022-36359.patch
diff --git a/CVE-2022-34265.patch b/CVE-2022-34265.patch
deleted file mode 100644
index 3d4f452..0000000
--- a/CVE-2022-34265.patch
+++ /dev/null
@@ -1,109 +0,0 @@
-From a9010fe5555e6086a9d9ae50069579400ef0685e Mon Sep 17 00:00:00 2001
-From: Mariusz Felisiak
-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)
diff --git a/CVE-2023-23969.patch b/CVE-2023-23969.patch
deleted file mode 100644
index 13dc1bb..0000000
--- a/CVE-2023-23969.patch
+++ /dev/null
@@ -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
-
diff --git a/CVE-2023-24580.patch b/CVE-2023-24580.patch
deleted file mode 100644
index 9f23a54..0000000
--- a/CVE-2023-24580.patch
+++ /dev/null
@@ -1,401 +0,0 @@
-From a665ed5179f5bbd3db95ce67286d0192eff041d8 Mon Sep 17 00:00:00 2001
-From: Markus Holtermann
-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 ` 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', '']))
diff --git a/CVE-2023-31047.patch b/CVE-2023-31047.patch
deleted file mode 100644
index bdcf26b..0000000
--- a/CVE-2023-31047.patch
+++ /dev/null
@@ -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
-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
-
diff --git a/CVE-2023-36053.patch b/CVE-2023-36053.patch
deleted file mode 100644
index e0dc2ae..0000000
--- a/CVE-2023-36053.patch
+++ /dev/null
@@ -1,244 +0,0 @@
-From 454f2fb93437f98917283336201b4048293f7582 Mon Sep 17 00:00:00 2001
-From: Mariusz Felisiak
-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, ' ')
-+ self.assertEqual(f.max_length, 320)
-+ self.assertWidgetRendersTo(
-+ f, ' '
-+ )
- 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']), ' ')
-+ self.assertHTMLEqual(
-+ str(f["email"]),
-+ ' ',
-+ )
- self.assertHTMLEqual(str(f['get_spam']), ' ')
-
- f = SignupForm({'email': 'test@example.com', 'get_spam': True}, auto_id=False)
-- self.assertHTMLEqual(str(f['email']), ' ')
-+ self.assertHTMLEqual(
-+ str(f["email"]),
-+ ' ",
-+ )
- self.assertHTMLEqual(
- str(f['get_spam']),
- ' ',
-@@ -2824,7 +2831,7 @@ class Person(Form):
- Yes
- No
-
--Email:
-+Email:
-
- Age: """
- )
-@@ -2840,7 +2847,7 @@ class Person(Form):
- Yes
- No
-
--Email:
-+Email:
-
- Age:
-
"""
-@@ -2859,7 +2866,7 @@ class Person(Form):
- No
-
- Email:
--
-+
- Age:
-
- """
-@@ -3489,7 +3496,7 @@ class CommentForm(Form):
- f = CommentForm(data, auto_id=False, error_class=DivErrorList)
- self.assertHTMLEqual(f.as_p(), """Name:
- Enter a valid email address.
--Email:
-+Email:
-
- Comment:
""")
-
-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),
diff --git a/CVE-2023-41164.patch b/CVE-2023-41164.patch
deleted file mode 100644
index ad4bd06..0000000
--- a/CVE-2023-41164.patch
+++ /dev/null
@@ -1,83 +0,0 @@
-From 6f030b1149bd8fa4ba90452e77cb3edc095ce54e Mon Sep 17 00:00:00 2001
-From: Mariusz Felisiak
-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):
-
diff --git a/CVE-2023-43665.patch b/CVE-2023-43665.patch
deleted file mode 100644
index 523934e..0000000
--- a/CVE-2023-43665.patch
+++ /dev/null
@@ -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 ``"Joel is a slug
"``, 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 ``"Joel is a slug
"``, 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 = "Joel is a slug
" # 14 chars
- perf_test_values = [
-- (('', None),
-- ('&' * 50000, '&' * 9 + '…'),
-- ('_X<<<<<<<<<<<>', None),
-+ ("", None),
-+ ("", "", None),
-+ (valid_html * bigger_len, "Joel is a…
"), # 10 chars
- ]
- for value, expected in perf_test_values:
- with self.subTest(value=value):
-@@ -152,15 +159,25 @@ class TestUtilsText(SimpleTestCase):
- truncator = text.Truncator('I <3 python, what about you?
')
- self.assertEqual('I <3 python,…
', 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 = "Joel is a slug
" # 4 words
- perf_test_values = [
-- ('',
-- '&' * 50000,
-- '_X<<<<<<<<<<<>',
-+ ("", None),
-+ ("", "", None),
-+ (valid_html * bigger_len, valid_html * 12 + "Joel is…
"), # 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
-
diff --git a/CVE-2023-46695.patch b/CVE-2023-46695.patch
deleted file mode 100644
index 30f6c6f..0000000
--- a/CVE-2023-46695.patch
+++ /dev/null
@@ -1,62 +0,0 @@
-From f9a7fb8466a7ba4857eaf930099b5258f3eafb2b Mon Sep 17 00:00:00 2001
-From: Mariusz Felisiak
-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
-
diff --git a/CVE-2024-24680.patch b/CVE-2024-24680.patch
deleted file mode 100644
index 3d8ab4d..0000000
--- a/CVE-2024-24680.patch
+++ /dev/null
@@ -1,204 +0,0 @@
-From c1171ffbd570db90ca206c30f8e2b9f691243820 Mon Sep 17 00:00:00 2001
-From: Adam Johnson
-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
-Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
-Co-authored-by: Shai Berger
----
- .../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
-
diff --git a/CVE-2024-27351.patch b/CVE-2024-27351.patch
deleted file mode 100644
index 606f755..0000000
--- a/CVE-2024-27351.patch
+++ /dev/null
@@ -1,122 +0,0 @@
-From 072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521 Mon Sep 17 00:00:00 2001
-From: Shai Berger
-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
----
- 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('I <3 python, what about you?
')
- self.assertEqual('I <3 python,…
', 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(
-+ """Hello, my dear lady! """
-+ )
-+ self.assertEqual(
-+ """Hello, my dear… """,
-+ truncator.words(3, html=True),
-+ )
-+
-+ # Tags with special non-latin chars in attrs.
-+ truncator = text.Truncator("""Hello, my dear lady!
""")
-+ self.assertEqual(
-+ """Hello, my dear…
""",
-+ 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
-
diff --git a/3.2.12.tar.gz b/Django-4.2.15.tar.gz
similarity index 53%
rename from 3.2.12.tar.gz
rename to Django-4.2.15.tar.gz
index 6233704040e9580429c8f4a1a77a5311ed24db39..c77fade850f7089ec82208edd58c16b145e211f3 100644
GIT binary patch
delta 10369630
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