Sync from SUSE:SLFO:Main python-Django revision 2bd7ba1c498ee607b8fe04ff26398472

This commit is contained in:
Adrian Schröter 2024-09-04 09:08:57 +02:00
parent e1462fda93
commit 237f286c53
20 changed files with 1596 additions and 322 deletions

View File

@ -1,121 +0,0 @@
From 2d173757922183f7e9b79d31fd4ccd9086cc6ce2 Mon Sep 17 00:00:00 2001
From: Shai Berger <shai@platonix.com>
Date: Mon, 19 Feb 2024 13:56:37 +0100
Subject: [PATCH] [4.2.x] Fixed CVE-2024-27351 -- Prevented potential ReDoS in
Truncator.words().
Thanks Seokchan Yoon for the report.
Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
---
django/utils/text.py | 57 ++++++++++++++++++++++++++++++++--
docs/releases/3.2.25.txt | 8 +++++
docs/releases/4.2.11.txt | 8 +++++
tests/utils_tests/test_text.py | 26 ++++++++++++++++
4 files changed, 97 insertions(+), 2 deletions(-)
Index: Django-4.2.6/django/utils/text.py
===================================================================
--- Django-4.2.6.orig/django/utils/text.py
+++ Django-4.2.6/django/utils/text.py
@@ -23,8 +23,61 @@ def capfirst(x):
return x[0].upper() + 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
Index: Django-4.2.6/tests/utils_tests/test_text.py
===================================================================
--- Django-4.2.6.orig/tests/utils_tests/test_text.py
+++ Django-4.2.6/tests/utils_tests/test_text.py
@@ -183,6 +183,32 @@ class TestUtilsText(SimpleTestCase):
truncator = text.Truncator("<p>I &lt;3 python, what about you?</p>")
self.assertEqual("<p>I &lt;3 python,…</p>", truncator.words(3, html=True))
+ # Only open brackets.
+ test = "<" * 60_000
+ truncator = text.Truncator(test)
+ self.assertEqual(truncator.words(1, html=True), test)
+
+ # Tags with special chars in attrs.
+ truncator = text.Truncator(
+ """<i style="margin: 5%; font: *;">Hello, my dear lady!</i>"""
+ )
+ self.assertEqual(
+ """<i style="margin: 5%; font: *;">Hello, my dear…</i>""",
+ truncator.words(3, html=True),
+ )
+
+ # Tags with special non-latin chars in attrs.
+ truncator = text.Truncator("""<p data-x="א">Hello, my dear lady!</p>""")
+ self.assertEqual(
+ """<p data-x="א">Hello, my dear…</p>""",
+ truncator.words(3, html=True),
+ )
+
+ # Misplaced brackets.
+ truncator = text.Truncator("hello >< world")
+ self.assertEqual(truncator.words(1, html=True), "hello…")
+ self.assertEqual(truncator.words(2, html=True), "hello >< world")
+
@patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
def test_truncate_words_html_size_limit(self):
max_len = text.Truncator.MAX_LENGTH_HTML

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

@ -0,0 +1,164 @@
From 79f368764295df109a37192f6182fb6f361d85b5 Mon Sep 17 00:00:00 2001
From: Adam Johnson <me@adamj.eu>
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

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

@ -0,0 +1,87 @@
From 156d3186c96e3ec2ca73b8b25dc2ef366e38df14 Mon Sep 17 00:00:00 2001
From: Michael Manfre <mike@manfre.net>
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

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

@ -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

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

