Fix CVE-2024-38875 CVE-2024-39329 CVE-2024-39330 CVE-2024-39614 CVE-2024-41989 CVE-2024-41990 CVE-2024-41991 CVE-2024-42005 CVE-2024-45230 CVE-2024-45231

This commit is contained in:
wk333 2024-10-11 19:52:27 +08:00
parent fc8e6abbbd
commit b92a28148a
11 changed files with 1174 additions and 1 deletions

154
CVE-2024-38875.patch Normal file
View File

@ -0,0 +1,154 @@
From 8623260fb0949d368376a128bee2189ec0a67ae5 Mon Sep 17 00:00:00 2001
From: nkrapp <nico.krapp@suse.com>
Date: Mon, 22 Jul 2024 09:43:08 +0200
Subject: [PATCH] [PATCH] [4.2.x] Fixed CVE-2024-38875 -- Mitigated potential
DoS in urlize and urlizetrunc template filters.
---
django/utils/html.py | 72 +++++++++++++++++++++++++++++-----
tests/utils_tests/test_html.py | 21 ++++++----
2 files changed, 75 insertions(+), 17 deletions(-)
diff --git a/django/utils/html.py b/django/utils/html.py
index 7a33d5f68d..1dbe39ccd1 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -1,5 +1,6 @@
"""HTML utilities suitable for global use."""
+import html
import json
import re
from html.parser import HTMLParser
@@ -235,6 +235,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]
+
+
@keep_lazy_text
def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
"""
@@ -259,6 +269,15 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
return x
return '%s…' % x[:max(0, limit - 1)]
+ def wrapping_punctuation_openings():
+ return "".join(dict(WRAPPING_PUNCTUATION).keys())
+
+ def trailing_punctuation_chars_no_semicolon():
+ return TRAILING_PUNCTUATION_CHARS.replace(";", "")
+
+ def trailing_punctuation_chars_has_semicolon():
+ return ";" in TRAILING_PUNCTUATION_CHARS
+
def unescape(text):
"""
If input URL is HTML-escaped, unescape it so that it can be safely fed
@@ -273,21 +292,53 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
Trim trailing and wrapping punctuation from `middle`. Return the items
of the new state.
"""
+ # Strip all opening wrapping punctuation.
+ middle = word.lstrip(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 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
+ 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(trailing_punctuation_chars_no_semicolon())
+ if rstripped != middle:
+ trail = middle[len(rstripped) :] + trail
+ middle = rstripped
+ trimmed_something = True
+
+ if 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
# Trim trailing punctuation (after trimming wrapping punctuation,
# as encoded entities contain ';'). Unescape entites to avoid
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index 5cc2d9b95d..dc89009b63 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -260,13 +260,20 @@ class TestUtilsHtml(SimpleTestCase):
def test_urlize_unchanged_inputs(self):
tests = (
- ('a' + '@a' * 50000) + 'a', # simple_email_re catastrophic test
- ('a' + '.' * 1000000) + 'a', # trailing_punctuation catastrophic test
- 'foo@',
- '@foo.com',
- 'foo@.example.com',
- 'foo@localhost',
- 'foo@localhost.',
+ ("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
+ ("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
+ "foo@",
+ "@foo.com",
+ "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

77
CVE-2024-39329.patch Normal file
View File

@ -0,0 +1,77 @@
From c676b05c98df58252509dd5ad16b959c351ac770 Mon Sep 17 00:00:00 2001
From: nkrapp <nico.krapp@suse.com>
Date: Fri, 26 Jul 2024 14:01:36 +0200
Subject: [PATCH] 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 | 22 ++++++++++++++++++++++
2 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index 1e8d7547fc..af55e57389 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -36,14 +36,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 ee6441b237..170bd07c4b 100644
--- a/tests/auth_tests/test_hashers.py
+++ b/tests/auth_tests/test_hashers.py
@@ -433,6 +433,28 @@ 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):
+ with mock.patch("django.contrib.auth.hashers.identify_hasher", side_effect=hasher_side_effect) as mock_identify_hasher:
+ with mock.patch("django.contrib.auth.hashers.make_password") as mock_make_password:
+ with mock.patch("django.contrib.auth.hashers.get_random_string", side_effect=lambda size: "x" * size):
+ with 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)],
+ )
+
class BasePasswordHasherTests(SimpleTestCase):
not_implemented_msg = 'subclasses of BasePasswordHasher must provide %s() method'
--
2.45.2

147
CVE-2024-39330.patch Normal file
View File

@ -0,0 +1,147 @@
From 72af4b325aa9ffd96b18ef68d26ec2260e982c2a Mon Sep 17 00:00:00 2001
From: nkrapp <nico.krapp@suse.com>
Date: Wed, 24 Jul 2024 17:08:23 +0200
Subject: [PATCH] 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.py | 11 ++++++
django/core/files/utils.py | 7 ++--
tests/file_storage/test_base.py | 64 +++++++++++++++++++++++++++++++++
tests/file_storage/tests.py | 6 ----
4 files changed, 78 insertions(+), 10 deletions(-)
create mode 100644 tests/file_storage/test_base.py
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
index ea5bbc82d0..8c633ec040 100644
--- a/django/core/files/storage.py
+++ b/django/core/files/storage.py
@@ -50,7 +50,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 f28cea1077..a1fea44ded 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..7a0838f7a5
--- /dev/null
+++ b/tests/file_storage/test_base.py
@@ -0,0 +1,64 @@
+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):
+ with mock.patch.object(s, "get_available_name") as mock_get_available_names:
+ with 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_names.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):
+ with mock.patch.object(s, "get_available_name", return_value=name):
+ with 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):
+ with 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 4c6f6920ed..0e692644b7 100644
--- a/tests/file_storage/tests.py
+++ b/tests/file_storage/tests.py
@@ -291,12 +291,6 @@ 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)
-
def test_save_doesnt_close(self):
with TemporaryUploadedFile('test', 'text/plain', 1, 'utf8') as file:
file.write(b'1')
--
2.45.2

195
CVE-2024-39614.patch Normal file
View File

@ -0,0 +1,195 @@
From 2f128b1865bc43f6cf3583b1255bf1bd8be29e57 Mon Sep 17 00:00:00 2001
From: nkrapp <nico.krapp@suse.com>
Date: Mon, 22 Jul 2024 11:23:29 +0200
Subject: [PATCH] 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 | 29 ++++++++++---
docs/ref/utils.txt | 25 +++++++++++
tests/i18n/tests.py | 59 ++++++++++++++++++++++++++
3 files changed, 107 insertions(+), 6 deletions(-)
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index ecd701f3d8..0a237a5afc 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -30,9 +30,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 MDNs 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 MDNs
+# Accept-Language page.
+LANGUAGE_CODE_MAX_LENGTH = 500
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
# and RFC 3066, section 2.1
@@ -473,12 +474,28 @@ 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
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
"""
if lang_code:
- # If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
+ # Truncate the language code to a maximum length to avoid potential
+ # denial of service attacks.
+ if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
+ index = lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)
+ if (
+ not strict
+ and index > 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]
try:
possible_lang_codes.extend(LANG_INFO[lang_code]['fallback'])
@@ -599,13 +616,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.
- index = lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)
+ index = lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH)
if index > 0:
return _parse_accept_lang_header(lang_string[:index])
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index 390f167ce2..d0a8e8c1f3 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -1150,6 +1150,31 @@ functions without the ``u``.
Raises :exc:`LookupError` if nothing is found.
+.. function:: get_supported_language_variant(lang_code, strict=False)
+
+ Returns ``lang_code`` if it's in the :setting:`LANGUAGES` setting, possibly
+ selecting a more generic variant. For example, ``'es'`` is returned if
+ ``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
+ returned for ``lang_code``\s like ``'es'`` and ``'es-ar'``. Those matches
+ aren't returned if ``strict=True``.
+
+ 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 6efc3a5ae3..3087e5b6a6 100644
--- a/tests/i18n/tests.py
+++ b/tests/i18n/tests.py
@@ -39,6 +39,7 @@ from django.utils.translation import (
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
@@ -1434,6 +1435,64 @@ class MiscTests(SimpleTestCase):
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'zh-hans'}
r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'}
self.assertEqual(g(r), 'zh-hans')
+
+ @override_settings(
+ USE_I18N=True,
+ LANGUAGES=[
+ ("en", "English"),
+ ("ar-dz", "Algerian Arabic"),
+ ("de", "German"),
+ ("de-at", "Austrian German"),
+ ("pt-BR", "Portuguese (Brazil)"),
+ ],
+ )
+ def test_get_supported_language_variant_real(self):
+ g = trans_real.get_supported_language_variant
+ self.assertEqual(g("en"), "en")
+ self.assertEqual(g("en-gb"), "en")
+ self.assertEqual(g("de"), "de")
+ self.assertEqual(g("de-at"), "de-at")
+ self.assertEqual(g("de-ch"), "de")
+ self.assertEqual(g("pt-br"), "pt-br")
+ self.assertEqual(g("pt-BR"), "pt-BR")
+ self.assertEqual(g("pt"), "pt-br")
+ self.assertEqual(g("pt-pt"), "pt-br")
+ self.assertEqual(g("ar-dz"), "ar-dz")
+ self.assertEqual(g("ar-DZ"), "ar-DZ")
+ with self.assertRaises(LookupError):
+ g("pt", strict=True)
+ with self.assertRaises(LookupError):
+ g("pt-pt", strict=True)
+ with self.assertRaises(LookupError):
+ 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
+ self.assertEqual(g(settings.LANGUAGE_CODE), settings.LANGUAGE_CODE)
+ with self.assertRaises(LookupError):
+ g("pt")
+ with self.assertRaises(LookupError):
+ g("de")
+ with self.assertRaises(LookupError):
+ g("de-at")
+ with self.assertRaises(LookupError):
+ g("de", strict=True)
+ with self.assertRaises(LookupError):
+ g("de-at", strict=True)
+ with self.assertRaises(LookupError):
+ g("xyz")
@override_settings(
USE_I18N=True,
--
2.45.2

76
CVE-2024-41989.patch Normal file
View File

@ -0,0 +1,76 @@
From 0521744d21a7854e849336af1e3a3aad44cee017 Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Fri, 12 Jul 2024 11:38:34 +0200
Subject: [PATCH 1/4] [4.2.x] Fixed CVE-2024-41989 -- Prevented excessive
memory consumption in floatformat.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Thanks Elias Myllymäki for the report.
Co-authored-by: Shai Berger <shai@platonix.com>
---
django/template/defaultfilters.py | 13 +++++++++++++
.../filter_tests/test_floatformat.py | 17 +++++++++++++++++
3 files changed, 39 insertions(+)
Index: Django-2.2.28/django/template/defaultfilters.py
===================================================================
--- Django-2.2.28.orig/django/template/defaultfilters.py
+++ Django-2.2.28/django/template/defaultfilters.py
@@ -135,6 +135,19 @@ def floatformat(text, arg=-1):
except ValueError:
return input_val
+ _, digits, exponent = d.as_tuple()
+ try:
+ number_of_digits_and_exponent_sum = len(digits) + abs(exponent)
+ except TypeError:
+ # Exponent values can be "F", "n", "N".
+ number_of_digits_and_exponent_sum = 0
+
+ # Values with more than 200 digits, or with a large exponent, are returned "as is"
+ # to avoid high memory consumption and potential denial-of-service attacks.
+ # The cut-off of 200 is consistent with django.utils.numberformat.floatformat().
+ if number_of_digits_and_exponent_sum > 200:
+ return input_val
+
try:
m = int(d) - d
except (ValueError, OverflowError, InvalidOperation):
Index: Django-2.2.28/tests/template_tests/filter_tests/test_floatformat.py
===================================================================
--- Django-2.2.28.orig/tests/template_tests/filter_tests/test_floatformat.py
+++ Django-2.2.28/tests/template_tests/filter_tests/test_floatformat.py
@@ -55,6 +55,7 @@ class FunctionTests(SimpleTestCase):
self.assertEqual(floatformat(1.5e-15, 20), '0.00000000000000150000')
self.assertEqual(floatformat(1.5e-15, -20), '0.00000000000000150000')
self.assertEqual(floatformat(1.00000000000000015, 16), '1.0000000000000002')
+ self.assertEqual(floatformat("1e199"), "1" + "0" * 199)
def test_zero_values(self):
self.assertEqual(floatformat(0, 6), '0.000000')
@@ -68,6 +69,22 @@ class FunctionTests(SimpleTestCase):
self.assertEqual(floatformat(pos_inf), 'inf')
self.assertEqual(floatformat(neg_inf), '-inf')
self.assertEqual(floatformat(pos_inf / pos_inf), 'nan')
+ self.assertEqual(floatformat("inf"), "inf")
+ self.assertEqual(floatformat("NaN"), "NaN")
+
+ def test_too_many_digits_to_render(self):
+ cases = [
+ "1e200",
+ "1E200",
+ "1E10000000000000000",
+ "-1E10000000000000000",
+ "1e10000000000000000",
+ "-1e10000000000000000",
+ "1" + "0" * 1_000_000,
+ ]
+ for value in cases:
+ with self.subTest(value=value):
+ self.assertEqual(floatformat(value), value)
def test_float_dunder_method(self):
class FloatWrapper:

61
CVE-2024-41990.patch Normal file
View File

@ -0,0 +1,61 @@
From 729d7934e34ff91f262f3e7089e32cab701b09ca Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Thu, 18 Jul 2024 13:19:34 +0200
Subject: [PATCH 2/4] [4.2.x] Fixed CVE-2024-41990 -- Mitigated potential DoS
in urlize and urlizetrunc template filters.
Thanks to MProgrammer for the report.
---
django/utils/html.py | 18 ++++++++----------
tests/utils_tests/test_html.py | 2 ++
3 files changed, 17 insertions(+), 10 deletions(-)
Index: Django-2.2.28/django/utils/html.py
===================================================================
--- Django-2.2.28.orig/django/utils/html.py
+++ Django-2.2.28/django/utils/html.py
@@ -314,7 +314,11 @@ def urlize(text, trim_url_limit=None, no
trimmed_something = True
counts[closing] -= strip
- rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
+ amp = middle.rfind("&")
+ if amp == -1:
+ rstripped = middle.rstrip(TRAILING_PUNCTUATION_CHARS)
+ else:
+ rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
if rstripped != middle:
trail = middle[len(rstripped) :] + trail
middle = rstripped
@@ -322,15 +326,9 @@ def urlize(text, trim_url_limit=None, no
if 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:
+ potential_entity = middle[amp:]
+ escaped = html.unescape(potential_entity)
+ if escaped == potential_entity or escaped.endswith(";"):
rstripped = middle.rstrip(";")
amount_stripped = len(middle) - len(rstripped)
if amp > -1 and amount_stripped > 1:
Index: Django-2.2.28/tests/utils_tests/test_html.py
===================================================================
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
+++ Django-2.2.28/tests/utils_tests/test_html.py
@@ -274,6 +274,8 @@ class TestUtilsHtml(SimpleTestCase):
"[(" * 100_000 + ":" + ")]" * 100_000,
"([[" * 100_000 + ":" + "]])" * 100_000,
"&:" + ";" * 100_000,
+ "&.;" * 100_000,
+ ".;" * 100_000,
)
for value in tests:
with self.subTest(value=value):

98
CVE-2024-41991.patch Normal file
View File

@ -0,0 +1,98 @@
From 772a73f70c3d249c99c23012849e66276b7b0715 Mon Sep 17 00:00:00 2001
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Date: Wed, 10 Jul 2024 20:30:12 +0200
Subject: [PATCH 3/4] [4.2.x] Fixed CVE-2024-41991 -- Prevented potential ReDoS
in django.utils.html.urlize() and AdminURLFieldWidget.
Thanks Seokchan Yoon for the report.
Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
---
django/contrib/admin/widgets.py | 2 +-
django/utils/html.py | 10 ++++++++--
tests/admin_widgets/tests.py | 7 ++++++-
tests/utils_tests/test_html.py | 13 +++++++++++++
5 files changed, 35 insertions(+), 4 deletions(-)
Index: Django-2.2.28/django/contrib/admin/widgets.py
===================================================================
--- Django-2.2.28.orig/django/contrib/admin/widgets.py
+++ Django-2.2.28/django/contrib/admin/widgets.py
@@ -344,7 +344,7 @@ class AdminURLFieldWidget(forms.URLInput
context = super().get_context(name, value, attrs)
context['current_label'] = _('Currently:')
context['change_label'] = _('Change:')
- context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else ''
+ context['widget']['href'] = smart_urlquote(context['widget']['value']) if url_valid else ''
context['url_valid'] = url_valid
return context
Index: Django-2.2.28/django/utils/html.py
===================================================================
--- Django-2.2.28.orig/django/utils/html.py
+++ Django-2.2.28/django/utils/html.py
@@ -33,6 +33,8 @@ _html_escapes = {
ord("'"): '&#39;',
}
+MAX_URL_LENGTH = 2048
+
@keep_lazy(str, SafeText)
def escape(text):
@@ -360,6 +362,10 @@ def urlize(text, trim_url_limit=None, no
except ValueError:
# value contains more than one @.
return False
+ # Max length for domain name labels is 63 characters per RFC 1034.
+ # Helps to avoid ReDoS vectors in the domain part.
+ if len(p2) > 63:
+ return False
# Dot must be in p2 (e.g. example.com)
if '.' not in p2 or p2.startswith('.'):
return False
@@ -378,9 +384,9 @@ def urlize(text, trim_url_limit=None, no
# Make URL we want to point to.
url = None
nofollow_attr = ' rel="nofollow"' if nofollow else ''
- if simple_url_re.match(middle):
+ if len(middle) <= MAX_URL_LENGTH and simple_url_re.match(middle):
url = smart_urlquote(unescape(middle))
- elif simple_url_2_re.match(middle):
+ elif len(middle) <= MAX_URL_LENGTH and simple_url_2_re.match(middle):
url = smart_urlquote('http://%s' % unescape(middle))
elif ':' not in middle and is_email_simple(middle):
local, domain = middle.rsplit('@', 1)
Index: Django-2.2.28/tests/admin_widgets/tests.py
===================================================================
--- Django-2.2.28.orig/tests/admin_widgets/tests.py
+++ Django-2.2.28/tests/admin_widgets/tests.py
@@ -336,7 +336,12 @@ class AdminSplitDateTimeWidgetTest(Simpl
class AdminURLWidgetTest(SimpleTestCase):
def test_get_context_validates_url(self):
w = widgets.AdminURLFieldWidget()
- for invalid in ['', '/not/a/full/url/', 'javascript:alert("Danger XSS!")']:
+ for invalid in [
+ "",
+ "/not/a/full/url/",
+ 'javascript:alert("Danger XSS!")',
+ "http://" + "한.글." * 1_000_000 + "com",
+ ]:
with self.subTest(url=invalid):
self.assertFalse(w.get_context('name', invalid, {})['url_valid'])
self.assertTrue(w.get_context('name', 'http://example.com', {})['url_valid'])
Index: Django-2.2.28/tests/utils_tests/test_html.py
===================================================================
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
+++ Django-2.2.28/tests/utils_tests/test_html.py
@@ -261,6 +261,10 @@ class TestUtilsHtml(SimpleTestCase):
def test_urlize_unchanged_inputs(self):
tests = (
("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
+ # Unicode domain catastrophic tests.
+ "a@" + "한.글." * 1_000_000 + "a",
+ "http://" + "한.글." * 1_000_000 + "com",
+ "www." + "한.글." * 1_000_000 + "com",
("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
"foo@",
"@foo.com",

84
CVE-2024-42005.patch Normal file
View File

@ -0,0 +1,84 @@
From b6de28f897709ee5d94ca2da21bcc98f9dade01c Mon Sep 17 00:00:00 2001
From: Simon Charette <charette.s@gmail.com>
Date: Thu, 25 Jul 2024 18:19:13 +0200
Subject: [PATCH 4/4] [4.2.x] Fixed CVE-2024-42005 -- Mitigated
QuerySet.values() SQL injection attacks against JSON fields.
Thanks Eyal (eyalgabay) for the report.
---
django/db/models/sql/query.py | 2 ++
tests/expressions/models.py | 7 +++++++
tests/expressions/test_queryset_values.py | 17 +++++++++++++++--
4 files changed, 31 insertions(+), 2 deletions(-)
Index: Django-2.0.7/django/db/models/sql/query.py
===================================================================
--- Django-2.0.7.orig/django/db/models/sql/query.py
+++ Django-2.0.7/django/db/models/sql/query.py
@@ -1924,6 +1924,8 @@ class Query:
self.clear_select_fields()
if fields:
+ for field in fields:
+ self.check_alias(field)
field_names = []
extra_names = []
annotation_names = []
Index: Django-2.0.7/tests/expressions/models.py
===================================================================
--- Django-2.0.7.orig/tests/expressions/models.py
+++ Django-2.0.7/tests/expressions/models.py
@@ -4,6 +4,7 @@ Tests for F() query expression syntax.
import uuid
from django.db import models
+from django.contrib.postgres.fields import JSONField
class Employee(models.Model):
@@ -91,3 +92,10 @@ class UUID(models.Model):
def __str__(self):
return "%s" % self.uuid
+
+
+class JSONFieldModel(models.Model):
+ data = JSONField(null=True)
+
+ class Meta:
+ required_db_features = {"supports_json_field"}
Index: Django-2.0.7/tests/expressions/test_queryset_values.py
===================================================================
--- Django-2.0.7.orig/tests/expressions/test_queryset_values.py
+++ Django-2.0.7/tests/expressions/test_queryset_values.py
@@ -1,8 +1,8 @@
from django.db.models.aggregates import Sum
from django.db.models.expressions import F
-from django.test import TestCase
+from django.test import TestCase, skipUnlessDBFeature
-from .models import Company, Employee
+from .models import Company, Employee, JSONFieldModel
class ValuesExpressionsTests(TestCase):
@@ -36,6 +36,19 @@ class ValuesExpressionsTests(TestCase):
with self.assertRaisesMessage(ValueError, msg):
Company.objects.values(**{crafted_alias: F("ceo__salary")})
+ @skipUnlessDBFeature("supports_json_field")
+ def test_values_expression_alias_sql_injection_json_field(self):
+ crafted_alias = """injected_name" from "expressions_company"; --"""
+ msg = (
+ "Column aliases cannot contain whitespace characters, quotation marks, "
+ "semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ JSONFieldModel.objects.values(f"data__{crafted_alias}")
+
+ with self.assertRaisesMessage(ValueError, msg):
+ JSONFieldModel.objects.values_list(f"data__{crafted_alias}")
+
def test_values_expression_group_by(self):
# values() applies annotate() first, so values selected are grouped by
# id, not firstname.

133
CVE-2024-45230.patch Normal file
View File

@ -0,0 +1,133 @@
From 65a776dd25b657cc32edafaad98d91aa0b51e641 Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Mon, 12 Aug 2024 15:17:57 +0200
Subject: [PATCH 1/2] [4.2.x] Fixed CVE-2024-45230 -- Mitigated potential DoS
in urlize and urlizetrunc template filters.
Thanks MProgrammer (https://hackerone.com/mprogrammer) for the report.
---
django/utils/html.py | 17 ++++++++------
docs/ref/templates/builtins.txt | 11 ++++++++++
docs/releases/4.2.16.txt | 15 +++++++++++++
docs/releases/index.txt | 1 +
.../filter_tests/test_urlize.py | 22 +++++++++++++++++++
tests/utils_tests/test_html.py | 1 +
6 files changed, 60 insertions(+), 7 deletions(-)
create mode 100644 docs/releases/4.2.16.txt
Index: Django-2.2.28/django/utils/html.py
===================================================================
--- Django-2.2.28.orig/django/utils/html.py
+++ Django-2.2.28/django/utils/html.py
@@ -322,14 +322,17 @@ def urlize(text, trim_url_limit=None, no
potential_entity = middle[amp:]
escaped = html.unescape(potential_entity)
if escaped == potential_entity or escaped.endswith(";"):
- 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 + ";"
+ rstripped = middle.rstrip(TRAILING_PUNCTUATION_CHARS)
+ trail_start = len(rstripped)
+ amount_trailing_semicolons = len(middle) - len(middle.rstrip(";"))
+ if amp > -1 and amount_trailing_semicolons > 1:
+ # Leave up to most recent semicolon as might be an entity.
+ recent_semicolon = middle[trail_start:].index(";")
+ middle_semicolon_index = recent_semicolon + trail_start + 1
+ trail = middle[middle_semicolon_index:] + trail
+ middle = rstripped + middle[trail_start:middle_semicolon_index]
else:
- trail = middle[len(rstripped) :] + trail
+ trail = middle[trail_start:] + trail
middle = rstripped
trimmed_something = True
# Trim trailing punctuation (after trimming wrapping punctuation,
Index: Django-2.2.28/docs/ref/templates/builtins.txt
===================================================================
--- Django-2.2.28.orig/docs/ref/templates/builtins.txt
+++ Django-2.2.28/docs/ref/templates/builtins.txt
@@ -2463,6 +2463,17 @@ Django's built-in :tfilter:`escape` filt
email addresses that contain single quotes (``'``), things won't work as
expected. Apply this filter only to plain text.
+.. warning::
+
+ Using ``urlize`` or ``urlizetrunc`` can incur a performance penalty, which
+ can become severe when applied to user controlled values such as content
+ stored in a :class:`~django.db.models.TextField`. You can use
+ :tfilter:`truncatechars` to add a limit to such inputs:
+
+ .. code-block:: html+django
+
+ {{ value|truncatechars:500|urlize }}
+
.. templatefilter:: urlizetrunc
``urlizetrunc``
Index: Django-2.2.28/docs/releases/4.2.16.txt
===================================================================
--- /dev/null
+++ Django-2.2.28/docs/releases/4.2.16.txt
@@ -0,0 +1,15 @@
+===========================
+Django 4.2.16 release notes
+===========================
+
+*September 3, 2024*
+
+Django 4.2.16 fixes one security issue with severity "moderate" and one
+security issues with severity "low" in 4.2.15.
+
+CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html.urlize()``
+===========================================================================================
+
+:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
+denial-of-service attack via very large inputs with a specific sequence of
+characters.
Index: Django-2.2.28/tests/template_tests/filter_tests/test_urlize.py
===================================================================
--- Django-2.2.28.orig/tests/template_tests/filter_tests/test_urlize.py
+++ Django-2.2.28/tests/template_tests/filter_tests/test_urlize.py
@@ -260,6 +260,28 @@ class FunctionTests(SimpleTestCase):
'A test <a href="http://testing.com/example" rel="nofollow">http://testing.com/example</a>.,:;)&quot;!'
)
+ def test_trailing_semicolon(self):
+ self.assertEqual(
+ urlize("http://example.com?x=&amp;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp;</a>",
+ )
+ self.assertEqual(
+ urlize("http://example.com?x=&amp;;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp;</a>;",
+ )
+ self.assertEqual(
+ urlize("http://example.com?x=&amp;;;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp;</a>;;",
+ )
+ self.assertEqual(
+ urlize("http://example.com?x=&amp.;...;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp</a>.;...;",
+ )
+
def test_brackets(self):
"""
#19070 - Check urlize handles brackets properly
Index: Django-2.2.28/tests/utils_tests/test_html.py
===================================================================
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
+++ Django-2.2.28/tests/utils_tests/test_html.py
@@ -282,6 +282,7 @@ class TestUtilsHtml(SimpleTestCase):
"&:" + ";" * 100_000,
"&.;" * 100_000,
".;" * 100_000,
+ "&" + ";:" * 100_000,
)
for value in tests:
with self.subTest(value=value):

133
CVE-2024-45231.patch Normal file
View File

@ -0,0 +1,133 @@
From fe42da9cdacd9f43fb0d499244314c36f9a11a19 Mon Sep 17 00:00:00 2001
From: Natalia <124304+nessita@users.noreply.github.com>
Date: Mon, 19 Aug 2024 14:47:38 -0300
Subject: [PATCH 2/2] [4.2.x] Fixed CVE-2024-45231 -- Avoided server error on
password reset when email sending fails.
On successful submission of a password reset request, an email is sent
to the accounts known to the system. If sending this email fails (due to
email backend misconfiguration, service provider outage, network issues,
etc.), an attacker might exploit this by detecting which password reset
requests succeed and which ones generate a 500 error response.
Thanks to Thibaut Spriet for the report, and to Mariusz Felisiak and
Sarah Boyce for the reviews.
---
django/contrib/auth/forms.py | 9 ++++++++-
docs/ref/logging.txt | 12 ++++++++++++
docs/releases/4.2.16.txt | 11 +++++++++++
docs/topics/auth/default.txt | 4 +++-
tests/auth_tests/test_forms.py | 21 +++++++++++++++++++++
tests/mail/custombackend.py | 5 +++++
6 files changed, 60 insertions(+), 2 deletions(-)
Index: Django-2.2.28/django/contrib/auth/forms.py
===================================================================
--- Django-2.2.28.orig/django/contrib/auth/forms.py
+++ Django-2.2.28/django/contrib/auth/forms.py
@@ -1,3 +1,4 @@
+import logging
import unicodedata
from django import forms
@@ -18,6 +19,7 @@ from django.utils.text import capfirst
from django.utils.translation import gettext, gettext_lazy as _
UserModel = get_user_model()
+logger = logging.getLogger("django.contrib.auth")
def _unicode_ci_compare(s1, s2):
@@ -256,7 +258,12 @@ class PasswordResetForm(forms.Form):
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
- email_message.send()
+ try:
+ email_message.send()
+ except Exception:
+ logger.exception(
+ "Failed to send password reset email to %s:", context["user"].pk
+ )
def get_users(self, email):
"""Given an email, return matching user(s) who should receive a reset.
Index: Django-2.2.28/docs/releases/4.2.16.txt
===================================================================
--- Django-2.2.28.orig/docs/releases/4.2.16.txt
+++ Django-2.2.28/docs/releases/4.2.16.txt
@@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-serv
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
denial-of-service attack via very large inputs with a specific sequence of
characters.
+
+CVE-2024-45231: Potential user email enumeration via response status on password reset
+======================================================================================
+
+Due to unhandled email sending failures, the
+:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
+attackers to enumerate user emails by issuing password reset requests and
+observing the outcomes.
+
+To mitigate this risk, exceptions occurring during password reset email sending
+are now handled and logged using the :ref:`django-contrib-auth-logger` logger.
Index: Django-2.2.28/docs/topics/auth/default.txt
===================================================================
--- Django-2.2.28.orig/docs/topics/auth/default.txt
+++ Django-2.2.28/docs/topics/auth/default.txt
@@ -1530,7 +1530,9 @@ provides several built-in forms located
.. method:: send_mail(subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None)
Uses the arguments to send an ``EmailMultiAlternatives``.
- Can be overridden to customize how the email is sent to the user.
+ Can be overridden to customize how the email is sent to the user. If
+ you choose to override this method, be mindful of handling potential
+ exceptions raised due to email sending failures.
:param subject_template_name: the template for the subject.
:param email_template_name: the template for the email body.
Index: Django-2.2.28/tests/auth_tests/test_forms.py
===================================================================
--- Django-2.2.28.orig/tests/auth_tests/test_forms.py
+++ Django-2.2.28/tests/auth_tests/test_forms.py
@@ -929,6 +929,27 @@ class PasswordResetFormTest(TestDataMixi
message.get_payload(1).get_payload()
))
+ @override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend")
+ def test_save_send_email_exceptions_are_catched_and_logged(self):
+ (user, username, email) = self.create_dummy_user()
+ form = PasswordResetForm({"email": email})
+ self.assertTrue(form.is_valid())
+
+ with self.assertLogs("django.contrib.auth", level=0) as cm:
+ form.save()
+
+ self.assertEqual(len(mail.outbox), 0)
+ self.assertEqual(len(cm.output), 1)
+ errors = cm.output[0].split("\n")
+ pk = user.pk
+ self.assertEqual(
+ errors[0],
+ f"ERROR:django.contrib.auth:Failed to send password reset email to {pk}:",
+ )
+ self.assertEqual(
+ errors[-1], "ValueError: FailingEmailBackend is doomed to fail."
+ )
+
@override_settings(AUTH_USER_MODEL='auth_tests.CustomEmailField')
def test_custom_email_field(self):
email = 'test@mail.com'
Index: Django-2.2.28/tests/mail/custombackend.py
===================================================================
--- Django-2.2.28.orig/tests/mail/custombackend.py
+++ Django-2.2.28/tests/mail/custombackend.py
@@ -13,3 +13,8 @@ class EmailBackend(BaseEmailBackend):
# Messages are stored in an instance variable for testing.
self.test_outbox.extend(email_messages)
return len(email_messages)
+
+
+class FailingEmailBackend(BaseEmailBackend):
+ def send_messages(self, email_messages):
+ raise ValueError("FailingEmailBackend is doomed to fail.")

View File

@ -1,7 +1,7 @@
%global _empty_manifest_terminate_build 0
Name: python-django
Version: 2.2.27
Release: 11
Release: 12
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
License: Apache-2.0 and Python-2.0 and OFL-1.1 and MIT
URL: https://www.djangoproject.com/
@ -25,6 +25,17 @@ Patch8: CVE-2023-46695.patch
Patch9: CVE-2024-24680.patch
# https://github.com/django/django/commit/072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521
Patch10: CVE-2024-27351.patch
# patch11-20 origin: https://build.opensuse.org/package/show/openSUSE:Backports:SLE-15-SP5:Update/python-Django
Patch11: CVE-2024-38875.patch
Patch12: CVE-2024-39329.patch
Patch13: CVE-2024-39330.patch
Patch14: CVE-2024-39614.patch
Patch15: CVE-2024-41989.patch
Patch16: CVE-2024-41990.patch
Patch17: CVE-2024-41991.patch
Patch18: CVE-2024-42005.patch
Patch19: CVE-2024-45230.patch
Patch20: CVE-2024-45231.patch
BuildArch: noarch
%description
@ -91,6 +102,10 @@ mv %{buildroot}/doclist.lst .
%{_docdir}/*
%changelog
* Fri Oct 11 2024 wangkai <13474090681@163.com> - 2.2.27-12
- Fix CVE-2024-38875 CVE-2024-39329 CVE-2024-39330 CVE-2024-39614 CVE-2024-41989
CVE-2024-41990 CVE-2024-41991 CVE-2024-42005 CVE-2024-45230 CVE-2024-45231
* Tue Mar 05 2024 yaoxin <yao_xin001@hoperun.com> - 2.2.27-11
- Fix CVE-2024-27351