Compare commits

..

No commits in common. "48ae12b320ea1796ed522aa17e9317867b4fe7fe" and "02439efa1bb5eca00ee8c9e5443880a8ce6af714" have entirely different histories.

15 changed files with 1 additions and 1903 deletions

View File

@ -1,205 +0,0 @@
From c1171ffbd570db90ca206c30f8e2b9f691243820 Mon Sep 17 00:00:00 2001
From: Adam Johnson <me@adamj.eu>
Date: Mon, 22 Jan 2024 13:21:13 +0000
Subject: [PATCH] [3.2.x] Fixed CVE-2024-24680 -- Mitigated potential DoS
in
intcomma template filter.
Thanks Seokchan Yoon for the report.
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Co-authored-by: Shai Berger <shai@platonix.com>
---
.../contrib/humanize/templatetags/humanize.py | 13 +-
tests/humanize_tests/tests.py | 140 ++++++++++++++++--
2 files changed, 135 insertions(+), 18 deletions(-)
diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py
index 194c7e8..98ba276 100644
--- a/django/contrib/humanize/templatetags/humanize.py
+++ b/django/contrib/humanize/templatetags/humanize.py
@@ -71,12 +71,13 @@ def intcomma(value, use_l10n=True):
return intcomma(value, False)
else:
return number_format(value, force_grouping=True)
- orig = str(value)
- new = re.sub(r"^(-?\d+)(\d{3})", r'\g<1>,\g<2>', orig)
- if orig == new:
- return new
- else:
- return intcomma(new, use_l10n)
+ result = str(value)
+ match = re.match(r"-?\d+", result)
+ if match:
+ prefix = match[0]
+ prefix_with_commas = re.sub(r"\d{3}", r"\g<0>,", prefix[::-1])[::-1]
+ result = prefix_with_commas + result[len(prefix) :]
+ return result
# A tuple of standard large number to their converters
diff --git a/tests/humanize_tests/tests.py b/tests/humanize_tests/tests.py
index 16e8fa6..e047330 100644
--- a/tests/humanize_tests/tests.py
+++ b/tests/humanize_tests/tests.py
@@ -62,28 +62,144 @@ class HumanizeTests(SimpleTestCase):
def test_intcomma(self):
test_list = (
- 100, 1000, 10123, 10311, 1000000, 1234567.25, '100', '1000',
- '10123', '10311', '1000000', '1234567.1234567',
- Decimal('1234567.1234567'), None,
+ 100,
+ -100,
+ 1000,
+ -1000,
+ 10123,
+ -10123,
+ 10311,
+ -10311,
+ 1000000,
+ -1000000,
+ 1234567.25,
+ -1234567.25,
+ "100",
+ "-100",
+ "1000",
+ "-1000",
+ "10123",
+ "-10123",
+ "10311",
+ "-10311",
+ "1000000",
+ "-1000000",
+ "1234567.1234567",
+ "-1234567.1234567",
+ Decimal("1234567.1234567"),
+ Decimal("-1234567.1234567"),
+ None,
+ "",
+ "-",
+ ".",
+ "-.",
+ "the quick brown fox jumped over the lazy dog",
)
result_list = (
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
- '1,234,567.1234567', None,
+ "100",
+ "-100",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.25",
+ "-1,234,567.25",
+ "100",
+ "-100",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ None,
+ "1,234,567",
+ "-1,234,567",
+ ",,.",
+ "-,,.",
+ "the quick brown fox jumped over the lazy dog",
)
with translation.override('en'):
self.humanize_tester(test_list, result_list, 'intcomma')
def test_l10n_intcomma(self):
test_list = (
- 100, 1000, 10123, 10311, 1000000, 1234567.25, '100', '1000',
- '10123', '10311', '1000000', '1234567.1234567',
- Decimal('1234567.1234567'), None,
+ 100,
+ -100,
+ 1000,
+ -1000,
+ 10123,
+ -10123,
+ 10311,
+ -10311,
+ 1000000,
+ -1000000,
+ 1234567.25,
+ -1234567.25,
+ "100",
+ "-100",
+ "1000",
+ "-1000",
+ "10123",
+ "-10123",
+ "10311",
+ "-10311",
+ "1000000",
+ "-1000000",
+ "1234567.1234567",
+ "-1234567.1234567",
+ Decimal("1234567.1234567"),
+ -Decimal("1234567.1234567"),
+ None,
+ "",
+ "-",
+ ".",
+ "-.",
+ "the quick brown fox jumped over the lazy dog",
)
result_list = (
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
- '1,234,567.1234567', None,
+ "100",
+ "-100",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.25",
+ "-1,234,567.25",
+ "100",
+ "-100",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ None,
+ "1,234,567",
+ "-1,234,567",
+ ",,.",
+ "-,,.",
+ "the quick brown fox jumped over the lazy dog",
)
with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=False):
with translation.override('en'):
--
2.33.0