@ -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 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 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
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
"""
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

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

@ -0,0 +1,79 @@
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(+)
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index d446b54ade..3f89eba6bb 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -163,6 +163,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):
diff --git a/tests/template_tests/filter_tests/test_floatformat.py b/tests/template_tests/filter_tests/test_floatformat.py
index db17622309..c22b5dca6b 100644
--- a/tests/template_tests/filter_tests/test_floatformat.py
+++ b/tests/template_tests/filter_tests/test_floatformat.py
@@ -77,6 +77,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_force_grouping(self):
with translation.override("en"):
@@ -134,6 +135,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:
--
2.34.1

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

@ -0,0 +1,64 @@
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(-)
diff --git a/django/utils/html.py b/django/utils/html.py
index fd313ff9ca..dd52f1f7fe 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -378,7 +378,11 @@ class Urlizer:
trimmed_something = True
counts[closing] -= strip
- rstripped = middle.rstrip(self.trailing_punctuation_chars_no_semicolon)
+ amp = middle.rfind("&")
+ if amp == -1:
+ rstripped = middle.rstrip(self.trailing_punctuation_chars)
+ else:
+ rstripped = middle.rstrip(self.trailing_punctuation_chars_no_semicolon)
if rstripped != middle:
trail = middle[len(rstripped) :] + trail
middle = rstripped
@@ -386,15 +390,9 @@ class Urlizer:
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:
+ 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:
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index 6dab41634a..c45e0dfac1 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -349,6 +349,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):
--
2.34.1

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

@ -0,0 +1,117 @@
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(-)
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 5e3416bc28..3d11a40efe 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -383,7 +383,7 @@ class AdminURLFieldWidget(forms.URLInput):
context["current_label"] = _("Currently:")
context["change_label"] = _("Change:")
context["widget"]["href"] = (
- smart_urlquote(context["widget"]["value"]) if value else ""
+ smart_urlquote(context["widget"]["value"]) if url_valid else ""
)
context["url_valid"] = url_valid
return context
diff --git a/django/utils/html.py b/django/utils/html.py
index dd52f1f7fe..23575d3c11 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -13,6 +13,8 @@ from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import SafeData, SafeString, mark_safe
from django.utils.text import normalize_newlines
+MAX_URL_LENGTH = 2048
+
@keep_lazy(SafeString)
def escape(text):
@@ -300,9 +302,9 @@ class Urlizer:
# Make URL we want to point to.
url = None
nofollow_attr = ' rel="nofollow"' if nofollow else ""
- if self.simple_url_re.match(middle):
+ if len(middle) <= MAX_URL_LENGTH and self.simple_url_re.match(middle):
url = smart_urlquote(html.unescape(middle))
- elif self.simple_url_2_re.match(middle):
+ elif len(middle) <= MAX_URL_LENGTH and self.simple_url_2_re.match(middle):
url = smart_urlquote("http://%s" % html.unescape(middle))
elif ":" not in middle and self.is_email_simple(middle):
local, domain = middle.rsplit("@", 1)
@@ -417,6 +419,10 @@ class Urlizer:
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
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index 0e20206048..4281ed07c6 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -461,7 +461,12 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase):
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"])
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index c45e0dfac1..83ebe4334b 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -328,6 +328,15 @@ class TestUtilsHtml(SimpleTestCase):
'Search for <a href="http://google.com/?q=">google.com/?q=</a>!',
),
("foo@example.com", '<a href="mailto:foo@example.com">foo@example.com</a>'),
+ (
+ "test@" + "한.글." * 15 + "aaa",
+ '<a href="mailto:test@'
+ + "xn--6q8b.xn--bj0b." * 15
+ + 'aaa">'
+ + "test@"
+ + "한.글." * 15
+ + "aaa</a>",
+ ),
)
for value, output in tests:
with self.subTest(value=value):
@@ -336,6 +345,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",
--
2.34.1

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

@ -0,0 +1,78 @@
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(-)
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index f98c6c668b..e68fd9efb7 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -2415,6 +2415,8 @@ class Query(BaseExpression):
self.has_select_fields = True
if fields:
+ for field in fields:
+ self.check_alias(field)
field_names = []
extra_names = []
annotation_names = []
diff --git a/tests/expressions/models.py b/tests/expressions/models.py
index 0a8a0a6584..6b21e9ccf3 100644
--- a/tests/expressions/models.py
+++ b/tests/expressions/models.py
@@ -106,3 +106,10 @@ class UUIDPK(models.Model):
class UUID(models.Model):
uuid = models.UUIDField(null=True)
uuid_fk = models.ForeignKey(UUIDPK, models.CASCADE, null=True)
+
+
+class JSONFieldModel(models.Model):
+ data = models.JSONField(null=True)
+
+ class Meta:
+ required_db_features = {"supports_json_field"}
diff --git a/tests/expressions/test_queryset_values.py b/tests/expressions/test_queryset_values.py
index 80addef37b..47bd1358de 100644
--- a/tests/expressions/test_queryset_values.py
+++ b/tests/expressions/test_queryset_values.py
@@ -1,7 +1,7 @@
from django.db.models import F, Sum
-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):
@@ -43,6 +43,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.
--
2.34.1

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-4.2.11/django/utils/html.py
===================================================================
--- Django-4.2.11.orig/django/utils/html.py
+++ Django-4.2.11/django/utils/html.py
@@ -395,14 +395,17 @@ class Urlizer:
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(self.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
Index: Django-4.2.11/docs/ref/templates/builtins.txt
===================================================================
--- Django-4.2.11.orig/docs/ref/templates/builtins.txt
+++ Django-4.2.11/docs/ref/templates/builtins.txt
@@ -2831,6 +2831,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-4.2.11/docs/releases/4.2.16.txt
===================================================================
--- /dev/null
+++ Django-4.2.11/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-4.2.11/tests/template_tests/filter_tests/test_urlize.py
===================================================================
--- Django-4.2.11.orig/tests/template_tests/filter_tests/test_urlize.py
+++ Django-4.2.11/tests/template_tests/filter_tests/test_urlize.py
@@ -305,6 +305,28 @@ class FunctionTests(SimpleTestCase):
"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-4.2.11/tests/utils_tests/test_html.py
===================================================================
--- Django-4.2.11.orig/tests/utils_tests/test_html.py
+++ Django-4.2.11/tests/utils_tests/test_html.py
@@ -364,6 +364,7 @@ class TestUtilsHtml(SimpleTestCase):
"&:" + ";" * 100_000,
"&.;" * 100_000,
".;" * 100_000,
+ "&" + ";:" * 100_000,
)
for value in tests:
with self.subTest(value=value):

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

@ -0,0 +1,159 @@
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(-)
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 061dc81b42..20ce1ba39c 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -1,3 +1,4 @@
+import logging
import unicodedata
from django import forms
@@ -16,6 +17,7 @@ from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
UserModel = get_user_model()
+logger = logging.getLogger("django.contrib.auth")
def _unicode_ci_compare(s1, s2):
@@ -314,7 +316,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.
diff --git a/docs/ref/logging.txt b/docs/ref/logging.txt
index b11fb752f7..3d33e0af63 100644
--- a/docs/ref/logging.txt
+++ b/docs/ref/logging.txt
@@ -204,6 +204,18 @@ all database queries.
Support for logging transaction management queries (``BEGIN``, ``COMMIT``,
and ``ROLLBACK``) was added.
+.. _django-contrib-auth-logger:
+
+``django.contrib.auth``
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 4.2.16
+
+Log messages related to :doc:`contrib/auth`, particularly ``ERROR`` messages
+are generated when a :class:`~django.contrib.auth.forms.PasswordResetForm` is
+successfully submitted but the password reset email cannot be delivered due to
+a mail sending exception.
+
.. _django-security-logger:
``django.security.*``
diff --git a/docs/releases/4.2.16.txt b/docs/releases/4.2.16.txt
index 043041a97f..4e632d5d77 100644
--- a/docs/releases/4.2.16.txt
+++ b/docs/releases/4.2.16.txt
@@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html
: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.
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index 528902416d..ad840c5e57 100644
--- a/docs/topics/auth/default.txt
+++ b/docs/topics/auth/default.txt
@@ -1661,7 +1661,9 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
.. 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.
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index 81c56a428e..ccb1a26a2b 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -1245,6 +1245,27 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
)
)
+ @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"
diff --git a/tests/mail/custombackend.py b/tests/mail/custombackend.py
index 14e7f077ba..c6c567b642 100644
--- a/tests/mail/custombackend.py
+++ b/tests/mail/custombackend.py
@@ -12,3 +12,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.")
--
2.46.0

View File

@ -0,0 +1,67 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
This file contains MD5, SHA1, and SHA256 checksums for the source-code
tarball and wheel files of Django 4.2.11, released March 4, 2024.
To use this file, you will need a working install of PGP or other
compatible public-key encryption software. You will also need to have
the Django release manager's public key in your keyring. This key has
the ID ``2EF56372BA48CD1B`` and can be imported from the MIT
keyserver, for example, if using the open-source GNU Privacy Guard
implementation of PGP:
gpg --keyserver pgp.mit.edu --recv-key 2EF56372BA48CD1B
or via the GitHub API:
curl https://github.com/felixxm.gpg | gpg --import -
Once the key is imported, verify this file:
gpg --verify Django-4.2.11.checksum.txt
Once you have verified this file, you can use normal MD5, SHA1, or SHA256
checksumming applications to generate the checksums of the Django
package and compare them to the checksums listed below.
Release packages
================
https://www.djangoproject.com/m/releases/4.2/Django-4.2.11-py3-none-any.whl
https://www.djangoproject.com/m/releases/4.2/Django-4.2.11.tar.gz
MD5 checksums
=============
5ac62cf0d75216275a8d5f3b9a87b7a1 Django-4.2.11-py3-none-any.whl
33dc961e25b6ed54e22b62726b334d4d Django-4.2.11.tar.gz
SHA1 checksums
==============
69943b2e90d352cd8d536f34a0cd38dc3d3026be Django-4.2.11-py3-none-any.whl
fda76a55736054cb5aafb73d2caa3f2d47765f9f Django-4.2.11.tar.gz
SHA256 checksums
================
ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3 Django-4.2.11-py3-none-any.whl
6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 Django-4.2.11.tar.gz
-----BEGIN PGP SIGNATURE-----
iQJPBAEBCAA5FiEEq7LCqM0B8WE2GLcNLvVjcrpIzRsFAmXle9IbHGZlbGlzaWFr
Lm1hcml1c3pAZ21haWwuY29tAAoJEC71Y3K6SM0bYRAP/RaamVJZrHq8H1vXx0IF
+H99BDF282S6rEjajxe4vhEz8JnWFUkALlvh9MQQ2GOH7M66EfYP5K0BBWZHJTki
Sf8zFRSaOYkblFaKvKMKC8m4nQ4XI2S2y3Nvx7KaaJSBsanahgDFFFcEdx8LnZdY
2Vj9S2hnm9eT/0GSbTO2nn1lWcrShoYm2ZVHgmrH1qkX24uBO7VXD3x6j2pzdplg
mW7rW03seWUtf/FQCGVnbTblxX7N0E+5BeeqwJvom8ijFEpcoFHY6EDLooXoq0MQ
aDKOU5xns4k6YnPIDWSlZKa/RhxLUhkAyyiMrS1ADZF8Ee7Xk+M8cAt6okv6EBul
gEWVtVKGYV9DKlKBqTkWcgiFH4nKSl+ckVrTK8OTss3zIUxkXQr34Ee5rJ6ciC+8
2FHq3S55ylBvXDW1U+tfknyi78GLywjySxhdSOnZIEAaWDnFpW3X+838FKRUXMlC
rMvQJswtpPPx76E1RyzwSuBdpVkzHoC49GGeZfyPynlupZJ9Vcue7w2q8WvQ0GrX
/qhPFU21AEvf2siOlFwSr9TopjIMFckHMuLrSrVyoYoDZq1DXyprEpkasPXOq9zM
FTqWPscC7M2BI0mAAMcJTWPBlqmfwF0W7Jiqo7cZutmdSVhOxDrySr3zWYXBzfht
ERfQPBvTEYmsXtBC+H3mk040
=I96k
-----END PGP SIGNATURE-----

BIN
Django-4.2.11.tar.gz (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,67 +0,0 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
This file contains MD5, SHA1, and SHA256 checksums for the source-code
tarball and wheel files of Django 4.2.6, released October 4, 2023.
To use this file, you will need a working install of PGP or other
compatible public-key encryption software. You will also need to have
the Django release manager's public key in your keyring. This key has
the ID ``2EE82A8D9470983E`` and can be imported from the MIT
keyserver, for example, if using the open-source GNU Privacy Guard
implementation of PGP:
gpg --keyserver pgp.mit.edu --recv-key 2EE82A8D9470983E
or via the GitHub API:
curl https://github.com/nessita.gpg | gpg --import -
Once the key is imported, verify this file:
gpg --verify Django-4.2.6.checksum.txt
Once you have verified this file, you can use normal MD5, SHA1, or SHA256
checksumming applications to generate the checksums of the Django
package and compare them to the checksums listed below.
Release packages
================
https://www.djangoproject.com/m/releases/4.2/Django-4.2.6-py3-none-any.whl
https://www.djangoproject.com/m/releases/4.2/Django-4.2.6.tar.gz
MD5 checksums
=============
db83d48600d6afff838e53f42f9ebebb Django-4.2.6-py3-none-any.whl
ad84c2b9bbebaa26427a2a656fe5ceea Django-4.2.6.tar.gz
SHA1 checksums
==============
36650eb323bd34afbe47936bd3e7bf62ed4d929c Django-4.2.6-py3-none-any.whl
6e912eeabd1df0b652e0da44cd3a556a496a1811 Django-4.2.6.tar.gz
SHA256 checksums
================
a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215 Django-4.2.6-py3-none-any.whl
08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f Django-4.2.6.tar.gz
-----BEGIN PGP SIGNATURE-----
iQJcBAEBCABGFiEEW1sboQ2FrHxcduOPLugqjZRwmD4FAmUdYL4oHDEyNDMwNCtu
ZXNzaXRhQHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbQAKCRAu6CqNlHCYPsQzEACE
1e0nWDjh2RkV0nLraeEOd8DkyeCAMhFsiWGVkNY7chpeoXnF0YksHg9z2MiTDDJ9
12EyYLFZPMCzqt3gO1/4iWYu/zx7Pb8gPTeg5NTLUnezZt4QT6FSv3fY7ByubqXQ
lUp0jJJd8B3uc5zdZNLyg9OGBOHG7lqv7Eg7H3YUwXFo7VOkerLLgASTScE22Guo
jyQYlnnLtse70l/MTTdmJYwJxbNM7LP4RXSovHV34nL2HCI5vDWyNlOgVeU+MT9F
AQCW8Lb0H+GvrhL6Hc1D8xQl7OOvpo/5/53J1i/M2Ml60qeYbjWkqEByPI5d/9oS
oHMzZcbnhlWcePy7zEYfyzQ0qFv3m/qIIf2rcd3mnrusMScWGsCFSSjqWLdoT2eO
Cvz5Q+FGH8g2ce+DyfEDjDTzceReNL81lArmSPqntByYfp8COUuqBwe5PZ7T0yx7
w2LWWICVmCfjKgQ12Rk7ElxcliIILFgETJVuPtjx6SrkDEzNDpiTVQH2E9LXZYsV
5Qd7QEfTh0oEBBTPxHtSskTnfP/mJWAk62uLWYEcbmHTTcw4wQdnncwJS01tG+BD
sd4iY0UeL4cof3sxkwGkvC6Sr0H5fgYCJs4AgAmcWBCzwFvtUp/J3+/WEr9wExBH
/Fveza/vFJifyN1FwiemueuOqG/tvy1XJL6jCRH3gQ==
=cttz
-----END PGP SIGNATURE-----

BIN
Django-4.2.6.tar.gz (Stored with Git LFS)

Binary file not shown.

View File

@ -0,0 +1,25 @@
From 36736edaf595d2bbf1fe881609b2a4c8e3bac68a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz>
Date: Thu, 29 Jun 2023 12:29:21 +0200
Subject: [PATCH] Dirty hack: Remove a failing assert, failure does not seem
critical
---
tests/settings_tests/tests.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py
index 62cbffb..b7432d3 100644
--- a/tests/settings_tests/tests.py
+++ b/tests/settings_tests/tests.py
@@ -397,7 +397,6 @@ class TestComplexSettingOverride(SimpleTestCase):
with self.assertWarnsMessage(UserWarning, msg) as cm:
with override_settings(TEST_WARN="override"):
self.assertEqual(settings.TEST_WARN, "override")
- self.assertEqual(cm.filename, __file__)
class SecureProxySslHeaderTest(SimpleTestCase):
--
2.40.1

View File

@ -0,0 +1,14 @@
Index: Django-4.2.11/django/core/mail/message.py
===================================================================
--- Django-4.2.11.orig/django/core/mail/message.py
+++ Django-4.2.11/django/core/mail/message.py
@@ -168,7 +168,8 @@ class SafeMIMEText(MIMEMixin, MIMEText):
def set_payload(self, payload, charset=None):
if charset == "utf-8" and not isinstance(charset, Charset.Charset):
has_long_lines = any(
- len(line.encode()) > RFC5322_EMAIL_LINE_LENGTH_LIMIT
+ len(line.encode(errors="surrogateescape"))
+ > RFC5322_EMAIL_LINE_LENGTH_LIMIT
for line in payload.splitlines()
)
# Quoted-Printable encoding has the side effect of shortening long

View File

@ -1,7 +1,112 @@
-------------------------------------------------------------------
Thu Feb 29 13:19:00 UTC 2024 - Alberto Planas Dominguez <aplanas@suse.com>
Mon Sep 2 12:48:52 UTC 2024 - Markéta Machová <mmachova@suse.com>
- Add CVE-2024-27351.patch patch (CVE-2024-27351, bsc#1220358)
- Add more security patches:
* CVE-2024-45230.patch (bsc#1229823)
* CVE-2024-45231.patch (bsc#1229824)
-------------------------------------------------------------------
Thu Aug 1 09:37:57 UTC 2024 - Markéta Machová <mmachova@suse.com>
- Add bunch of security patches:
* CVE-2024-42005.patch (bsc#1228629)
* CVE-2024-41989.patch (bsc#1228630)
* CVE-2024-41990.patch (bsc#1228631)
* CVE-2024-41991.patch (bsc#1228632)
-------------------------------------------------------------------
Fri Jul 12 12:40:47 UTC 2024 - Nico Krapp <nico.krapp@suse.com>
- 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 Apr 18 06:39:36 UTC 2024 - Daniel Garcia <daniel.garcia@suse.com>
- Add fix-safemimetext-set_payload.patch, to support python 3.11.9+
(gh#django/django@b231bcd19e57, bsc#1222880)
-------------------------------------------------------------------
Mon Mar 4 14:05:28 UTC 2024 - Alberto Planas Dominguez <aplanas@suse.com>
- Update to 4.2.11 (CVE-2024-27351, bsc#1220358)
* CVE-2024-27351: Potential regular expression denial-of-service in
django.utils.text.Truncator.words()
* Fixed a regression in Django 4.2.10 where intcomma template filter
could return a leading comma for string representation of floats
- Remove python3122.patch, already upstream
-------------------------------------------------------------------
Fri Feb 9 10:18:37 UTC 2024 - Daniel Garcia <daniel.garcia@suse.com>
- Add python3122.patch to fix tests with python 3.12.2
gh#django/django#17843
- Update to 4.2.10 (bsc#1219683, CVE-2024-24680):
- Django 4.2.10 fixes a security issue with severity "moderate" in
4.2.9.
CVE-2024-24680: Potential denial-of-service in intcomma template
filter The intcomma template filter was subject to a potential
denial-of-service attack when used with very long strings.
-------------------------------------------------------------------
Thu Jan 4 09:27:51 UTC 2024 - Alberto Planas Dominguez <aplanas@suse.com>
- Update to 4.2.9:
* Fixed a regression in Django 4.2.8 where admin fields on the same
line could overflow the page and become non-interactive
-------------------------------------------------------------------
Mon Dec 4 10:21:00 UTC 2023 - Alberto Planas Dominguez <aplanas@suse.com>
- Update to 4.2.8
* Fixed a regression in Django 4.2 that caused makemigrations
--check to stop displaying pending migrations
* Fixed a regression in Django 4.2 that caused a crash of
QuerySet.aggregate() with aggregates referencing other aggregates
or window functions through conditional expressions
* Fixed a regression in Django 4.2 that caused a crash when
annotating a QuerySet with a Window expressions composed of a
partition_by clause mixing field types and aggregation expressions
* Fixed a regression in Django 4.2 where the admins change list
page had misaligned pagination links and inputs when using
list_editable
* Fixed a regression in Django 4.2 where checkboxes in the admin
would be centered on narrower screen widths
* Fixed a regression in Django 4.2 that caused a crash of querysets
with aggregations on MariaDB when the ONLY_FULL_GROUP_BY SQL mode
was enabled
* Fixed a regression in Django 4.2 where the admins read-only
password widget and some help texts were incorrectly aligned at
tablet widths
* Fixed a regression in Django 4.2 that caused a migration crash on
SQLite when altering unsupported Meta.db_table_comment
-------------------------------------------------------------------
Mon Nov 27 12:20:48 UTC 2023 - Dirk Müller <dmueller@suse.com>
- add dirty-hack-remove-assert.patch from fedora to fix
minor test failure with python 3.12
-------------------------------------------------------------------
Wed Nov 1 08:12:59 UTC 2023 - Alberto Planas Dominguez <aplanas@suse.com>
- Update to 4.2.7
* Fixed a regression in Django 4.2 that caused a crash of
QuerySet.aggregate() with aggregates referencing expressions
containing subqueries
* Restored, following a regression in Django 4.2, creating
varchar/text_pattern_ops indexes on CharField and TextField with
deterministic collations on PostgreSQL
-------------------------------------------------------------------
Mon Oct 16 08:33:05 UTC 2023 - Daniel Garcia Moreno <daniel.garcia@suse.com>
@ -117,7 +222,8 @@ Tue Jun 6 06:35:28 UTC 2023 - Alberto Planas Dominguez <aplanas@suse.com>
Thu May 4 07:02:58 UTC 2023 - Alberto Planas Dominguez <aplanas@suse.com>
- Update to 4.2.1
+ CVE-2023-31047: Potential bypass of validation when uploading multiple files using one form field
+ CVE-2023-31047: Potential bypass of validation when uploading
multiple files using one form field (bsc#1210866)
+ Bugfixes
* Fixed a regression in Django 4.2 that caused a crash of
QuerySet.defer() when deferring fields by attribute names

View File

@ -1,90 +1,121 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGiBErcoVkRBACt1HBsLQQ9HeRHrqMbYfWEW3d0KoWzjKU9ZW59oq8ceVCYfhyT
ZKxyLobyed+NhL3SJCE5e4hs5UfyBdS4c8I97MFDKCA5TBu3pMnYGxWje3fSwP6o
RGcP8Ji4/tISclyGrkMruDNzpT93R8H/SixPGFcH7kCp4xQxPBc0esdU4wCg1azF
kUuFijNryusT+i58hVE3dMkD/iAfCh4bcLyZ8aygLZxg3bn3YauJASEjuqVXUgTB
diBdhXnldq0xs2IwQJY1paAajXf5FsjlTVQrQWMtTQ5qWKpQr0lAanufnEDNu6GW
orWBzLaSWQWEkcRALmZS6MBkmVCx/JiIvt0sUxrG4boQ6qYlQYZsaHaAMUZT997v
1ktqA/4kPUfV2gqJuVzWwbhrKhAyhSivmhhe+1lUFa7phRmoMNw7/jXi9OV1lmL2
ty+0LkeCXUChrXarey4AnPI58aR0xshiAxGEI2jPi+vWkgGblOG3TBoZBH5jV+d2
/5mmlCs/KkJkdsN+LXR3m5o/oFs7MgGD8pxa1jwK9xcu1xKIqrQyTmF0YWxpYSBC
aWRhcnQgKG5lc3NpdGEpIDxuYXRhbGlhYmlkYXJ0QGdtYWlsLmNvbT6IYgQTEQIA
IgUCTG1snwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQrlwdYDo57Zf7
lQCeIHmWQQek0zboTqMuy60phrUIzowAn0ONlnzzL0oWiNUpbY8nDsernILWiGAE
ExECACAFAkrcoVkCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCuXB1gOjnt
l5FdAKCSLwUJNZXs3WXqKabi2adRcdqZ8gCeLgbbqJ2Dqqaeb3tXK6zWC7ZO9CK0
NE5hdGFsaWEgQmlkYXJ0IChuZXNzaXRhKSA8bmF0YWxpYS5iaWRhcnRAdWJ1bnR1
LmNvbT6IZQQTEQIAJQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AFAk8oONcC
GQEACgkQrlwdYDo57ZejrgCdFyBg4VipDYmoQ5eOpXe4Vegiwl4AoK00YytEeMvO
EFqZY+qVvqaV3It6iGIEExECACIFAkvrLFwCGwMGCwkIBwMCBhUIAgkKCwQWAgMB
Ah4BAheAAAoJEK5cHWA6Oe2XD+QAoK02osWaLzROXg54drLpJMNLs/DGAJ9XlSak
dQv6uX5QFT1QZCp/WwozIrQzTmF0YWxpYSBCaWRhcnQgKG5lc3NpdGEpIDxuYXRh
bGlhLmJpZGFydEBnbWFpbC5jb20+iGIEExECACIFAkvrLBMCGwMGCwkIBwMCBhUI
AgkKCwQWAgMBAh4BAheAAAoJEK5cHWA6Oe2XrQoAoIpzDPsuwhwuVcelVh3F8q3w
qhk2AKCj6rF6x+kzUwtT6lM8wkUj4x+CgLQ3TmF0YWxpYSBCaWRhcnQgKG5lc3Np
dGEpIDxuYXRhbGlhLmJpZGFydEBjYW5vbmljYWwuY29tPohgBBMRAgAgBQJK3gu6
AhsDBgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQrlwdYDo57ZfaNgCfXhjx28H4
WQ8CjWsdyJU2Kmh44qoAn0zp3TeEFuSPCEBZ0jAR4dwuSrpguQINBErcoVkQCACX
mxZ+acE5irfOe09OclJ+vKxqrnaEpveyLJZzKiWz5GlZLV3gPEMs3Pu0tGtTjadG
CRck2xIYArDz1aPwvM4dHswIy3TyzoSTgW1ybz5TXzkxWDcdwukYm1gKlWMb8JQW
v76KtoiNuY/EIUAaO9M7ZyUPSWunh5CK+ttYKs+KrD8wt8Te7PdsrstUMP2uplOt
I0zKK8P+gcCNZQTZh71Z8WAhZF/tn9LpkE9p0Au3pVEVk7Z8492TO4DySFhBNVEM
IY9KVNiZoEMAaiRUFgG3gPj3MD4wDyaiWp+5b8XQylXcfWsPx3nujLJNUiaJlV4u
Wjv0ZgwMHHLgORAlOJ2rAAMFB/94QWkhOmIzzx0iCob4fILZ2lqTt1fAAbaQxyq/
LIaI6iSHqebEVVR9OUVTzqNtc0yDifxsbDZXEHmU2qx+aARoYmonxNmNoUS/U6Io
2iPgP1Jwt13dbd284xlgDTx8QO/TjX9lFyvt7AEHIrcHaomwVS0Il7wIfzG24kqX
j17VhD2j/2V6uA7ADAh8u0WFO93i30qNSCaCRphCU4K7gLdHLIp8TsGLdx/gf2mB
5SyhNOkHwEx80kSiFt+H5fER7XQep/w51XybqAt7SsWaIjYLsyMYXyiVdQChwzBd
vusRKv9qjg9eiyHI6aOw6foOUFlpfMx1oeknFDJrjJ3PKUPyiEkEGBECAAkFAkrc
oVkCGwwACgkQrlwdYDo57ZffZACfS9pUk1P5poP86jh8K2K6jpjU0y0AoNQ4ejtn
mpJC4x7FruZyi1wVdkMxmQINBGQu6XIBEADAnmu8HNENZh7UTuu5GfTeFhpmyj5K
yz//txfrm0/b6uTW5TXPgLjuvMzGG8PtaZHRIgZ0gzA+x7T5zKMTaoKs3EvgR5D3
Y9NjteUWpf8FjvPhN01HZfaZ7yChwHwKobW0JYinNpBh0Cz51unGdLIDtELMaEFO
D8qdcpe63qG111S4G+4hcJUkXt4ALBpSnY9GOhlYQDn+ZDRGk1M9rjeMo+QsIJns
UZRlvBroJyg0toUXclw5QXFGp1+mrjOzKqdD0DmSN7LWlU0yCJB8H5bWZTiPAPOE
SW1Kb3kEW+Qy8YkcH7SkQ7N72wsuIwKJNiddMLZnXeR0Lcvt0t7ftUfs44VEZSwm
V0I7lyZZWr+Pei8nGaLxxCI4OtASXcQ+VVKF/HoR/necD1QmqmuCeiMLmYT5jEPZ
oovOri5onkWIQfjfWeUVErxNi9Uz18mi9P7PfAWOzNCmdkuVqsPtpymyDcKYYh9u
D/CTH9w1B69CRjld6NOfal05fIrfKuVgPvmQnPeCn+KgTBwv8T+mgGVjkBlDGpYy
6Y24s13R6WoawJnjIEjA/Q5QOSDXYtpgF8D3cMW+LUlD9lu2A6OO64H33rInIaut
8IFgKcTf3pXbzh1J6Zs+fcjOryitM7t4Fo1ClJ+DSn4yoUHxP2UEZL6LL0DF6LrE
kJjKxwRp20lPwwARAQABtDFOYXRhbGlhIDwxMjQzMDQrbmVzc2l0YUB1c2Vycy5u
b3JlcGx5LmdpdGh1Yi5jb20+iQJOBBMBCAA4FiEEW1sboQ2FrHxcduOPLugqjZRw
mD4FAmQu6XICGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQLugqjZRwmD56
rQ//UvzX9/4Eo3HPBMDEIwExjmYGyTqiQQm2KW9Z4U0zQVdeLVF8ZOdaIKn1RBUw
M02RJWI2HjovdLN3LbXpTtQtamSvvU6fv7vy5zLBJdVL9eKXXG+3MPCWDHOqwp/u
L/4lq0pUy5ejb4AnIQw1RIQiz6/pprDzgF0celWLhL3krxEqts76X1Jv9wCIYMXV
3reCBqpcfIqWQETLP/NdPVcOIRv0al1qcBw2hUOvEyx72n+17Hb30rFQp+35BaZZ
IjnL3H6pATgjk52EiDdRkyWL9CFrbJ/wkbAvEO29GcUPg1+bT6UZq8LKXyDs/WSL
UkxQ/6w8DzicBiFj/oT7GSMr8KfO4YUMzh/kAFA4v8Q+a53Dq1Icbk9LqkWpu1pA
Hopw3lpGdADuC0z/iYO/U04uUSN5YGyUUOgk5w+CkV8NuL/g2yniNf9AXrbt4ByI
V7cqBt9qdS6z6leuW1M8yrPhX1IcKKqj25sPKAgzp3A6Bt7orr1NZGOVJ4alR4ff
pYvq+wfmIPKf0AbzHGOkYjF4BMvkLRchhi28q+qX5cCuMr+aoqKUtJ2IFiXsvbFN
k0aYWUr5y5qSJoAVf0GMkByQW6+F6bXRRdCpS/JX5JA8qrYp+oV8VhveOOslHVqj
ILAlkMMXoTx6G79DdvQ87fdb4+aIQ96U4T8B6zMxazvmU1i5Ag0EZC7pcgEQAL9n
X/eaWY+v4GgeGjRIWmmrjMBYyeeyJIyL9Mk3iyH/gIOnTDmlX+njjyvyWKfMYIl1
HmMtzlF3OgsuLeekwbDrXA8xvslp1xmiKLOamPvXwPG/XqkJrYtzVUDEFCtRpEJ0
c38d+P8WEMjbviyIwJ9PxLllamEK61dRtj1NCMc/Ix4+b54UHxi44Jz1bqQxfgjj
u2o8sPnyZio+DRFWVE3Eocp0rdZ3rlKjUsBXKEElTuIScoKjGwKwaMfxoBgwRhzx
oESwk8CqlH7WzNookx1M1/JjKYdrwln2aNuChtlKLRmUqT7qqTNtett2vy73VM3b
zfXdor94S3q+YtMEvNbo9QCzn6La7HOx+PMm8XM2d9aC7Hz4FBK0xIQB+HLZEIhP
7KQ7GJ2Xn3LStyoO5K64uqi2X2YjsYUcPzvI3uUK+gtH3H1SSIazh7UAUbcEuo7N
K8vF9Vtqp6S2qkjoeV6Dnvy+6735b1WIBZieAmbKaz74IW1IP0lZn3pXeRFo2Wjq
Ojf8zkNacf61exysAkGU2fubsXSZxuxc8DVXKbkpK69tXDSOUmSKTBPVzzmIM79S
yYH1MMRZqQ52Y471qiEZxEPasJXIEVcWbdJxEC/eEiuptPAtojRQH6kJ/AF3Z9Xd
eBaxyuMQ249jqTYwjCehfumTbhP5VhO3QOxs31G/ABEBAAGJAjYEGAEIACAWIQRb
WxuhDYWsfFx2448u6CqNlHCYPgUCZC7pcgIbDAAKCRAu6CqNlHCYPhz3EACx3Hqf
KUMeqUTVOiDyHguBr1FrhMtU5m/nkjdbLWlBHOGHkM4RNDNQTPyQb/C8vcuHYv5l
DPFrzOawdjTyFCuo6f0TMIx38Bbjxo9C8XTnvKbUpyTEQ3dJm67ppF4n6cui+0IC
UefzPkkCbdIPzt2pYopMDB4Hv4Yv6hqeq987Iz1erh7dQe1TDTxIv9PXLYZT60Ro
K0+g+caU9LwVjYiLoeCM1Zhndy6fDV5mu3ctEzcqr/YVH9kDZAuF0O1SX9y42neJ
7hictnE0KrRymVL5d9pp2WKtPny+itSax/a///Q43m1gA9KFuKHtOuGUpYzf76FS
Ld0cC4xjDpPcVTGc8To4+CjNTIrjzbBYa3JU/3J2kwyEw/k1EucRb/RFPbklUSph
Kmd2ewcDLUvcasTwoR/0uplA8gAuV1x7wPBgAW7kmpjiQevl1KLj08HA/jTdfrdx
Yd1GGiNjBmHGu9C8YZ/7fJU50dhv4jWF4dw8OyXtAI4wk5aoJHsJ5iGIMVOVzNLe
mF4yM4XSBBno1mWgaSb42LInsYv/ti1VrOrBVzmAYAoUTZL0tfEXeyzHEmWGWVHe
SQMBvCqUmh/EcQDzPtkqjQQ1LyE5s2fyt5u+jE9JdK/61yKzbKI2UbpPtAaKSlDv
eAgTzM5bOOqtGR7VR2hlCM4I4k2D0Y/snh2HzA==
=ul9f
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
mSNZUt/duetfr8yS4yBrC5IMCo5nvfBpu8E=
=DZRV
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,7 +1,7 @@
#
# spec file for package python-Django
#
# Copyright (c) 2023 SUSE LLC
# Copyright (c) 2024 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@ -24,7 +24,7 @@
%{?sle15_python_module_pythons}
Name: python-Django
# We want support LTS versions of Django - numbered 2.2 -> 3.2 -> 4.2 etc
Version: 4.2.6
Version: 4.2.11
Release: 0
Summary: A high-level Python Web framework
License: BSD-3-Clause
@ -34,9 +34,32 @@ Source1: https://media.djangoproject.com/pgp/Django-%{version}.checksum.t
Source2: %{name}.keyring
Source99: python-Django-rpmlintrc
# PATCH-FIX-UPSTREAM https://github.com/django/django/commit/da2f8e8257d1bea4215381684ca4abfcee333c43 Refs #34118 -- Improved sanitize_address() error message for tuple with empty strings.
Patch: sanitize_address.patch
# PATCH-FIX-UPSTREAM CVE-2024-27351.patch bsc#1220358
Patch1: CVE-2024-27351.patch
Patch0: sanitize_address.patch
# PATCH-FIX-OPENSUSE: ignore minor failure on Python 3.12
Patch1: dirty-hack-remove-assert.patch
# PATCH-FIX-UPSTREAM: fix-safemimetext-set_payload.patch, gh#django/django@b231bcd19e57
# Add support for python 3.11.9+
Patch2: fix-safemimetext-set_payload.patch
# PATCH-FIX-UPSTREAM CVE-2024-38875.patch bsc#1227590
Patch3: CVE-2024-38875.patch
# PATCH-FIX-UPSTREAM CVE-2024-39329.patch bsc#1227593
Patch4: CVE-2024-39329.patch
# PATCH-FIX-UPSTREAM CVE-2024-39330.patch bsc#1227594
Patch5: CVE-2024-39330.patch
# PATCH-FIX-UPSTREAM CVE-2024-39614.patch bsc#1227595
Patch6: CVE-2024-39614.patch
# PATCH-FIX-UPSTREAM CVE-2024-41989.patch bsc#1228629
Patch7: CVE-2024-41989.patch
# PATCH-FIX-UPSTREAM CVE-2024-41990.patch bsc#1228630
Patch8: CVE-2024-41990.patch
# PATCH-FIX-UPSTREAM CVE-2024-41991.patch bsc#1228631
Patch9: CVE-2024-41991.patch
# PATCH-FIX-UPSTREAM CVE-2024-42005.patch bsc#1228632
Patch10: CVE-2024-42005.patch
# PATCH-FIX-UPSTREAM CVE-2024-45230.patch bsc#1229823
Patch11: CVE-2024-45230.patch
# PATCH-FIX-UPSTREAM CVE-2024-45231.patch bsc#1229824
Patch12: CVE-2024-45231.patch
BuildRequires: %{python_module Jinja2 >= 2.9.2}
BuildRequires: %{python_module Pillow >= 6.2.0}
BuildRequires: %{python_module PyYAML}
@ -68,7 +91,7 @@ Requires: python-pytz
Requires: python-setuptools
Requires: python-sqlparse >= 0.3.1
Requires(post): update-alternatives
Requires(postun):update-alternatives
Requires(postun): update-alternatives
Recommends: python-Jinja2 >= 2.9.2
Recommends: python-PyYAML
Recommends: python-geoip2