diff --git a/CVE-2024-38875.patch b/CVE-2024-38875.patch new file mode 100644 index 0000000..03afd64 --- /dev/null +++ b/CVE-2024-38875.patch @@ -0,0 +1,164 @@ +From 79f368764295df109a37192f6182fb6f361d85b5 Mon Sep 17 00:00:00 2001 +From: Adam Johnson +Date: Mon, 24 Jun 2024 15:30:59 +0200 +Subject: [PATCH] [4.2.x] Fixed CVE-2024-38875 -- Mitigated potential DoS in + urlize and urlizetrunc template filters. +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Thank you to Elias Myllymäki for the report. + +Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> +--- + django/utils/html.py | 90 +++++++++++++++++++++++++--------- + tests/utils_tests/test_html.py | 7 +++ + 2 files changed, 73 insertions(+), 24 deletions(-) + +diff --git a/django/utils/html.py b/django/utils/html.py +index fdb88d6709..fd313ff9ca 100644 +--- a/django/utils/html.py ++++ b/django/utils/html.py +@@ -7,7 +7,7 @@ from html.parser import HTMLParser + from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit + + from django.utils.encoding import punycode +-from django.utils.functional import Promise, keep_lazy, keep_lazy_text ++from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text + from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS + from django.utils.regex_helper import _lazy_re_compile + from django.utils.safestring import SafeData, SafeString, mark_safe +@@ -225,6 +225,16 @@ def smart_urlquote(url): + return urlunsplit((scheme, netloc, path, query, fragment)) + + ++class CountsDict(dict): ++ def __init__(self, *args, word, **kwargs): ++ super().__init__(*args, *kwargs) ++ self.word = word ++ ++ def __missing__(self, key): ++ self[key] = self.word.count(key) ++ return self[key] ++ ++ + class Urlizer: + """ + Convert any URLs in text into clickable links. +@@ -330,40 +340,72 @@ class Urlizer: + return x + return "%s…" % x[: max(0, limit - 1)] + ++ @cached_property ++ def wrapping_punctuation_openings(self): ++ return "".join(dict(self.wrapping_punctuation).keys()) ++ ++ @cached_property ++ def trailing_punctuation_chars_no_semicolon(self): ++ return self.trailing_punctuation_chars.replace(";", "") ++ ++ @cached_property ++ def trailing_punctuation_chars_has_semicolon(self): ++ return ";" in self.trailing_punctuation_chars ++ + def trim_punctuation(self, word): + """ + Trim trailing and wrapping punctuation from `word`. Return the items of + the new state. + """ +- lead, middle, trail = "", word, "" ++ # Strip all opening wrapping punctuation. ++ middle = word.lstrip(self.wrapping_punctuation_openings) ++ lead = word[: len(word) - len(middle)] ++ trail = "" ++ + # Continue trimming until middle remains unchanged. + trimmed_something = True +- while trimmed_something: ++ counts = CountsDict(word=middle) ++ while trimmed_something and middle: + trimmed_something = False + # Trim wrapping punctuation. + for opening, closing in self.wrapping_punctuation: +- if middle.startswith(opening): +- middle = middle[len(opening) :] +- lead += opening +- trimmed_something = True +- # Keep parentheses at the end only if they're balanced. +- if ( +- middle.endswith(closing) +- and middle.count(closing) == middle.count(opening) + 1 +- ): +- middle = middle[: -len(closing)] +- trail = closing + trail +- trimmed_something = True +- # Trim trailing punctuation (after trimming wrapping punctuation, +- # as encoded entities contain ';'). Unescape entities to avoid +- # breaking them by removing ';'. +- middle_unescaped = html.unescape(middle) +- stripped = middle_unescaped.rstrip(self.trailing_punctuation_chars) +- if middle_unescaped != stripped: +- punctuation_count = len(middle_unescaped) - len(stripped) +- trail = middle[-punctuation_count:] + trail +- middle = middle[:-punctuation_count] ++ if counts[opening] < counts[closing]: ++ rstripped = middle.rstrip(closing) ++ if rstripped != middle: ++ strip = counts[closing] - counts[opening] ++ trail = middle[-strip:] ++ middle = middle[:-strip] ++ trimmed_something = True ++ counts[closing] -= strip ++ ++ rstripped = middle.rstrip(self.trailing_punctuation_chars_no_semicolon) ++ if rstripped != middle: ++ trail = middle[len(rstripped) :] + trail ++ middle = rstripped + trimmed_something = True ++ ++ if self.trailing_punctuation_chars_has_semicolon and middle.endswith(";"): ++ # Only strip if not part of an HTML entity. ++ amp = middle.rfind("&") ++ if amp == -1: ++ can_strip = True ++ else: ++ potential_entity = middle[amp:] ++ escaped = html.unescape(potential_entity) ++ can_strip = (escaped == potential_entity) or escaped.endswith(";") ++ ++ if can_strip: ++ rstripped = middle.rstrip(";") ++ amount_stripped = len(middle) - len(rstripped) ++ if amp > -1 and amount_stripped > 1: ++ # Leave a trailing semicolon as might be an entity. ++ trail = middle[len(rstripped) + 1 :] + trail ++ middle = rstripped + ";" ++ else: ++ trail = middle[len(rstripped) :] + trail ++ middle = rstripped ++ trimmed_something = True ++ + return lead, middle, trail + + @staticmethod +diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py +index b7a7396075..6dab41634a 100644 +--- a/tests/utils_tests/test_html.py ++++ b/tests/utils_tests/test_html.py +@@ -342,6 +342,13 @@ class TestUtilsHtml(SimpleTestCase): + "foo@.example.com", + "foo@localhost", + "foo@localhost.", ++ # trim_punctuation catastrophic tests ++ "(" * 100_000 + ":" + ")" * 100_000, ++ "(" * 100_000 + "&:" + ")" * 100_000, ++ "([" * 100_000 + ":" + "])" * 100_000, ++ "[(" * 100_000 + ":" + ")]" * 100_000, ++ "([[" * 100_000 + ":" + "]])" * 100_000, ++ "&:" + ";" * 100_000, + ) + for value in tests: + with self.subTest(value=value): +-- +2.45.2 + diff --git a/CVE-2024-39329.patch b/CVE-2024-39329.patch new file mode 100644 index 0000000..8ca828d --- /dev/null +++ b/CVE-2024-39329.patch @@ -0,0 +1,87 @@ +From 156d3186c96e3ec2ca73b8b25dc2ef366e38df14 Mon Sep 17 00:00:00 2001 +From: Michael Manfre +Date: Fri, 14 Jun 2024 22:12:58 -0400 +Subject: [PATCH] [4.2.x] Fixed CVE-2024-39329 -- Standarized timing of + verify_password() when checking unusuable passwords. + +Refs #20760. + +Thanks Michael Manfre for the fix and to Adam Johnson for the review. +--- + django/contrib/auth/hashers.py | 10 ++++++++-- + tests/auth_tests/test_hashers.py | 32 ++++++++++++++++++++++++++++++++ + 2 files changed, 40 insertions(+), 2 deletions(-) + +diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py +index 9db5a12e13..f11bc8a0f3 100644 +--- a/django/contrib/auth/hashers.py ++++ b/django/contrib/auth/hashers.py +@@ -43,14 +43,20 @@ def check_password(password, encoded, setter=None, preferred="default"): + If setter is specified, it'll be called when you need to + regenerate the password. + """ +- if password is None or not is_password_usable(encoded): +- return False ++ fake_runtime = password is None or not is_password_usable(encoded) + + preferred = get_hasher(preferred) + try: + hasher = identify_hasher(encoded) + except ValueError: + # encoded is gibberish or uses a hasher that's no longer installed. ++ fake_runtime = True ++ ++ if fake_runtime: ++ # Run the default password hasher once to reduce the timing difference ++ # between an existing user with an unusable password and a nonexistent ++ # user or missing hasher (similar to #20760). ++ make_password(get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)) + return False + + hasher_changed = hasher.algorithm != preferred.algorithm +diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py +index 36f22d5f09..3da495f2be 100644 +--- a/tests/auth_tests/test_hashers.py ++++ b/tests/auth_tests/test_hashers.py +@@ -613,6 +613,38 @@ class TestUtilsHashPass(SimpleTestCase): + check_password("wrong_password", encoded) + self.assertEqual(hasher.harden_runtime.call_count, 1) + ++ def test_check_password_calls_make_password_to_fake_runtime(self): ++ hasher = get_hasher("default") ++ cases = [ ++ (None, None, None), # no plain text password provided ++ ("foo", make_password(password=None), None), # unusable encoded ++ ("letmein", make_password(password="letmein"), ValueError), # valid encoded ++ ] ++ for password, encoded, hasher_side_effect in cases: ++ with ( ++ self.subTest(encoded=encoded), ++ mock.patch( ++ "django.contrib.auth.hashers.identify_hasher", ++ side_effect=hasher_side_effect, ++ ) as mock_identify_hasher, ++ mock.patch( ++ "django.contrib.auth.hashers.make_password" ++ ) as mock_make_password, ++ mock.patch( ++ "django.contrib.auth.hashers.get_random_string", ++ side_effect=lambda size: "x" * size, ++ ), ++ mock.patch.object(hasher, "verify"), ++ ): ++ # Ensure make_password is called to standardize timing. ++ check_password(password, encoded) ++ self.assertEqual(hasher.verify.call_count, 0) ++ self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)]) ++ self.assertEqual( ++ mock_make_password.mock_calls, ++ [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)], ++ ) ++ + def test_encode_invalid_salt(self): + hasher_classes = [ + MD5PasswordHasher, +-- +2.45.2 + diff --git a/CVE-2024-39330.patch b/CVE-2024-39330.patch new file mode 100644 index 0000000..fc9ba25 --- /dev/null +++ b/CVE-2024-39330.patch @@ -0,0 +1,180 @@ +From 2b00edc0151a660d1eb86da4059904a0fc4e095e Mon Sep 17 00:00:00 2001 +From: Natalia <124304+nessita@users.noreply.github.com> +Date: Wed, 20 Mar 2024 13:55:21 -0300 +Subject: [PATCH] [4.2.x] Fixed CVE-2024-39330 -- Added extra file name + validation in Storage's save method. + +Thanks to Josh Schneier for the report, and to Carlton Gibson and Sarah +Boyce for the reviews. +--- + django/core/files/storage/base.py | 11 +++++ + django/core/files/utils.py | 7 ++-- + tests/file_storage/test_base.py | 70 +++++++++++++++++++++++++++++++ + tests/file_storage/tests.py | 11 ++--- + tests/file_uploads/tests.py | 2 +- + 5 files changed, 88 insertions(+), 13 deletions(-) + create mode 100644 tests/file_storage/test_base.py + +diff --git a/django/core/files/storage/base.py b/django/core/files/storage/base.py +index 16ac22f70a..03a1b44edb 100644 +--- a/django/core/files/storage/base.py ++++ b/django/core/files/storage/base.py +@@ -34,7 +34,18 @@ class Storage: + if not hasattr(content, "chunks"): + content = File(content, name) + ++ # Ensure that the name is valid, before and after having the storage ++ # system potentially modifying the name. This duplicates the check made ++ # inside `get_available_name` but it's necessary for those cases where ++ # `get_available_name` is overriden and validation is lost. ++ validate_file_name(name, allow_relative_path=True) ++ ++ # Potentially find a different name depending on storage constraints. + name = self.get_available_name(name, max_length=max_length) ++ # Validate the (potentially) new name. ++ validate_file_name(name, allow_relative_path=True) ++ ++ # The save operation should return the actual name of the file saved. + name = self._save(name, content) + # Ensure that the name returned from the storage system is still valid. + validate_file_name(name, allow_relative_path=True) +diff --git a/django/core/files/utils.py b/django/core/files/utils.py +index 85342b2f3f..11e4f07724 100644 +--- a/django/core/files/utils.py ++++ b/django/core/files/utils.py +@@ -10,10 +10,9 @@ def validate_file_name(name, allow_relative_path=False): + raise SuspiciousFileOperation("Could not derive file name from '%s'" % name) + + if allow_relative_path: +- # Use PurePosixPath() because this branch is checked only in +- # FileField.generate_filename() where all file paths are expected to be +- # Unix style (with forward slashes). +- path = pathlib.PurePosixPath(name) ++ # Ensure that name can be treated as a pure posix path, i.e. Unix ++ # style (with forward slashes). ++ path = pathlib.PurePosixPath(str(name).replace("\\", "/")) + if path.is_absolute() or ".." in path.parts: + raise SuspiciousFileOperation( + "Detected path traversal attempt in '%s'" % name +diff --git a/tests/file_storage/test_base.py b/tests/file_storage/test_base.py +new file mode 100644 +index 0000000000..c5338b8e66 +--- /dev/null ++++ b/tests/file_storage/test_base.py +@@ -0,0 +1,70 @@ ++import os ++from unittest import mock ++ ++from django.core.exceptions import SuspiciousFileOperation ++from django.core.files.storage import Storage ++from django.test import SimpleTestCase ++ ++ ++class CustomStorage(Storage): ++ """Simple Storage subclass implementing the bare minimum for testing.""" ++ ++ def exists(self, name): ++ return False ++ ++ def _save(self, name): ++ return name ++ ++ ++class StorageValidateFileNameTests(SimpleTestCase): ++ invalid_file_names = [ ++ os.path.join("path", "to", os.pardir, "test.file"), ++ os.path.join(os.path.sep, "path", "to", "test.file"), ++ ] ++ error_msg = "Detected path traversal attempt in '%s'" ++ ++ def test_validate_before_get_available_name(self): ++ s = CustomStorage() ++ # The initial name passed to `save` is not valid nor safe, fail early. ++ for name in self.invalid_file_names: ++ with ( ++ self.subTest(name=name), ++ mock.patch.object(s, "get_available_name") as mock_get_available_name, ++ mock.patch.object(s, "_save") as mock_internal_save, ++ ): ++ with self.assertRaisesMessage( ++ SuspiciousFileOperation, self.error_msg % name ++ ): ++ s.save(name, content="irrelevant") ++ self.assertEqual(mock_get_available_name.mock_calls, []) ++ self.assertEqual(mock_internal_save.mock_calls, []) ++ ++ def test_validate_after_get_available_name(self): ++ s = CustomStorage() ++ # The initial name passed to `save` is valid and safe, but the returned ++ # name from `get_available_name` is not. ++ for name in self.invalid_file_names: ++ with ( ++ self.subTest(name=name), ++ mock.patch.object(s, "get_available_name", return_value=name), ++ mock.patch.object(s, "_save") as mock_internal_save, ++ ): ++ with self.assertRaisesMessage( ++ SuspiciousFileOperation, self.error_msg % name ++ ): ++ s.save("valid-file-name.txt", content="irrelevant") ++ self.assertEqual(mock_internal_save.mock_calls, []) ++ ++ def test_validate_after_internal_save(self): ++ s = CustomStorage() ++ # The initial name passed to `save` is valid and safe, but the result ++ # from `_save` is not (this is achieved by monkeypatching _save). ++ for name in self.invalid_file_names: ++ with ( ++ self.subTest(name=name), ++ mock.patch.object(s, "_save", return_value=name), ++ ): ++ with self.assertRaisesMessage( ++ SuspiciousFileOperation, self.error_msg % name ++ ): ++ s.save("valid-file-name.txt", content="irrelevant") +diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py +index 7fb57fbce4..44bea8c180 100644 +--- a/tests/file_storage/tests.py ++++ b/tests/file_storage/tests.py +@@ -342,22 +342,17 @@ class FileStorageTests(SimpleTestCase): + + self.storage.delete("path/to/test.file") + +- def test_file_save_abs_path(self): +- test_name = "path/to/test.file" +- f = ContentFile("file saved with path") +- f_name = self.storage.save(os.path.join(self.temp_dir, test_name), f) +- self.assertEqual(f_name, test_name) +- + @unittest.skipUnless( + symlinks_supported(), "Must be able to symlink to run this test." + ) + def test_file_save_broken_symlink(self): + """A new path is created on save when a broken symlink is supplied.""" + nonexistent_file_path = os.path.join(self.temp_dir, "nonexistent.txt") +- broken_symlink_path = os.path.join(self.temp_dir, "symlink.txt") ++ broken_symlink_file_name = "symlink.txt" ++ broken_symlink_path = os.path.join(self.temp_dir, broken_symlink_file_name) + os.symlink(nonexistent_file_path, broken_symlink_path) + f = ContentFile("some content") +- f_name = self.storage.save(broken_symlink_path, f) ++ f_name = self.storage.save(broken_symlink_file_name, f) + self.assertIs(os.path.exists(os.path.join(self.temp_dir, f_name)), True) + + def test_save_doesnt_close(self): +diff --git a/tests/file_uploads/tests.py b/tests/file_uploads/tests.py +index 693efc4c62..24c703a309 100644 +--- a/tests/file_uploads/tests.py ++++ b/tests/file_uploads/tests.py +@@ -826,7 +826,7 @@ class DirectoryCreationTests(SimpleTestCase): + default_storage.delete(UPLOAD_TO) + # Create a file with the upload directory name + with SimpleUploadedFile(UPLOAD_TO, b"x") as file: +- default_storage.save(UPLOAD_TO, file) ++ default_storage.save(UPLOAD_FOLDER, file) + self.addCleanup(default_storage.delete, UPLOAD_TO) + msg = "%s exists and is not a directory." % UPLOAD_TO + with self.assertRaisesMessage(FileExistsError, msg): +-- +2.45.2 + diff --git a/CVE-2024-39614.patch b/CVE-2024-39614.patch new file mode 100644 index 0000000..0832eeb --- /dev/null +++ b/CVE-2024-39614.patch @@ -0,0 +1,135 @@ +From 17358fb35fb7217423d4c4877ccb6d1a3a40b1c3 Mon Sep 17 00:00:00 2001 +From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> +Date: Wed, 26 Jun 2024 12:11:54 +0200 +Subject: [PATCH] [4.2.x] Fixed CVE-2024-39614 -- Mitigated potential DoS in + get_supported_language_variant(). + +Language codes are now parsed with a maximum length limit of 500 chars. + +Thanks to MProgrammer for the report. +--- + django/utils/translation/trans_real.py | 25 ++++++++++++++++++++----- + docs/ref/utils.txt | 10 ++++++++++ + tests/i18n/tests.py | 11 +++++++++++ + 3 files changed, 41 insertions(+), 5 deletions(-) + +diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py +index 872c80b00f..ce13d1e69c 100644 +--- a/django/utils/translation/trans_real.py ++++ b/django/utils/translation/trans_real.py +@@ -31,9 +31,10 @@ _default = None + 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 ++# header or cookie to prevent possible denial of service or memory exhaustion ++# attacks. About 10x longer than the longest value shown on MDN’s ++# Accept-Language page. ++LANGUAGE_CODE_MAX_LENGTH = 500 + + # Format of Accept-Language header values. From RFC 9110 Sections 12.4.2 and + # 12.5.4, and RFC 5646 Section 2.1. +@@ -497,11 +498,25 @@ def get_supported_language_variant(lang_code, strict=False): + If `strict` is False (the default), look for a country-specific variant + when neither the language code nor its generic variant is found. + ++ The language code is truncated to a maximum length to avoid potential ++ denial of service attacks. ++ + lru_cache should have a maxsize to prevent from memory exhaustion attacks, + as the provided language codes are taken from the HTTP request. See also + . + """ + if lang_code: ++ # Truncate the language code to a maximum length to avoid potential ++ # denial of service attacks. ++ if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH: ++ if ( ++ not strict ++ and (index := lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0 ++ ): ++ # There is a generic variant under the maximum length accepted length. ++ lang_code = lang_code[:index] ++ else: ++ raise ValueError("'lang_code' exceeds the maximum accepted length") + # If 'zh-hant-tw' is not supported, try special fallback or subsequent + # language codes i.e. 'zh-hant' and 'zh'. + possible_lang_codes = [lang_code] +@@ -625,13 +640,13 @@ def parse_accept_lang_header(lang_string): + 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: ++ if len(lang_string) <= LANGUAGE_CODE_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. +- if (index := lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)) > 0: ++ if (index := lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0: + return _parse_accept_lang_header(lang_string[:index]) + + # Don't attempt to parse if there is only one language-range value which is +diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt +index b2b826684d..471a4b31eb 100644 +--- a/docs/ref/utils.txt ++++ b/docs/ref/utils.txt +@@ -1155,6 +1155,11 @@ For a complete discussion on the usage of the following see the + ``lang_code`` is ``'es-ar'`` and ``'es'`` is in :setting:`LANGUAGES` but + ``'es-ar'`` isn't. + ++ ``lang_code`` has a maximum accepted length of 500 characters. A ++ :exc:`ValueError` is raised if ``lang_code`` exceeds this limit and ++ ``strict`` is ``True``, or if there is no generic variant and ``strict`` ++ is ``False``. ++ + If ``strict`` is ``False`` (the default), a country-specific variant may + be returned when neither the language code nor its generic variant is found. + For example, if only ``'es-co'`` is in :setting:`LANGUAGES`, that's +@@ -1163,6 +1168,11 @@ For a complete discussion on the usage of the following see the + + Raises :exc:`LookupError` if nothing is found. + ++ .. versionchanged:: 4.2.14 ++ ++ In older versions, ``lang_code`` values over 500 characters were ++ processed without raising a :exc:`ValueError`. ++ + .. function:: to_locale(language) + + Turns a language name (en-us) into a locale name (en_US). +diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py +index f517c5eac7..9b029d5992 100644 +--- a/tests/i18n/tests.py ++++ b/tests/i18n/tests.py +@@ -65,6 +65,7 @@ from django.utils.translation.reloader import ( + translation_file_changed, + watch_for_translation_changes, + ) ++from django.utils.translation.trans_real import LANGUAGE_CODE_MAX_LENGTH + + from .forms import CompanyForm, I18nForm, SelectDateForm + from .models import Company, TestModel +@@ -1888,6 +1889,16 @@ class MiscTests(SimpleTestCase): + g("xyz") + with self.assertRaises(LookupError): + g("xy-zz") ++ msg = "'lang_code' exceeds the maximum accepted length" ++ with self.assertRaises(LookupError): ++ g("x" * LANGUAGE_CODE_MAX_LENGTH) ++ with self.assertRaisesMessage(ValueError, msg): ++ g("x" * (LANGUAGE_CODE_MAX_LENGTH + 1)) ++ # 167 * 3 = 501 which is LANGUAGE_CODE_MAX_LENGTH + 1. ++ self.assertEqual(g("en-" * 167), "en") ++ with self.assertRaisesMessage(ValueError, msg): ++ g("en-" * 167, strict=True) ++ self.assertEqual(g("en-" * 30000), "en") # catastrophic test + + def test_get_supported_language_variant_null(self): + g = trans_null.get_supported_language_variant +-- +2.45.2 + diff --git a/python-Django.changes b/python-Django.changes index 4ff73c9..e6c9ac4 100644 --- a/python-Django.changes +++ b/python-Django.changes @@ -1,3 +1,19 @@ +------------------------------------------------------------------- +Fri Jul 12 13:41:03 UTC 2024 - Nico Krapp + +- Add CVE-2024-38875.patch (bsc#1227590) + * CVE-2024-38875: Potential denial-of-service attack via + certain inputs with a very large number of brackets +- Add CVE-2024-39329.patch (bsc#1227593) + * CVE-2024-39329: Username enumeration through timing difference + for users with unusable passwords +- Add CVE-2024-39330.patch (bsc#1227594) + * CVE-2024-39330: Potential directory traversal in + django.core.files.storage.Storage.save() +- Add CVE-2024-39614.patch (bsc#1227595) + * CVE-2024-39614: Potential denial-of-service through + django.utils.translation.get_supported_language_variant() + ------------------------------------------------------------------- Thu Feb 29 13:19:00 UTC 2024 - Alberto Planas Dominguez diff --git a/python-Django.spec b/python-Django.spec index f7d63bb..be4e261 100644 --- a/python-Django.spec +++ b/python-Django.spec @@ -37,6 +37,14 @@ Source99: python-Django-rpmlintrc Patch: sanitize_address.patch # PATCH-FIX-UPSTREAM CVE-2024-27351.patch bsc#1220358 Patch1: CVE-2024-27351.patch +# PATCH-FIX-UPSTREAM CVE-2024-38875.patch bsc#1227590 +Patch2: CVE-2024-38875.patch +# PATCH-FIX-UPSTREAM CVE-2024-39329.patch bsc#1227593 +Patch3: CVE-2024-39329.patch +# PATCH-FIX-UPSTREAM CVE-2024-39330.patch bsc#1227594 +Patch4: CVE-2024-39330.patch +# PATCH-FIX-UPSTREAM CVE-2024-39614.patch bsc#1227595 +Patch5: CVE-2024-39614.patch BuildRequires: %{python_module Jinja2 >= 2.9.2} BuildRequires: %{python_module Pillow >= 6.2.0} BuildRequires: %{python_module PyYAML}