View File

@ -1,122 +0,0 @@
From 072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521 Mon Sep 17 00:00:00 2001
From: Shai Berger <shai@platonix.com>
Date: Mon, 19 Feb 2024 13:56:37 +0100
Subject: [PATCH] [3.2.x] Fixed CVE-2024-27351 -- Prevented potential ReDoS in
Truncator.words().
Thanks Seokchan Yoon for the report.
Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
---
django/utils/text.py | 57 ++++++++++++++++++++++++++++++++--
tests/utils_tests/test_text.py | 26 ++++++++++++++++
2 files changed, 81 insertions(+), 2 deletions(-)
diff --git a/django/utils/text.py b/django/utils/text.py
index 06a377b..6631a00 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -15,8 +15,61 @@ def capfirst(x):
return x and str(x)[0].upper() + str(x)[1:]
-# Set up regular expressions
-re_words = re.compile(r'<[^>]+?>|([^<>\s]+)', re.S)
+# ----- Begin security-related performance workaround -----
+
+# We used to have, below
+#
+# re_words = 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 = re.compile(r"([^<>\s]+)", re.S)
+re_prt = 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 = re.compile(r'<[^>]+?>|(.)', re.S)
re_tag = re.compile(r'<(/)?(\S+?)(?:(\s*/)|\s.*?)?>', re.S)
re_newlines = re.compile(r'\r\n|\r') # Used in normalize_newlines
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
index cb3063d..7e9f2b3 100644
--- a/tests/utils_tests/test_text.py
+++ b/tests/utils_tests/test_text.py
@@ -156,6 +156,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
--
2.33.0

View File

@ -1,154 +0,0 @@
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

View File

@ -1,77 +0,0 @@
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

View File

@ -1,147 +0,0 @@
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

View File

@ -1,195 +0,0 @@
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

View File

@ -1,76 +0,0 @@
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:

View File

@ -1,61 +0,0 @@
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):

View File

@ -1,98 +0,0 @@
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",

View File

@ -1,84 +0,0 @@
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.

View File

@ -1,133 +0,0 @@
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):

View File

