diff --git a/CVE-2022-34265.patch b/CVE-2022-34265.patch deleted file mode 100644 index 3d4f452e08f999e126286f2217083e5c68fc71fa..0000000000000000000000000000000000000000 --- 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 13dc1bb1428f1fb3a2f59e5cd899821762a05c56..0000000000000000000000000000000000000000 --- 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 9f23a5499be925bf9981a635994e041bdd10e554..0000000000000000000000000000000000000000 --- 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 bdcf26b8987a65ef9e6052269892893840c09bfa..0000000000000000000000000000000000000000 --- 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 e0dc2aee91b335e499eb897dfb82475df68c00ac..0000000000000000000000000000000000000000 --- 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): - - - --
  • -+
  • -
    • This field is required.
    -
  • """ - ) -@@ -2840,7 +2847,7 @@ class Person(Form): - - -

    --

    -+

    -
    • This field is required.
    -

    -

    """ -@@ -2859,7 +2866,7 @@ class Person(Form): - - - -- -+ - -
    • This field is required.
    - """ -@@ -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:

    -
    This field is required.
    -

    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 ad4bd06c8699a093321834ef337f6cbfae1dd30e..0000000000000000000000000000000000000000 --- 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 523934e792255f63dbf6fd4ae0e75fc8010e4110..0000000000000000000000000000000000000000 --- 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 30f6c6ff13aabe3bfd23b5d3baea2b7652aac0e3..0000000000000000000000000000000000000000 --- 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 3d8ab4dcf517deafb344c24af51b17eba0d159cf..0000000000000000000000000000000000000000 --- 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 606f75529c7e55207151494400c465390f714941..0000000000000000000000000000000000000000 --- 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 Binary files a/3.2.12.tar.gz and b/Django-4.2.15.tar.gz differ diff --git a/backport-CVE-2022-36359.patch b/backport-CVE-2022-36359.patch deleted file mode 100644 index 97dc3ff6ee85773ee3922c0a9caa77c368d95a43..0000000000000000000000000000000000000000 --- a/backport-CVE-2022-36359.patch +++ /dev/null @@ -1,74 +0,0 @@ -From 8c5a1dfe34ea52cc2af21064a8654bfaa8b7a012 Mon Sep 17 00:00:00 2001 -From: Carlton Gibson -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 - diff --git a/python-django.spec b/python-django.spec index 8f6c515fa2bbb3d7057e5eeec2c9e00d4433d19c..aa3ef771213e3d7701f883cc9d31508f9dba6e98 100644 --- a/python-django.spec +++ b/python-django.spec @@ -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 - 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 - 3.2.12-10 - Fix CVE-2024-27351