@ -1,133 +0,0 @@
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,91 +0,0 @@
From 790eb058b0716c536a2f2e8d1c6d5079d776c22b Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Wed, 13 Nov 2024 15:06:23 +0100
Subject: [PATCH] [4.2.x] Fixed CVE-2024-53907 -- Mitigated potential DoS in
strip_tags().
Origin: https://github.com/django/django/commit/790eb058b0716c536a2f2e8d1c6d5079d776c22b
Thanks to jiangniao for the report, and Shai Berger and Natalia Bidart
for the reviews.
---
django/utils/html.py | 10 ++++++++--
tests/utils_tests/test_html.py | 7 +++++++
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/django/utils/html.py b/django/utils/html.py
index b3a6122..a8d23ce 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -12,6 +12,7 @@ from django.utils.functional import Promise, keep_lazy, keep_lazy_text
from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS
from django.utils.safestring import SafeData, SafeText, mark_safe
from django.utils.text import normalize_newlines
+from django.core.exceptions import SuspiciousOperation
# Configuration for urlize() function.
TRAILING_PUNCTUATION_CHARS = '.,:;!'
@@ -34,6 +35,7 @@ _html_escapes = {
}
MAX_URL_LENGTH = 2048
+MAX_STRIP_TAGS_DEPTH = 50
@keep_lazy(str, SafeText)
@@ -185,15 +187,19 @@ def _strip_once(value):
@keep_lazy_text
def strip_tags(value):
"""Return the given HTML with all tags stripped."""
- # Note: in typical case this loop executes _strip_once once. Loop condition
- # is redundant, but helps to reduce number of executions of _strip_once.
value = str(value)
+ # Note: in typical case this loop executes _strip_once twice (the second
+ # execution does not remove any more tags).
+ strip_tags_depth = 0
while '<' in value and '>' in value:
+ if strip_tags_depth >= MAX_STRIP_TAGS_DEPTH:
+ raise SuspiciousOperation
new_value = _strip_once(value)
if value.count('<') == new_value.count('<'):
# _strip_once wasn't able to detect more tags.
break
value = new_value
+ strip_tags_depth += 1
return value
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index fdabb63..625511e 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -8,6 +8,7 @@ from django.utils.html import (
linebreaks, smart_urlquote, strip_spaces_between_tags, strip_tags, urlize,
)
from django.utils.safestring import mark_safe
+from django.core.exceptions import SuspiciousOperation
class TestUtilsHtml(SimpleTestCase):
@@ -90,12 +91,18 @@ class TestUtilsHtml(SimpleTestCase):
('<script>alert()</script>&h', 'alert()h'),
('><!' + ('&' * 16000) + 'D', '><!' + ('&' * 16000) + 'D'),
('X<<<<br>br>br>br>X', 'XX'),
+ ("<" * 50 + "a>" * 50, ""),
)
for value, output in items:
with self.subTest(value=value, output=output):
self.check_output(strip_tags, value, output)
self.check_output(strip_tags, lazystr(value), output)
+ def test_strip_tags_suspicious_operation(self):
+ value = "<" * 51 + "a>" * 51, "<a>"
+ with self.assertRaises(SuspiciousOperation):
+ strip_tags(value)
+
def test_strip_tags_files(self):
# Test with more lengthy content (also catching performance regressions)
for filename in ('strip_tags1.html', 'strip_tags2.txt'):
--
2.33.0

View File

@ -1,293 +0,0 @@
From ad866a1ca3e7d60da888d25d27e46a8adb2ed36e Mon Sep 17 00:00:00 2001
From: Natalia <124304+nessita@users.noreply.github.com>
Date: Mon, 6 Jan 2025 15:51:45 -0300
Subject: [PATCH] [4.2.x] Fixed CVE-2024-56374 -- Mitigated potential DoS in
IPv6 validation.
Thanks Saravana Kumar for the report, and Sarah Boyce and Mariusz
Felisiak for the reviews.
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
---
django/db/models/fields/__init__.py | 6 +--
django/forms/fields.py | 7 +++-
django/utils/ipv6.py | 19 +++++++--
docs/ref/forms/fields.txt | 13 +++++-
docs/releases/4.2.18.txt | 12 ++++++
.../field_tests/test_genericipaddressfield.py | 33 ++++++++++++++-
tests/utils_tests/test_ipv6.py | 40 +++++++++++++++++--
7 files changed, 116 insertions(+), 14 deletions(-)
Index: Django-2.2.28/django/db/models/fields/__init__.py
===================================================================
--- Django-2.2.28.orig/django/db/models/fields/__init__.py
+++ Django-2.2.28/django/db/models/fields/__init__.py
@@ -26,7 +26,7 @@ from django.utils.dateparse import (
)
from django.utils.duration import duration_microseconds, duration_string
from django.utils.functional import Promise, cached_property
-from django.utils.ipv6 import clean_ipv6_address
+from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address
from django.utils.itercompat import is_iterable
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
@@ -1904,7 +1904,7 @@ class GenericIPAddressField(Field):
self.default_validators, invalid_error_message = \
validators.ip_address_validators(protocol, unpack_ipv4)
self.default_error_messages['invalid'] = invalid_error_message
- kwargs['max_length'] = 39
+ kwargs['max_length'] = MAX_IPV6_ADDRESS_LENGTH
super().__init__(verbose_name, name, *args, **kwargs)
def check(self, **kwargs):
@@ -1931,7 +1931,7 @@ class GenericIPAddressField(Field):
kwargs['unpack_ipv4'] = self.unpack_ipv4
if self.protocol != "both":
kwargs['protocol'] = self.protocol
- if kwargs.get("max_length") == 39:
+ if kwargs.get("max_length") == self.max_length:
del kwargs['max_length']
return name, path, args, kwargs
Index: Django-2.2.28/django/forms/fields.py
===================================================================
--- Django-2.2.28.orig/django/forms/fields.py
+++ Django-2.2.28/django/forms/fields.py
@@ -29,7 +29,7 @@ from django.forms.widgets import (
from django.utils import formats
from django.utils.dateparse import parse_duration
from django.utils.duration import duration_string
-from django.utils.ipv6 import clean_ipv6_address
+from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address
from django.utils.translation import gettext_lazy as _, ngettext_lazy
__all__ = (
@@ -1162,6 +1162,7 @@ class GenericIPAddressField(CharField):
def __init__(self, *, protocol='both', unpack_ipv4=False, **kwargs):
self.unpack_ipv4 = unpack_ipv4
self.default_validators = validators.ip_address_validators(protocol, unpack_ipv4)[0]
+ kwargs.setdefault("max_length", MAX_IPV6_ADDRESS_LENGTH)
super().__init__(**kwargs)
def to_python(self, value):
@@ -1169,7 +1170,9 @@ class GenericIPAddressField(CharField):
return ''
value = value.strip()
if value and ':' in value:
- return clean_ipv6_address(value, self.unpack_ipv4)
+ return clean_ipv6_address(
+ value, self.unpack_ipv4, max_length=self.max_length
+ )
return value
Index: Django-2.2.28/django/utils/ipv6.py
===================================================================
--- Django-2.2.28.orig/django/utils/ipv6.py
+++ Django-2.2.28/django/utils/ipv6.py
@@ -3,9 +3,23 @@ import ipaddress
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
+MAX_IPV6_ADDRESS_LENGTH = 39
-def clean_ipv6_address(ip_str, unpack_ipv4=False,
- error_message=_("This is not a valid IPv6 address.")):
+
+def _ipv6_address_from_str(ip_str, max_length=MAX_IPV6_ADDRESS_LENGTH):
+ if len(ip_str) > max_length:
+ raise ValueError(
+ f"Unable to convert {ip_str} to an IPv6 address (value too long)."
+ )
+ return ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str)))
+
+
+def clean_ipv6_address(
+ ip_str,
+ unpack_ipv4=False,
+ error_message=_("This is not a valid IPv6 address."),
+ max_length=MAX_IPV6_ADDRESS_LENGTH,
+ ):
"""
Clean an IPv6 address string.
@@ -23,7 +37,7 @@ def clean_ipv6_address(ip_str, unpack_ip
Return a compressed IPv6 address or the same value.
"""
try:
- addr = ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str)))
+ addr = _ipv6_address_from_str(ip_str, max_length)
except ValueError:
raise ValidationError(error_message, code='invalid')
@@ -40,7 +54,7 @@ def is_valid_ipv6_address(ip_str):
Return whether or not the `ip_str` string is a valid IPv6 address.
"""
try:
- ipaddress.IPv6Address(ip_str)
+ _ipv6_address_from_str(ip_str)
except ValueError:
return False
return True
Index: Django-2.2.28/docs/ref/forms/fields.txt
===================================================================
--- Django-2.2.28.orig/docs/ref/forms/fields.txt
+++ Django-2.2.28/docs/ref/forms/fields.txt
@@ -787,7 +787,7 @@ For each field, we describe the default
* Empty value: ``''`` (an empty string)
* Normalizes to: A string. IPv6 addresses are normalized as described below.
* Validates that the given value is a valid IP address.
- * Error message keys: ``required``, ``invalid``
+ * Error message keys: ``required``, ``invalid``, ``max_length``
The IPv6 address normalization follows :rfc:`4291#section-2.2` section 2.2,
including using the IPv4 format suggested in paragraph 3 of that section, like
@@ -795,7 +795,7 @@ For each field, we describe the default
``2001::1``, and ``::ffff:0a0a:0a0a`` to ``::ffff:10.10.10.10``. All characters
are converted to lowercase.
- Takes two optional arguments:
+ Takes three optional arguments:
.. attribute:: protocol
@@ -810,6 +810,15 @@ For each field, we describe the default
``192.0.2.1``. Default is disabled. Can only be used
when ``protocol`` is set to ``'both'``.
+ .. attribute:: max_length
+
+ Defaults to 39, and behaves the same way as it does for
+ :class:`CharField`.
+
+ .. versionchanged:: 4.2.18
+
+ The default value for ``max_length`` was set to 39 characters.
+
``MultipleChoiceField``
-----------------------
Index: Django-2.2.28/tests/forms_tests/field_tests/test_genericipaddressfield.py
===================================================================
--- Django-2.2.28.orig/tests/forms_tests/field_tests/test_genericipaddressfield.py
+++ Django-2.2.28/tests/forms_tests/field_tests/test_genericipaddressfield.py
@@ -1,5 +1,6 @@
from django.forms import GenericIPAddressField, ValidationError
from django.test import SimpleTestCase
+from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH
class GenericIPAddressFieldTest(SimpleTestCase):
@@ -28,7 +29,7 @@ class GenericIPAddressFieldTest(SimpleTe
with self.assertRaisesMessage(ValidationError, "'Enter a valid IPv4 or IPv6 address.'"):
f.clean('256.125.1.5')
self.assertEqual(f.clean(' fe80::223:6cff:fe8a:2e8a '), 'fe80::223:6cff:fe8a:2e8a')
- self.assertEqual(f.clean(' 2a02::223:6cff:fe8a:2e8a '), '2a02::223:6cff:fe8a:2e8a')
+ self.assertEqual(f.clean(' ' * MAX_IPV6_ADDRESS_LENGTH + ' 2a02::223:6cff:fe8a:2e8a '), '2a02::223:6cff:fe8a:2e8a')
with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"):
f.clean('12345:2:3:4')
with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"):
@@ -89,6 +90,35 @@ class GenericIPAddressFieldTest(SimpleTe
with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"):
f.clean('1:2')
+ def test_generic_ipaddress_max_length_custom(self):
+ # Valid IPv4-mapped IPv6 address, len 45.
+ addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228"
+ f = GenericIPAddressField(max_length=len(addr))
+ f.clean(addr)
+
+ def test_generic_ipaddress_max_length_validation_error(self):
+ # Valid IPv4-mapped IPv6 address, len 45.
+ addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228"
+
+ cases = [
+ ({}, MAX_IPV6_ADDRESS_LENGTH), # Default value.
+ ({"max_length": len(addr) - 1}, len(addr) - 1),
+ ]
+ for kwargs, max_length in cases:
+ max_length_plus_one = max_length + 1
+ msg = (
+ f"Ensure this value has at most {max_length} characters (it has "
+ f"{max_length_plus_one}).'"
+ )
+ with self.subTest(max_length=max_length):
+ f = GenericIPAddressField(**kwargs)
+ with self.assertRaisesMessage(ValidationError, msg):
+ f.clean("x" * max_length_plus_one)
+ with self.assertRaisesMessage(
+ ValidationError, "This is not a valid IPv6 address."
+ ):
+ f.clean(addr)
+
def test_generic_ipaddress_as_generic_not_required(self):
f = GenericIPAddressField(required=False)
self.assertEqual(f.clean(''), '')
Index: Django-2.2.28/tests/utils_tests/test_ipv6.py
===================================================================
--- Django-2.2.28.orig/tests/utils_tests/test_ipv6.py
+++ Django-2.2.28/tests/utils_tests/test_ipv6.py
@@ -1,9 +1,16 @@
-import unittest
+import traceback
+from io import StringIO
-from django.utils.ipv6 import clean_ipv6_address, is_valid_ipv6_address
+from django.core.exceptions import ValidationError
+from django.test import SimpleTestCase
+from django.utils.ipv6 import (
+ MAX_IPV6_ADDRESS_LENGTH,
+ clean_ipv6_address,
+ is_valid_ipv6_address,
+)
+from django.utils.version import PY310
-
-class TestUtilsIPv6(unittest.TestCase):
+class TestUtilsIPv6(SimpleTestCase):
def test_validates_correct_plain_address(self):
self.assertTrue(is_valid_ipv6_address('fe80::223:6cff:fe8a:2e8a'))
@@ -55,3 +62,29 @@ class TestUtilsIPv6(unittest.TestCase):
self.assertEqual(clean_ipv6_address('::ffff:0a0a:0a0a', unpack_ipv4=True), '10.10.10.10')
self.assertEqual(clean_ipv6_address('::ffff:1234:1234', unpack_ipv4=True), '18.52.18.52')
self.assertEqual(clean_ipv6_address('::ffff:18.52.18.52', unpack_ipv4=True), '18.52.18.52')
+
+ def test_address_too_long(self):
+ addresses = [
+ "0000:0000:0000:0000:0000:ffff:192.168.100.228", # IPv4-mapped IPv6 address
+ "0000:0000:0000:0000:0000:ffff:192.168.100.228%123456", # % scope/zone
+ "fe80::223:6cff:fe8a:2e8a:1234:5678:00000", # MAX_IPV6_ADDRESS_LENGTH + 1
+ ]
+ msg = "This is the error message."
+ value_error_msg = "Unable to convert %s to an IPv6 address (value too long)."
+ for addr in addresses:
+ with self.subTest(addr=addr):
+ self.assertGreater(len(addr), MAX_IPV6_ADDRESS_LENGTH)
+ self.assertEqual(is_valid_ipv6_address(addr), False)
+ with self.assertRaisesMessage(ValidationError, msg) as ctx:
+ clean_ipv6_address(addr, error_message=msg)
+ exception_traceback = StringIO()
+ if PY310:
+ traceback.print_exception(ctx.exception, file=exception_traceback)
+ else:
+ traceback.print_exception(
+ type(ctx.exception),
+ value=ctx.exception,
+ tb=ctx.exception.__traceback__,
+ file=exception_traceback,
+ )
+ self.assertIn(value_error_msg % addr, exception_traceback.getvalue())
Index: Django-2.2.28/django/utils/version.py
===================================================================
--- Django-2.2.28.orig/django/utils/version.py
+++ Django-2.2.28/django/utils/version.py
@@ -13,6 +13,7 @@ PY36 = sys.version_info >= (3, 6)
PY37 = sys.version_info >= (3, 7)
PY38 = sys.version_info >= (3, 8)
PY39 = sys.version_info >= (3, 9)
+PY310 = sys.version_info >= (3, 10)
def get_version(version=None):

View File

@ -1,7 +1,7 @@
%global _empty_manifest_terminate_build 0
Name: python-django
Version: 2.2.27
Release: 14
Release: 9
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/
@ -21,23 +21,6 @@ Patch6: CVE-2023-41164.patch
Patch7: CVE-2023-43665.patch
# https://github.com/django/django/commit/f9a7fb8466a7ba4857eaf930099b5258f3eafb2b
Patch8: CVE-2023-46695.patch
# https://github.com/django/django/commit/c1171ffbd570db90ca206c30f8e2b9f691243820
Patch9: CVE-2024-24680.patch
# https://github.com/django/django/commit/072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521
Patch10: CVE-2024-27351.patch
# 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
Patch21: CVE-2024-53907.patch
Patch22: CVE-2024-56374.patch
BuildArch: noarch
%description
@ -104,22 +87,6 @@ mv %{buildroot}/doclist.lst .
%{_docdir}/*
%changelog
* Fri Jan 17 2025 caodongxia <caodongxia@h-partners.com> - 2.2.27-14
- Fix CVE-2024-56374
* Mon Dec 09 2024 wangkai <13474090681@163.com> - 2.2.27-13
- Fix CVE-2024-53907
* 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
* Wed Feb 07 2024 yaoxin <yao_xin001@hoperun.com> - 2.2.27-10
- Fix CVE-2024-24680
* Mon Nov 06 2023 yaoxin <yao_xin001@hoperun.com> - 2.2.27-9
- Fix CVE-2023-46695