Compare commits
10 Commits
02439efa1b
...
48ae12b320
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48ae12b320 | ||
|
|
de59fad5e9 | ||
|
|
a0c2947aae | ||
|
|
91e4a39b6a | ||
|
|
8553f6f573 | ||
|
|
b92a28148a | ||
|
|
fc8e6abbbd | ||
|
|
f2bf9ce08d | ||
|
|
790df9a5e3 | ||
|
|
63002bf52b |
205
CVE-2024-24680.patch
Normal file
205
CVE-2024-24680.patch
Normal file
@ -0,0 +1,205 @@
|
||||
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,
|
||||
+ "1234567",
|
||||
+ "-1234567",
|
||||
+ "1234567.12",
|
||||
+ "-1234567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
result_list = (
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
|
||||
- '1,234,567.1234567', None,
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.25",
|
||||
+ "-1,234,567.25",
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ None,
|
||||
+ "1,234,567",
|
||||
+ "-1,234,567",
|
||||
+ "1,234,567.12",
|
||||
+ "-1,234,567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
with translation.override('en'):
|
||||
self.humanize_tester(test_list, result_list, 'intcomma')
|
||||
|
||||
def test_l10n_intcomma(self):
|
||||
test_list = (
|
||||
- 100, 1000, 10123, 10311, 1000000, 1234567.25, '100', '1000',
|
||||
- '10123', '10311', '1000000', '1234567.1234567',
|
||||
- Decimal('1234567.1234567'), None,
|
||||
+ 100,
|
||||
+ -100,
|
||||
+ 1000,
|
||||
+ -1000,
|
||||
+ 10123,
|
||||
+ -10123,
|
||||
+ 10311,
|
||||
+ -10311,
|
||||
+ 1000000,
|
||||
+ -1000000,
|
||||
+ 1234567.25,
|
||||
+ -1234567.25,
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1000",
|
||||
+ "-1000",
|
||||
+ "10123",
|
||||
+ "-10123",
|
||||
+ "10311",
|
||||
+ "-10311",
|
||||
+ "1000000",
|
||||
+ "-1000000",
|
||||
+ "1234567.1234567",
|
||||
+ "-1234567.1234567",
|
||||
+ Decimal("1234567.1234567"),
|
||||
+ -Decimal("1234567.1234567"),
|
||||
+ None,
|
||||
+ "1234567",
|
||||
+ "-1234567",
|
||||
+ "1234567.12",
|
||||
+ "-1234567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
result_list = (
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
|
||||
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
|
||||
- '1,234,567.1234567', None,
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.25",
|
||||
+ "-1,234,567.25",
|
||||
+ "100",
|
||||
+ "-100",
|
||||
+ "1,000",
|
||||
+ "-1,000",
|
||||
+ "10,123",
|
||||
+ "-10,123",
|
||||
+ "10,311",
|
||||
+ "-10,311",
|
||||
+ "1,000,000",
|
||||
+ "-1,000,000",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ "1,234,567.1234567",
|
||||
+ "-1,234,567.1234567",
|
||||
+ None,
|
||||
+ "1,234,567",
|
||||
+ "-1,234,567",
|
||||
+ "1,234,567.12",
|
||||
+ "-1,234,567.12",
|
||||
+ "the quick brown fox jumped over the lazy dog",
|
||||
)
|
||||
with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=False):
|
||||
with translation.override('en'):
|
||||
--
|
||||
2.33.0
|
||||
|
||||
122
CVE-2024-27351.patch
Normal file
122
CVE-2024-27351.patch
Normal file
@ -0,0 +1,122 @@
|
||||
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 <3 python, what about you?</p>')
|
||||
self.assertEqual('<p>I <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
|
||||
|
||||
154
CVE-2024-38875.patch
Normal file
154
CVE-2024-38875.patch
Normal file
@ -0,0 +1,154 @@
|
||||
From 8623260fb0949d368376a128bee2189ec0a67ae5 Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Mon, 22 Jul 2024 09:43:08 +0200
|
||||
Subject: [PATCH] [PATCH] [4.2.x] Fixed CVE-2024-38875 -- Mitigated potential
|
||||
DoS in urlize and urlizetrunc template filters.
|
||||
|
||||
---
|
||||
django/utils/html.py | 72 +++++++++++++++++++++++++++++-----
|
||||
tests/utils_tests/test_html.py | 21 ++++++----
|
||||
2 files changed, 75 insertions(+), 17 deletions(-)
|
||||
|
||||
diff --git a/django/utils/html.py b/django/utils/html.py
|
||||
index 7a33d5f68d..1dbe39ccd1 100644
|
||||
--- a/django/utils/html.py
|
||||
+++ b/django/utils/html.py
|
||||
@@ -1,5 +1,6 @@
|
||||
"""HTML utilities suitable for global use."""
|
||||
|
||||
+import html
|
||||
import json
|
||||
import re
|
||||
from html.parser import HTMLParser
|
||||
@@ -235,6 +235,16 @@ def smart_urlquote(url):
|
||||
return urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
+class CountsDict(dict):
|
||||
+ def __init__(self, *args, word, **kwargs):
|
||||
+ super().__init__(*args, *kwargs)
|
||||
+ self.word = word
|
||||
+
|
||||
+ def __missing__(self, key):
|
||||
+ self[key] = self.word.count(key)
|
||||
+ return self[key]
|
||||
+
|
||||
+
|
||||
@keep_lazy_text
|
||||
def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
"""
|
||||
@@ -259,6 +269,15 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
return x
|
||||
return '%s…' % x[:max(0, limit - 1)]
|
||||
|
||||
+ def wrapping_punctuation_openings():
|
||||
+ return "".join(dict(WRAPPING_PUNCTUATION).keys())
|
||||
+
|
||||
+ def trailing_punctuation_chars_no_semicolon():
|
||||
+ return TRAILING_PUNCTUATION_CHARS.replace(";", "")
|
||||
+
|
||||
+ def trailing_punctuation_chars_has_semicolon():
|
||||
+ return ";" in TRAILING_PUNCTUATION_CHARS
|
||||
+
|
||||
def unescape(text):
|
||||
"""
|
||||
If input URL is HTML-escaped, unescape it so that it can be safely fed
|
||||
@@ -273,21 +292,53 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
Trim trailing and wrapping punctuation from `middle`. Return the items
|
||||
of the new state.
|
||||
"""
|
||||
+ # Strip all opening wrapping punctuation.
|
||||
+ middle = word.lstrip(wrapping_punctuation_openings())
|
||||
+ lead = word[: len(word) - len(middle)]
|
||||
+ trail = ""
|
||||
+
|
||||
# Continue trimming until middle remains unchanged.
|
||||
trimmed_something = True
|
||||
- while trimmed_something:
|
||||
+ counts = CountsDict(word=middle)
|
||||
+ while trimmed_something and middle:
|
||||
trimmed_something = False
|
||||
# Trim wrapping punctuation.
|
||||
for opening, closing in WRAPPING_PUNCTUATION:
|
||||
- if middle.startswith(opening):
|
||||
- middle = middle[len(opening):]
|
||||
- lead += opening
|
||||
- trimmed_something = True
|
||||
- # Keep parentheses at the end only if they're balanced.
|
||||
- if (middle.endswith(closing) and
|
||||
- middle.count(closing) == middle.count(opening) + 1):
|
||||
- middle = middle[:-len(closing)]
|
||||
- trail = closing + trail
|
||||
+ if counts[opening] < counts[closing]:
|
||||
+ rstripped = middle.rstrip(closing)
|
||||
+ if rstripped != middle:
|
||||
+ strip = counts[closing] - counts[opening]
|
||||
+ trail = middle[-strip:]
|
||||
+ middle = middle[:-strip]
|
||||
+ trimmed_something = True
|
||||
+ counts[closing] -= strip
|
||||
+
|
||||
+ rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
|
||||
+ if rstripped != middle:
|
||||
+ trail = middle[len(rstripped) :] + trail
|
||||
+ middle = rstripped
|
||||
+ trimmed_something = True
|
||||
+
|
||||
+ if trailing_punctuation_chars_has_semicolon() and middle.endswith(";"):
|
||||
+ # Only strip if not part of an HTML entity.
|
||||
+ amp = middle.rfind("&")
|
||||
+ if amp == -1:
|
||||
+ can_strip = True
|
||||
+ else:
|
||||
+ potential_entity = middle[amp:]
|
||||
+ escaped = html.unescape(potential_entity)
|
||||
+ can_strip = (escaped == potential_entity) or escaped.endswith(";")
|
||||
+
|
||||
+ if can_strip:
|
||||
+ rstripped = middle.rstrip(";")
|
||||
+ amount_stripped = len(middle) - len(rstripped)
|
||||
+ if amp > -1 and amount_stripped > 1:
|
||||
+ # Leave a trailing semicolon as might be an entity.
|
||||
+ trail = middle[len(rstripped) + 1 :] + trail
|
||||
+ middle = rstripped + ";"
|
||||
+ else:
|
||||
+ trail = middle[len(rstripped) :] + trail
|
||||
+ middle = rstripped
|
||||
trimmed_something = True
|
||||
# Trim trailing punctuation (after trimming wrapping punctuation,
|
||||
# as encoded entities contain ';'). Unescape entites to avoid
|
||||
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
|
||||
index 5cc2d9b95d..dc89009b63 100644
|
||||
--- a/tests/utils_tests/test_html.py
|
||||
+++ b/tests/utils_tests/test_html.py
|
||||
@@ -260,13 +260,20 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
|
||||
def test_urlize_unchanged_inputs(self):
|
||||
tests = (
|
||||
- ('a' + '@a' * 50000) + 'a', # simple_email_re catastrophic test
|
||||
- ('a' + '.' * 1000000) + 'a', # trailing_punctuation catastrophic test
|
||||
- 'foo@',
|
||||
- '@foo.com',
|
||||
- 'foo@.example.com',
|
||||
- 'foo@localhost',
|
||||
- 'foo@localhost.',
|
||||
+ ("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
|
||||
+ ("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
|
||||
+ "foo@",
|
||||
+ "@foo.com",
|
||||
+ "foo@.example.com",
|
||||
+ "foo@localhost",
|
||||
+ "foo@localhost.",
|
||||
+ # trim_punctuation catastrophic tests
|
||||
+ "(" * 100_000 + ":" + ")" * 100_000,
|
||||
+ "(" * 100_000 + "&:" + ")" * 100_000,
|
||||
+ "([" * 100_000 + ":" + "])" * 100_000,
|
||||
+ "[(" * 100_000 + ":" + ")]" * 100_000,
|
||||
+ "([[" * 100_000 + ":" + "]])" * 100_000,
|
||||
+ "&:" + ";" * 100_000,
|
||||
)
|
||||
for value in tests:
|
||||
with self.subTest(value=value):
|
||||
--
|
||||
2.45.2
|
||||
|
||||
77
CVE-2024-39329.patch
Normal file
77
CVE-2024-39329.patch
Normal file
@ -0,0 +1,77 @@
|
||||
From c676b05c98df58252509dd5ad16b959c351ac770 Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Fri, 26 Jul 2024 14:01:36 +0200
|
||||
Subject: [PATCH] Fixed CVE-2024-39329 -- Standarized timing of
|
||||
verify_password() when checking unusuable passwords.
|
||||
|
||||
Refs #20760.
|
||||
|
||||
Thanks Michael Manfre for the fix and to Adam Johnson for the review.
|
||||
---
|
||||
django/contrib/auth/hashers.py | 10 ++++++++--
|
||||
tests/auth_tests/test_hashers.py | 22 ++++++++++++++++++++++
|
||||
2 files changed, 30 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
|
||||
index 1e8d7547fc..af55e57389 100644
|
||||
--- a/django/contrib/auth/hashers.py
|
||||
+++ b/django/contrib/auth/hashers.py
|
||||
@@ -36,14 +36,20 @@ def check_password(password, encoded, setter=None, preferred='default'):
|
||||
If setter is specified, it'll be called when you need to
|
||||
regenerate the password.
|
||||
"""
|
||||
- if password is None or not is_password_usable(encoded):
|
||||
- return False
|
||||
+ fake_runtime = password is None or not is_password_usable(encoded)
|
||||
|
||||
preferred = get_hasher(preferred)
|
||||
try:
|
||||
hasher = identify_hasher(encoded)
|
||||
except ValueError:
|
||||
# encoded is gibberish or uses a hasher that's no longer installed.
|
||||
+ fake_runtime = True
|
||||
+
|
||||
+ if fake_runtime:
|
||||
+ # Run the default password hasher once to reduce the timing difference
|
||||
+ # between an existing user with an unusable password and a nonexistent
|
||||
+ # user or missing hasher (similar to #20760).
|
||||
+ make_password(get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH))
|
||||
return False
|
||||
|
||||
hasher_changed = hasher.algorithm != preferred.algorithm
|
||||
diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py
|
||||
index ee6441b237..170bd07c4b 100644
|
||||
--- a/tests/auth_tests/test_hashers.py
|
||||
+++ b/tests/auth_tests/test_hashers.py
|
||||
@@ -433,6 +433,28 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||
check_password('wrong_password', encoded)
|
||||
self.assertEqual(hasher.harden_runtime.call_count, 1)
|
||||
|
||||
+ def test_check_password_calls_make_password_to_fake_runtime(self):
|
||||
+ hasher = get_hasher("default")
|
||||
+ cases = [
|
||||
+ (None, None, None), # no plain text password provided
|
||||
+ ("foo", make_password(password=None), None), # unusable encoded
|
||||
+ ("letmein", make_password(password="letmein"), ValueError), # valid encoded
|
||||
+ ]
|
||||
+ for password, encoded, hasher_side_effect in cases:
|
||||
+ with self.subTest(encoded=encoded):
|
||||
+ with mock.patch("django.contrib.auth.hashers.identify_hasher", side_effect=hasher_side_effect) as mock_identify_hasher:
|
||||
+ with mock.patch("django.contrib.auth.hashers.make_password") as mock_make_password:
|
||||
+ with mock.patch("django.contrib.auth.hashers.get_random_string", side_effect=lambda size: "x" * size):
|
||||
+ with mock.patch.object(hasher, "verify"):
|
||||
+ # Ensure make_password is called to standardize timing.
|
||||
+ check_password(password, encoded)
|
||||
+ self.assertEqual(hasher.verify.call_count, 0)
|
||||
+ self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)])
|
||||
+ self.assertEqual(
|
||||
+ mock_make_password.mock_calls,
|
||||
+ [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)],
|
||||
+ )
|
||||
+
|
||||
|
||||
class BasePasswordHasherTests(SimpleTestCase):
|
||||
not_implemented_msg = 'subclasses of BasePasswordHasher must provide %s() method'
|
||||
--
|
||||
2.45.2
|
||||
|
||||
147
CVE-2024-39330.patch
Normal file
147
CVE-2024-39330.patch
Normal file
@ -0,0 +1,147 @@
|
||||
From 72af4b325aa9ffd96b18ef68d26ec2260e982c2a Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Wed, 24 Jul 2024 17:08:23 +0200
|
||||
Subject: [PATCH] Fixed CVE-2024-39330 -- Added extra file name validation in
|
||||
Storage's save method.
|
||||
|
||||
Thanks to Josh Schneier for the report, and to Carlton Gibson and Sarah
|
||||
Boyce for the reviews.
|
||||
---
|
||||
django/core/files/storage.py | 11 ++++++
|
||||
django/core/files/utils.py | 7 ++--
|
||||
tests/file_storage/test_base.py | 64 +++++++++++++++++++++++++++++++++
|
||||
tests/file_storage/tests.py | 6 ----
|
||||
4 files changed, 78 insertions(+), 10 deletions(-)
|
||||
create mode 100644 tests/file_storage/test_base.py
|
||||
|
||||
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
|
||||
index ea5bbc82d0..8c633ec040 100644
|
||||
--- a/django/core/files/storage.py
|
||||
+++ b/django/core/files/storage.py
|
||||
@@ -50,7 +50,18 @@ class Storage:
|
||||
if not hasattr(content, 'chunks'):
|
||||
content = File(content, name)
|
||||
|
||||
+ # Ensure that the name is valid, before and after having the storage
|
||||
+ # system potentially modifying the name. This duplicates the check made
|
||||
+ # inside `get_available_name` but it's necessary for those cases where
|
||||
+ # `get_available_name` is overriden and validation is lost.
|
||||
+ validate_file_name(name, allow_relative_path=True)
|
||||
+
|
||||
+ # Potentially find a different name depending on storage constraints.
|
||||
name = self.get_available_name(name, max_length=max_length)
|
||||
+ # Validate the (potentially) new name.
|
||||
+ validate_file_name(name, allow_relative_path=True)
|
||||
+
|
||||
+ # The save operation should return the actual name of the file saved.
|
||||
name = self._save(name, content)
|
||||
# Ensure that the name returned from the storage system is still valid.
|
||||
validate_file_name(name, allow_relative_path=True)
|
||||
diff --git a/django/core/files/utils.py b/django/core/files/utils.py
|
||||
index f28cea1077..a1fea44ded 100644
|
||||
--- a/django/core/files/utils.py
|
||||
+++ b/django/core/files/utils.py
|
||||
@@ -10,10 +10,9 @@ def validate_file_name(name, allow_relative_path=False):
|
||||
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
|
||||
|
||||
if allow_relative_path:
|
||||
- # Use PurePosixPath() because this branch is checked only in
|
||||
- # FileField.generate_filename() where all file paths are expected to be
|
||||
- # Unix style (with forward slashes).
|
||||
- path = pathlib.PurePosixPath(name)
|
||||
+ # Ensure that name can be treated as a pure posix path, i.e. Unix
|
||||
+ # style (with forward slashes).
|
||||
+ path = pathlib.PurePosixPath(str(name).replace("\\", "/"))
|
||||
if path.is_absolute() or '..' in path.parts:
|
||||
raise SuspiciousFileOperation(
|
||||
"Detected path traversal attempt in '%s'" % name
|
||||
diff --git a/tests/file_storage/test_base.py b/tests/file_storage/test_base.py
|
||||
new file mode 100644
|
||||
index 0000000000..7a0838f7a5
|
||||
--- /dev/null
|
||||
+++ b/tests/file_storage/test_base.py
|
||||
@@ -0,0 +1,64 @@
|
||||
+import os
|
||||
+from unittest import mock
|
||||
+
|
||||
+from django.core.exceptions import SuspiciousFileOperation
|
||||
+from django.core.files.storage import Storage
|
||||
+from django.test import SimpleTestCase
|
||||
+
|
||||
+
|
||||
+class CustomStorage(Storage):
|
||||
+ """Simple Storage subclass implementing the bare minimum for testing."""
|
||||
+
|
||||
+ def exists(self, name):
|
||||
+ return False
|
||||
+
|
||||
+ def _save(self, name):
|
||||
+ return name
|
||||
+
|
||||
+
|
||||
+class StorageValidateFileNameTests(SimpleTestCase):
|
||||
+ invalid_file_names = [
|
||||
+ os.path.join("path", "to", os.pardir, "test.file"),
|
||||
+ os.path.join(os.path.sep, "path", "to", "test.file"),
|
||||
+ ]
|
||||
+ error_msg = "Detected path traversal attempt in '%s'"
|
||||
+
|
||||
+ def test_validate_before_get_available_name(self):
|
||||
+ s = CustomStorage()
|
||||
+ # The initial name passed to `save` is not valid nor safe, fail early.
|
||||
+ for name in self.invalid_file_names:
|
||||
+ with self.subTest(name=name):
|
||||
+ with mock.patch.object(s, "get_available_name") as mock_get_available_names:
|
||||
+ with mock.patch.object(s, "_save") as mock_internal_save:
|
||||
+ with self.assertRaisesMessage(
|
||||
+ SuspiciousFileOperation, self.error_msg % name
|
||||
+ ):
|
||||
+ s.save(name, content="irrelevant")
|
||||
+ self.assertEqual(mock_get_available_names.mock_calls, [])
|
||||
+ self.assertEqual(mock_internal_save.mock_calls, [])
|
||||
+
|
||||
+ def test_validate_after_get_available_name(self):
|
||||
+ s = CustomStorage()
|
||||
+ # The initial name passed to `save` is valid and safe, but the returned
|
||||
+ # name from `get_available_name` is not.
|
||||
+ for name in self.invalid_file_names:
|
||||
+ with self.subTest(name=name):
|
||||
+ with mock.patch.object(s, "get_available_name", return_value=name):
|
||||
+ with mock.patch.object(s, "_save") as mock_internal_save:
|
||||
+ with self.assertRaisesMessage(
|
||||
+ SuspiciousFileOperation, self.error_msg % name
|
||||
+ ):
|
||||
+ s.save("valid-file-name.txt", content="irrelevant")
|
||||
+ self.assertEqual(mock_internal_save.mock_calls, [])
|
||||
+
|
||||
+ def test_validate_after_internal_save(self):
|
||||
+ s = CustomStorage()
|
||||
+ # The initial name passed to `save` is valid and safe, but the result
|
||||
+ # from `_save` is not (this is achieved by monkeypatching _save).
|
||||
+ for name in self.invalid_file_names:
|
||||
+ with self.subTest(name=name):
|
||||
+ with mock.patch.object(s, "_save", return_value=name):
|
||||
+ with self.assertRaisesMessage(
|
||||
+ SuspiciousFileOperation, self.error_msg % name
|
||||
+ ):
|
||||
+ s.save("valid-file-name.txt", content="irrelevant")
|
||||
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
|
||||
index 4c6f6920ed..0e692644b7 100644
|
||||
--- a/tests/file_storage/tests.py
|
||||
+++ b/tests/file_storage/tests.py
|
||||
@@ -291,12 +291,6 @@ class FileStorageTests(SimpleTestCase):
|
||||
|
||||
self.storage.delete('path/to/test.file')
|
||||
|
||||
- def test_file_save_abs_path(self):
|
||||
- test_name = 'path/to/test.file'
|
||||
- f = ContentFile('file saved with path')
|
||||
- f_name = self.storage.save(os.path.join(self.temp_dir, test_name), f)
|
||||
- self.assertEqual(f_name, test_name)
|
||||
-
|
||||
def test_save_doesnt_close(self):
|
||||
with TemporaryUploadedFile('test', 'text/plain', 1, 'utf8') as file:
|
||||
file.write(b'1')
|
||||
--
|
||||
2.45.2
|
||||
|
||||
195
CVE-2024-39614.patch
Normal file
195
CVE-2024-39614.patch
Normal file
@ -0,0 +1,195 @@
|
||||
From 2f128b1865bc43f6cf3583b1255bf1bd8be29e57 Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Mon, 22 Jul 2024 11:23:29 +0200
|
||||
Subject: [PATCH] Fixed CVE-2024-39614 -- Mitigated potential DoS in
|
||||
get_supported_language_variant().
|
||||
|
||||
Language codes are now parsed with a maximum length limit of 500 chars.
|
||||
|
||||
Thanks to MProgrammer for the report.
|
||||
---
|
||||
django/utils/translation/trans_real.py | 29 ++++++++++---
|
||||
docs/ref/utils.txt | 25 +++++++++++
|
||||
tests/i18n/tests.py | 59 ++++++++++++++++++++++++++
|
||||
3 files changed, 107 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
|
||||
index ecd701f3d8..0a237a5afc 100644
|
||||
--- a/django/utils/translation/trans_real.py
|
||||
+++ b/django/utils/translation/trans_real.py
|
||||
@@ -30,9 +30,10 @@ _default = None
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
# Maximum number of characters that will be parsed from the Accept-Language
|
||||
-# header to prevent possible denial of service or memory exhaustion attacks.
|
||||
-# About 10x longer than the longest value shown on MDN’s Accept-Language page.
|
||||
-ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500
|
||||
+# header or cookie to prevent possible denial of service or memory exhaustion
|
||||
+# attacks. About 10x longer than the longest value shown on MDN’s
|
||||
+# Accept-Language page.
|
||||
+LANGUAGE_CODE_MAX_LENGTH = 500
|
||||
|
||||
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
|
||||
# and RFC 3066, section 2.1
|
||||
@@ -473,12 +474,28 @@ def get_supported_language_variant(lang_code, strict=False):
|
||||
If `strict` is False (the default), look for a country-specific variant
|
||||
when neither the language code nor its generic variant is found.
|
||||
|
||||
+ The language code is truncated to a maximum length to avoid potential
|
||||
+ denial of service attacks.
|
||||
+
|
||||
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
|
||||
as the provided language codes are taken from the HTTP request. See also
|
||||
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
|
||||
"""
|
||||
if lang_code:
|
||||
- # If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
|
||||
+ # Truncate the language code to a maximum length to avoid potential
|
||||
+ # denial of service attacks.
|
||||
+ if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
|
||||
+ index = lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)
|
||||
+ if (
|
||||
+ not strict
|
||||
+ and index > 0
|
||||
+ ):
|
||||
+ # There is a generic variant under the maximum length accepted length.
|
||||
+ lang_code = lang_code[:index]
|
||||
+ else:
|
||||
+ raise ValueError("'lang_code' exceeds the maximum accepted length")
|
||||
+ # If 'zh-hant-tw' is not supported, try special fallback or subsequent
|
||||
+ # language codes i.e. 'zh-hant' and 'zh'.
|
||||
possible_lang_codes = [lang_code]
|
||||
try:
|
||||
possible_lang_codes.extend(LANG_INFO[lang_code]['fallback'])
|
||||
@@ -599,13 +616,13 @@ def parse_accept_lang_header(lang_string):
|
||||
functools.lru_cache() to avoid repetitive parsing of common header values.
|
||||
"""
|
||||
# If the header value doesn't exceed the maximum allowed length, parse it.
|
||||
- if len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH:
|
||||
+ if len(lang_string) <= LANGUAGE_CODE_MAX_LENGTH:
|
||||
return _parse_accept_lang_header(lang_string)
|
||||
|
||||
# If there is at least one comma in the value, parse up to the last comma
|
||||
# before the max length, skipping any truncated parts at the end of the
|
||||
# header value.
|
||||
- index = lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)
|
||||
+ index = lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH)
|
||||
if index > 0:
|
||||
return _parse_accept_lang_header(lang_string[:index])
|
||||
|
||||
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
|
||||
index 390f167ce2..d0a8e8c1f3 100644
|
||||
--- a/docs/ref/utils.txt
|
||||
+++ b/docs/ref/utils.txt
|
||||
@@ -1150,6 +1150,31 @@ functions without the ``u``.
|
||||
|
||||
Raises :exc:`LookupError` if nothing is found.
|
||||
|
||||
+.. function:: get_supported_language_variant(lang_code, strict=False)
|
||||
+
|
||||
+ Returns ``lang_code`` if it's in the :setting:`LANGUAGES` setting, possibly
|
||||
+ selecting a more generic variant. For example, ``'es'`` is returned if
|
||||
+ ``lang_code`` is ``'es-ar'`` and ``'es'`` is in :setting:`LANGUAGES` but
|
||||
+ ``'es-ar'`` isn't.
|
||||
+
|
||||
+ ``lang_code`` has a maximum accepted length of 500 characters. A
|
||||
+ :exc:`ValueError` is raised if ``lang_code`` exceeds this limit and
|
||||
+ ``strict`` is ``True``, or if there is no generic variant and ``strict``
|
||||
+ is ``False``.
|
||||
+
|
||||
+ If ``strict`` is ``False`` (the default), a country-specific variant may
|
||||
+ be returned when neither the language code nor its generic variant is found.
|
||||
+ For example, if only ``'es-co'`` is in :setting:`LANGUAGES`, that's
|
||||
+ returned for ``lang_code``\s like ``'es'`` and ``'es-ar'``. Those matches
|
||||
+ aren't returned if ``strict=True``.
|
||||
+
|
||||
+ Raises :exc:`LookupError` if nothing is found.
|
||||
+
|
||||
+ .. versionchanged:: 4.2.14
|
||||
+
|
||||
+ In older versions, ``lang_code`` values over 500 characters were
|
||||
+ processed without raising a :exc:`ValueError`.
|
||||
+
|
||||
.. function:: to_locale(language)
|
||||
|
||||
Turns a language name (en-us) into a locale name (en_US).
|
||||
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
|
||||
index 6efc3a5ae3..3087e5b6a6 100644
|
||||
--- a/tests/i18n/tests.py
|
||||
+++ b/tests/i18n/tests.py
|
||||
@@ -39,6 +39,7 @@ from django.utils.translation import (
|
||||
from django.utils.translation.reloader import (
|
||||
translation_file_changed, watch_for_translation_changes,
|
||||
)
|
||||
+from django.utils.translation.trans_real import LANGUAGE_CODE_MAX_LENGTH
|
||||
|
||||
from .forms import CompanyForm, I18nForm, SelectDateForm
|
||||
from .models import Company, TestModel
|
||||
@@ -1434,6 +1435,64 @@ class MiscTests(SimpleTestCase):
|
||||
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'zh-hans'}
|
||||
r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'}
|
||||
self.assertEqual(g(r), 'zh-hans')
|
||||
+
|
||||
+ @override_settings(
|
||||
+ USE_I18N=True,
|
||||
+ LANGUAGES=[
|
||||
+ ("en", "English"),
|
||||
+ ("ar-dz", "Algerian Arabic"),
|
||||
+ ("de", "German"),
|
||||
+ ("de-at", "Austrian German"),
|
||||
+ ("pt-BR", "Portuguese (Brazil)"),
|
||||
+ ],
|
||||
+ )
|
||||
+ def test_get_supported_language_variant_real(self):
|
||||
+ g = trans_real.get_supported_language_variant
|
||||
+ self.assertEqual(g("en"), "en")
|
||||
+ self.assertEqual(g("en-gb"), "en")
|
||||
+ self.assertEqual(g("de"), "de")
|
||||
+ self.assertEqual(g("de-at"), "de-at")
|
||||
+ self.assertEqual(g("de-ch"), "de")
|
||||
+ self.assertEqual(g("pt-br"), "pt-br")
|
||||
+ self.assertEqual(g("pt-BR"), "pt-BR")
|
||||
+ self.assertEqual(g("pt"), "pt-br")
|
||||
+ self.assertEqual(g("pt-pt"), "pt-br")
|
||||
+ self.assertEqual(g("ar-dz"), "ar-dz")
|
||||
+ self.assertEqual(g("ar-DZ"), "ar-DZ")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("pt", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("pt-pt", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("xyz")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("xy-zz")
|
||||
+ msg = "'lang_code' exceeds the maximum accepted length"
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("x" * LANGUAGE_CODE_MAX_LENGTH)
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ g("x" * (LANGUAGE_CODE_MAX_LENGTH + 1))
|
||||
+ # 167 * 3 = 501 which is LANGUAGE_CODE_MAX_LENGTH + 1.
|
||||
+ self.assertEqual(g("en-" * 167), "en")
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ g("en-" * 167, strict=True)
|
||||
+ self.assertEqual(g("en-" * 30000), "en") # catastrophic test
|
||||
+
|
||||
+ def test_get_supported_language_variant_null(self):
|
||||
+ g = trans_null.get_supported_language_variant
|
||||
+ self.assertEqual(g(settings.LANGUAGE_CODE), settings.LANGUAGE_CODE)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("pt")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de-at")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de-at", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("xyz")
|
||||
|
||||
@override_settings(
|
||||
USE_I18N=True,
|
||||
--
|
||||
2.45.2
|
||||
|
||||
76
CVE-2024-41989.patch
Normal file
76
CVE-2024-41989.patch
Normal file
@ -0,0 +1,76 @@
|
||||
From 0521744d21a7854e849336af1e3a3aad44cee017 Mon Sep 17 00:00:00 2001
|
||||
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
Date: Fri, 12 Jul 2024 11:38:34 +0200
|
||||
Subject: [PATCH 1/4] [4.2.x] Fixed CVE-2024-41989 -- Prevented excessive
|
||||
memory consumption in floatformat.
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Thanks Elias Myllymäki for the report.
|
||||
|
||||
Co-authored-by: Shai Berger <shai@platonix.com>
|
||||
---
|
||||
django/template/defaultfilters.py | 13 +++++++++++++
|
||||
.../filter_tests/test_floatformat.py | 17 +++++++++++++++++
|
||||
3 files changed, 39 insertions(+)
|
||||
|
||||
Index: Django-2.2.28/django/template/defaultfilters.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/template/defaultfilters.py
|
||||
+++ Django-2.2.28/django/template/defaultfilters.py
|
||||
@@ -135,6 +135,19 @@ def floatformat(text, arg=-1):
|
||||
except ValueError:
|
||||
return input_val
|
||||
|
||||
+ _, digits, exponent = d.as_tuple()
|
||||
+ try:
|
||||
+ number_of_digits_and_exponent_sum = len(digits) + abs(exponent)
|
||||
+ except TypeError:
|
||||
+ # Exponent values can be "F", "n", "N".
|
||||
+ number_of_digits_and_exponent_sum = 0
|
||||
+
|
||||
+ # Values with more than 200 digits, or with a large exponent, are returned "as is"
|
||||
+ # to avoid high memory consumption and potential denial-of-service attacks.
|
||||
+ # The cut-off of 200 is consistent with django.utils.numberformat.floatformat().
|
||||
+ if number_of_digits_and_exponent_sum > 200:
|
||||
+ return input_val
|
||||
+
|
||||
try:
|
||||
m = int(d) - d
|
||||
except (ValueError, OverflowError, InvalidOperation):
|
||||
Index: Django-2.2.28/tests/template_tests/filter_tests/test_floatformat.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/template_tests/filter_tests/test_floatformat.py
|
||||
+++ Django-2.2.28/tests/template_tests/filter_tests/test_floatformat.py
|
||||
@@ -55,6 +55,7 @@ class FunctionTests(SimpleTestCase):
|
||||
self.assertEqual(floatformat(1.5e-15, 20), '0.00000000000000150000')
|
||||
self.assertEqual(floatformat(1.5e-15, -20), '0.00000000000000150000')
|
||||
self.assertEqual(floatformat(1.00000000000000015, 16), '1.0000000000000002')
|
||||
+ self.assertEqual(floatformat("1e199"), "1" + "0" * 199)
|
||||
|
||||
def test_zero_values(self):
|
||||
self.assertEqual(floatformat(0, 6), '0.000000')
|
||||
@@ -68,6 +69,22 @@ class FunctionTests(SimpleTestCase):
|
||||
self.assertEqual(floatformat(pos_inf), 'inf')
|
||||
self.assertEqual(floatformat(neg_inf), '-inf')
|
||||
self.assertEqual(floatformat(pos_inf / pos_inf), 'nan')
|
||||
+ self.assertEqual(floatformat("inf"), "inf")
|
||||
+ self.assertEqual(floatformat("NaN"), "NaN")
|
||||
+
|
||||
+ def test_too_many_digits_to_render(self):
|
||||
+ cases = [
|
||||
+ "1e200",
|
||||
+ "1E200",
|
||||
+ "1E10000000000000000",
|
||||
+ "-1E10000000000000000",
|
||||
+ "1e10000000000000000",
|
||||
+ "-1e10000000000000000",
|
||||
+ "1" + "0" * 1_000_000,
|
||||
+ ]
|
||||
+ for value in cases:
|
||||
+ with self.subTest(value=value):
|
||||
+ self.assertEqual(floatformat(value), value)
|
||||
|
||||
def test_float_dunder_method(self):
|
||||
class FloatWrapper:
|
||||
61
CVE-2024-41990.patch
Normal file
61
CVE-2024-41990.patch
Normal file
@ -0,0 +1,61 @@
|
||||
From 729d7934e34ff91f262f3e7089e32cab701b09ca Mon Sep 17 00:00:00 2001
|
||||
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
Date: Thu, 18 Jul 2024 13:19:34 +0200
|
||||
Subject: [PATCH 2/4] [4.2.x] Fixed CVE-2024-41990 -- Mitigated potential DoS
|
||||
in urlize and urlizetrunc template filters.
|
||||
|
||||
Thanks to MProgrammer for the report.
|
||||
---
|
||||
django/utils/html.py | 18 ++++++++----------
|
||||
tests/utils_tests/test_html.py | 2 ++
|
||||
3 files changed, 17 insertions(+), 10 deletions(-)
|
||||
|
||||
Index: Django-2.2.28/django/utils/html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/utils/html.py
|
||||
+++ Django-2.2.28/django/utils/html.py
|
||||
@@ -314,7 +314,11 @@ def urlize(text, trim_url_limit=None, no
|
||||
trimmed_something = True
|
||||
counts[closing] -= strip
|
||||
|
||||
- rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
|
||||
+ amp = middle.rfind("&")
|
||||
+ if amp == -1:
|
||||
+ rstripped = middle.rstrip(TRAILING_PUNCTUATION_CHARS)
|
||||
+ else:
|
||||
+ rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
|
||||
if rstripped != middle:
|
||||
trail = middle[len(rstripped) :] + trail
|
||||
middle = rstripped
|
||||
@@ -322,15 +326,9 @@ def urlize(text, trim_url_limit=None, no
|
||||
|
||||
if trailing_punctuation_chars_has_semicolon() and middle.endswith(";"):
|
||||
# Only strip if not part of an HTML entity.
|
||||
- amp = middle.rfind("&")
|
||||
- if amp == -1:
|
||||
- can_strip = True
|
||||
- else:
|
||||
- potential_entity = middle[amp:]
|
||||
- escaped = html.unescape(potential_entity)
|
||||
- can_strip = (escaped == potential_entity) or escaped.endswith(";")
|
||||
-
|
||||
- if can_strip:
|
||||
+ potential_entity = middle[amp:]
|
||||
+ escaped = html.unescape(potential_entity)
|
||||
+ if escaped == potential_entity or escaped.endswith(";"):
|
||||
rstripped = middle.rstrip(";")
|
||||
amount_stripped = len(middle) - len(rstripped)
|
||||
if amp > -1 and amount_stripped > 1:
|
||||
Index: Django-2.2.28/tests/utils_tests/test_html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
|
||||
+++ Django-2.2.28/tests/utils_tests/test_html.py
|
||||
@@ -274,6 +274,8 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
"[(" * 100_000 + ":" + ")]" * 100_000,
|
||||
"([[" * 100_000 + ":" + "]])" * 100_000,
|
||||
"&:" + ";" * 100_000,
|
||||
+ "&.;" * 100_000,
|
||||
+ ".;" * 100_000,
|
||||
)
|
||||
for value in tests:
|
||||
with self.subTest(value=value):
|
||||
98
CVE-2024-41991.patch
Normal file
98
CVE-2024-41991.patch
Normal file
@ -0,0 +1,98 @@
|
||||
From 772a73f70c3d249c99c23012849e66276b7b0715 Mon Sep 17 00:00:00 2001
|
||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
Date: Wed, 10 Jul 2024 20:30:12 +0200
|
||||
Subject: [PATCH 3/4] [4.2.x] Fixed CVE-2024-41991 -- Prevented potential ReDoS
|
||||
in django.utils.html.urlize() and AdminURLFieldWidget.
|
||||
|
||||
Thanks Seokchan Yoon for the report.
|
||||
|
||||
Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
---
|
||||
django/contrib/admin/widgets.py | 2 +-
|
||||
django/utils/html.py | 10 ++++++++--
|
||||
tests/admin_widgets/tests.py | 7 ++++++-
|
||||
tests/utils_tests/test_html.py | 13 +++++++++++++
|
||||
5 files changed, 35 insertions(+), 4 deletions(-)
|
||||
|
||||
Index: Django-2.2.28/django/contrib/admin/widgets.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/contrib/admin/widgets.py
|
||||
+++ Django-2.2.28/django/contrib/admin/widgets.py
|
||||
@@ -344,7 +344,7 @@ class AdminURLFieldWidget(forms.URLInput
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['current_label'] = _('Currently:')
|
||||
context['change_label'] = _('Change:')
|
||||
- context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else ''
|
||||
+ context['widget']['href'] = smart_urlquote(context['widget']['value']) if url_valid else ''
|
||||
context['url_valid'] = url_valid
|
||||
return context
|
||||
|
||||
Index: Django-2.2.28/django/utils/html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/utils/html.py
|
||||
+++ Django-2.2.28/django/utils/html.py
|
||||
@@ -33,6 +33,8 @@ _html_escapes = {
|
||||
ord("'"): ''',
|
||||
}
|
||||
|
||||
+MAX_URL_LENGTH = 2048
|
||||
+
|
||||
|
||||
@keep_lazy(str, SafeText)
|
||||
def escape(text):
|
||||
@@ -360,6 +362,10 @@ def urlize(text, trim_url_limit=None, no
|
||||
except ValueError:
|
||||
# value contains more than one @.
|
||||
return False
|
||||
+ # Max length for domain name labels is 63 characters per RFC 1034.
|
||||
+ # Helps to avoid ReDoS vectors in the domain part.
|
||||
+ if len(p2) > 63:
|
||||
+ return False
|
||||
# Dot must be in p2 (e.g. example.com)
|
||||
if '.' not in p2 or p2.startswith('.'):
|
||||
return False
|
||||
@@ -378,9 +384,9 @@ def urlize(text, trim_url_limit=None, no
|
||||
# Make URL we want to point to.
|
||||
url = None
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ''
|
||||
- if simple_url_re.match(middle):
|
||||
+ if len(middle) <= MAX_URL_LENGTH and simple_url_re.match(middle):
|
||||
url = smart_urlquote(unescape(middle))
|
||||
- elif simple_url_2_re.match(middle):
|
||||
+ elif len(middle) <= MAX_URL_LENGTH and simple_url_2_re.match(middle):
|
||||
url = smart_urlquote('http://%s' % unescape(middle))
|
||||
elif ':' not in middle and is_email_simple(middle):
|
||||
local, domain = middle.rsplit('@', 1)
|
||||
Index: Django-2.2.28/tests/admin_widgets/tests.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/admin_widgets/tests.py
|
||||
+++ Django-2.2.28/tests/admin_widgets/tests.py
|
||||
@@ -336,7 +336,12 @@ class AdminSplitDateTimeWidgetTest(Simpl
|
||||
class AdminURLWidgetTest(SimpleTestCase):
|
||||
def test_get_context_validates_url(self):
|
||||
w = widgets.AdminURLFieldWidget()
|
||||
- for invalid in ['', '/not/a/full/url/', 'javascript:alert("Danger XSS!")']:
|
||||
+ for invalid in [
|
||||
+ "",
|
||||
+ "/not/a/full/url/",
|
||||
+ 'javascript:alert("Danger XSS!")',
|
||||
+ "http://" + "한.글." * 1_000_000 + "com",
|
||||
+ ]:
|
||||
with self.subTest(url=invalid):
|
||||
self.assertFalse(w.get_context('name', invalid, {})['url_valid'])
|
||||
self.assertTrue(w.get_context('name', 'http://example.com', {})['url_valid'])
|
||||
Index: Django-2.2.28/tests/utils_tests/test_html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
|
||||
+++ Django-2.2.28/tests/utils_tests/test_html.py
|
||||
@@ -261,6 +261,10 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
def test_urlize_unchanged_inputs(self):
|
||||
tests = (
|
||||
("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
|
||||
+ # Unicode domain catastrophic tests.
|
||||
+ "a@" + "한.글." * 1_000_000 + "a",
|
||||
+ "http://" + "한.글." * 1_000_000 + "com",
|
||||
+ "www." + "한.글." * 1_000_000 + "com",
|
||||
("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
|
||||
"foo@",
|
||||
"@foo.com",
|
||||
84
CVE-2024-42005.patch
Normal file
84
CVE-2024-42005.patch
Normal file
@ -0,0 +1,84 @@
|
||||
From b6de28f897709ee5d94ca2da21bcc98f9dade01c Mon Sep 17 00:00:00 2001
|
||||
From: Simon Charette <charette.s@gmail.com>
|
||||
Date: Thu, 25 Jul 2024 18:19:13 +0200
|
||||
Subject: [PATCH 4/4] [4.2.x] Fixed CVE-2024-42005 -- Mitigated
|
||||
QuerySet.values() SQL injection attacks against JSON fields.
|
||||
|
||||
Thanks Eyal (eyalgabay) for the report.
|
||||
---
|
||||
django/db/models/sql/query.py | 2 ++
|
||||
tests/expressions/models.py | 7 +++++++
|
||||
tests/expressions/test_queryset_values.py | 17 +++++++++++++++--
|
||||
4 files changed, 31 insertions(+), 2 deletions(-)
|
||||
|
||||
Index: Django-2.0.7/django/db/models/sql/query.py
|
||||
===================================================================
|
||||
--- Django-2.0.7.orig/django/db/models/sql/query.py
|
||||
+++ Django-2.0.7/django/db/models/sql/query.py
|
||||
@@ -1924,6 +1924,8 @@ class Query:
|
||||
self.clear_select_fields()
|
||||
|
||||
if fields:
|
||||
+ for field in fields:
|
||||
+ self.check_alias(field)
|
||||
field_names = []
|
||||
extra_names = []
|
||||
annotation_names = []
|
||||
Index: Django-2.0.7/tests/expressions/models.py
|
||||
===================================================================
|
||||
--- Django-2.0.7.orig/tests/expressions/models.py
|
||||
+++ Django-2.0.7/tests/expressions/models.py
|
||||
@@ -4,6 +4,7 @@ Tests for F() query expression syntax.
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
+from django.contrib.postgres.fields import JSONField
|
||||
|
||||
|
||||
class Employee(models.Model):
|
||||
@@ -91,3 +92,10 @@ class UUID(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.uuid
|
||||
+
|
||||
+
|
||||
+class JSONFieldModel(models.Model):
|
||||
+ data = JSONField(null=True)
|
||||
+
|
||||
+ class Meta:
|
||||
+ required_db_features = {"supports_json_field"}
|
||||
Index: Django-2.0.7/tests/expressions/test_queryset_values.py
|
||||
===================================================================
|
||||
--- Django-2.0.7.orig/tests/expressions/test_queryset_values.py
|
||||
+++ Django-2.0.7/tests/expressions/test_queryset_values.py
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.db.models.aggregates import Sum
|
||||
from django.db.models.expressions import F
|
||||
-from django.test import TestCase
|
||||
+from django.test import TestCase, skipUnlessDBFeature
|
||||
|
||||
-from .models import Company, Employee
|
||||
+from .models import Company, Employee, JSONFieldModel
|
||||
|
||||
|
||||
class ValuesExpressionsTests(TestCase):
|
||||
@@ -36,6 +36,19 @@ class ValuesExpressionsTests(TestCase):
|
||||
with self.assertRaisesMessage(ValueError, msg):
|
||||
Company.objects.values(**{crafted_alias: F("ceo__salary")})
|
||||
|
||||
+ @skipUnlessDBFeature("supports_json_field")
|
||||
+ def test_values_expression_alias_sql_injection_json_field(self):
|
||||
+ crafted_alias = """injected_name" from "expressions_company"; --"""
|
||||
+ msg = (
|
||||
+ "Column aliases cannot contain whitespace characters, quotation marks, "
|
||||
+ "semicolons, or SQL comments."
|
||||
+ )
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ JSONFieldModel.objects.values(f"data__{crafted_alias}")
|
||||
+
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ JSONFieldModel.objects.values_list(f"data__{crafted_alias}")
|
||||
+
|
||||
def test_values_expression_group_by(self):
|
||||
# values() applies annotate() first, so values selected are grouped by
|
||||
# id, not firstname.
|
||||
133
CVE-2024-45230.patch
Normal file
133
CVE-2024-45230.patch
Normal file
@ -0,0 +1,133 @@
|
||||
From 65a776dd25b657cc32edafaad98d91aa0b51e641 Mon Sep 17 00:00:00 2001
|
||||
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
Date: Mon, 12 Aug 2024 15:17:57 +0200
|
||||
Subject: [PATCH 1/2] [4.2.x] Fixed CVE-2024-45230 -- Mitigated potential DoS
|
||||
in urlize and urlizetrunc template filters.
|
||||
|
||||
Thanks MProgrammer (https://hackerone.com/mprogrammer) for the report.
|
||||
---
|
||||
django/utils/html.py | 17 ++++++++------
|
||||
docs/ref/templates/builtins.txt | 11 ++++++++++
|
||||
docs/releases/4.2.16.txt | 15 +++++++++++++
|
||||
docs/releases/index.txt | 1 +
|
||||
.../filter_tests/test_urlize.py | 22 +++++++++++++++++++
|
||||
tests/utils_tests/test_html.py | 1 +
|
||||
6 files changed, 60 insertions(+), 7 deletions(-)
|
||||
create mode 100644 docs/releases/4.2.16.txt
|
||||
|
||||
Index: Django-2.2.28/django/utils/html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/utils/html.py
|
||||
+++ Django-2.2.28/django/utils/html.py
|
||||
@@ -322,14 +322,17 @@ def urlize(text, trim_url_limit=None, no
|
||||
potential_entity = middle[amp:]
|
||||
escaped = html.unescape(potential_entity)
|
||||
if escaped == potential_entity or escaped.endswith(";"):
|
||||
- rstripped = middle.rstrip(";")
|
||||
- amount_stripped = len(middle) - len(rstripped)
|
||||
- if amp > -1 and amount_stripped > 1:
|
||||
- # Leave a trailing semicolon as might be an entity.
|
||||
- trail = middle[len(rstripped) + 1 :] + trail
|
||||
- middle = rstripped + ";"
|
||||
+ rstripped = middle.rstrip(TRAILING_PUNCTUATION_CHARS)
|
||||
+ trail_start = len(rstripped)
|
||||
+ amount_trailing_semicolons = len(middle) - len(middle.rstrip(";"))
|
||||
+ if amp > -1 and amount_trailing_semicolons > 1:
|
||||
+ # Leave up to most recent semicolon as might be an entity.
|
||||
+ recent_semicolon = middle[trail_start:].index(";")
|
||||
+ middle_semicolon_index = recent_semicolon + trail_start + 1
|
||||
+ trail = middle[middle_semicolon_index:] + trail
|
||||
+ middle = rstripped + middle[trail_start:middle_semicolon_index]
|
||||
else:
|
||||
- trail = middle[len(rstripped) :] + trail
|
||||
+ trail = middle[trail_start:] + trail
|
||||
middle = rstripped
|
||||
trimmed_something = True
|
||||
# Trim trailing punctuation (after trimming wrapping punctuation,
|
||||
Index: Django-2.2.28/docs/ref/templates/builtins.txt
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/docs/ref/templates/builtins.txt
|
||||
+++ Django-2.2.28/docs/ref/templates/builtins.txt
|
||||
@@ -2463,6 +2463,17 @@ Django's built-in :tfilter:`escape` filt
|
||||
email addresses that contain single quotes (``'``), things won't work as
|
||||
expected. Apply this filter only to plain text.
|
||||
|
||||
+.. warning::
|
||||
+
|
||||
+ Using ``urlize`` or ``urlizetrunc`` can incur a performance penalty, which
|
||||
+ can become severe when applied to user controlled values such as content
|
||||
+ stored in a :class:`~django.db.models.TextField`. You can use
|
||||
+ :tfilter:`truncatechars` to add a limit to such inputs:
|
||||
+
|
||||
+ .. code-block:: html+django
|
||||
+
|
||||
+ {{ value|truncatechars:500|urlize }}
|
||||
+
|
||||
.. templatefilter:: urlizetrunc
|
||||
|
||||
``urlizetrunc``
|
||||
Index: Django-2.2.28/docs/releases/4.2.16.txt
|
||||
===================================================================
|
||||
--- /dev/null
|
||||
+++ Django-2.2.28/docs/releases/4.2.16.txt
|
||||
@@ -0,0 +1,15 @@
|
||||
+===========================
|
||||
+Django 4.2.16 release notes
|
||||
+===========================
|
||||
+
|
||||
+*September 3, 2024*
|
||||
+
|
||||
+Django 4.2.16 fixes one security issue with severity "moderate" and one
|
||||
+security issues with severity "low" in 4.2.15.
|
||||
+
|
||||
+CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html.urlize()``
|
||||
+===========================================================================================
|
||||
+
|
||||
+:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
|
||||
+denial-of-service attack via very large inputs with a specific sequence of
|
||||
+characters.
|
||||
Index: Django-2.2.28/tests/template_tests/filter_tests/test_urlize.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/template_tests/filter_tests/test_urlize.py
|
||||
+++ Django-2.2.28/tests/template_tests/filter_tests/test_urlize.py
|
||||
@@ -260,6 +260,28 @@ class FunctionTests(SimpleTestCase):
|
||||
'A test <a href="http://testing.com/example" rel="nofollow">http://testing.com/example</a>.,:;)"!'
|
||||
)
|
||||
|
||||
+ def test_trailing_semicolon(self):
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&;", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>;",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&;;", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>;;",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&.;...;", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>.;...;",
|
||||
+ )
|
||||
+
|
||||
def test_brackets(self):
|
||||
"""
|
||||
#19070 - Check urlize handles brackets properly
|
||||
Index: Django-2.2.28/tests/utils_tests/test_html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
|
||||
+++ Django-2.2.28/tests/utils_tests/test_html.py
|
||||
@@ -282,6 +282,7 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
"&:" + ";" * 100_000,
|
||||
"&.;" * 100_000,
|
||||
".;" * 100_000,
|
||||
+ "&" + ";:" * 100_000,
|
||||
)
|
||||
for value in tests:
|
||||
with self.subTest(value=value):
|
||||
133
CVE-2024-45231.patch
Normal file
133
CVE-2024-45231.patch
Normal file
@ -0,0 +1,133 @@
|
||||
From fe42da9cdacd9f43fb0d499244314c36f9a11a19 Mon Sep 17 00:00:00 2001
|
||||
From: Natalia <124304+nessita@users.noreply.github.com>
|
||||
Date: Mon, 19 Aug 2024 14:47:38 -0300
|
||||
Subject: [PATCH 2/2] [4.2.x] Fixed CVE-2024-45231 -- Avoided server error on
|
||||
password reset when email sending fails.
|
||||
|
||||
On successful submission of a password reset request, an email is sent
|
||||
to the accounts known to the system. If sending this email fails (due to
|
||||
email backend misconfiguration, service provider outage, network issues,
|
||||
etc.), an attacker might exploit this by detecting which password reset
|
||||
requests succeed and which ones generate a 500 error response.
|
||||
|
||||
Thanks to Thibaut Spriet for the report, and to Mariusz Felisiak and
|
||||
Sarah Boyce for the reviews.
|
||||
---
|
||||
django/contrib/auth/forms.py | 9 ++++++++-
|
||||
docs/ref/logging.txt | 12 ++++++++++++
|
||||
docs/releases/4.2.16.txt | 11 +++++++++++
|
||||
docs/topics/auth/default.txt | 4 +++-
|
||||
tests/auth_tests/test_forms.py | 21 +++++++++++++++++++++
|
||||
tests/mail/custombackend.py | 5 +++++
|
||||
6 files changed, 60 insertions(+), 2 deletions(-)
|
||||
|
||||
Index: Django-2.2.28/django/contrib/auth/forms.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/contrib/auth/forms.py
|
||||
+++ Django-2.2.28/django/contrib/auth/forms.py
|
||||
@@ -1,3 +1,4 @@
|
||||
+import logging
|
||||
import unicodedata
|
||||
|
||||
from django import forms
|
||||
@@ -18,6 +19,7 @@ from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
UserModel = get_user_model()
|
||||
+logger = logging.getLogger("django.contrib.auth")
|
||||
|
||||
|
||||
def _unicode_ci_compare(s1, s2):
|
||||
@@ -256,7 +258,12 @@ class PasswordResetForm(forms.Form):
|
||||
html_email = loader.render_to_string(html_email_template_name, context)
|
||||
email_message.attach_alternative(html_email, 'text/html')
|
||||
|
||||
- email_message.send()
|
||||
+ try:
|
||||
+ email_message.send()
|
||||
+ except Exception:
|
||||
+ logger.exception(
|
||||
+ "Failed to send password reset email to %s:", context["user"].pk
|
||||
+ )
|
||||
|
||||
def get_users(self, email):
|
||||
"""Given an email, return matching user(s) who should receive a reset.
|
||||
Index: Django-2.2.28/docs/releases/4.2.16.txt
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/docs/releases/4.2.16.txt
|
||||
+++ Django-2.2.28/docs/releases/4.2.16.txt
|
||||
@@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-serv
|
||||
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
|
||||
denial-of-service attack via very large inputs with a specific sequence of
|
||||
characters.
|
||||
+
|
||||
+CVE-2024-45231: Potential user email enumeration via response status on password reset
|
||||
+======================================================================================
|
||||
+
|
||||
+Due to unhandled email sending failures, the
|
||||
+:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
|
||||
+attackers to enumerate user emails by issuing password reset requests and
|
||||
+observing the outcomes.
|
||||
+
|
||||
+To mitigate this risk, exceptions occurring during password reset email sending
|
||||
+are now handled and logged using the :ref:`django-contrib-auth-logger` logger.
|
||||
Index: Django-2.2.28/docs/topics/auth/default.txt
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/docs/topics/auth/default.txt
|
||||
+++ Django-2.2.28/docs/topics/auth/default.txt
|
||||
@@ -1530,7 +1530,9 @@ provides several built-in forms located
|
||||
.. method:: send_mail(subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None)
|
||||
|
||||
Uses the arguments to send an ``EmailMultiAlternatives``.
|
||||
- Can be overridden to customize how the email is sent to the user.
|
||||
+ Can be overridden to customize how the email is sent to the user. If
|
||||
+ you choose to override this method, be mindful of handling potential
|
||||
+ exceptions raised due to email sending failures.
|
||||
|
||||
:param subject_template_name: the template for the subject.
|
||||
:param email_template_name: the template for the email body.
|
||||
Index: Django-2.2.28/tests/auth_tests/test_forms.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/auth_tests/test_forms.py
|
||||
+++ Django-2.2.28/tests/auth_tests/test_forms.py
|
||||
@@ -929,6 +929,27 @@ class PasswordResetFormTest(TestDataMixi
|
||||
message.get_payload(1).get_payload()
|
||||
))
|
||||
|
||||
+ @override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend")
|
||||
+ def test_save_send_email_exceptions_are_catched_and_logged(self):
|
||||
+ (user, username, email) = self.create_dummy_user()
|
||||
+ form = PasswordResetForm({"email": email})
|
||||
+ self.assertTrue(form.is_valid())
|
||||
+
|
||||
+ with self.assertLogs("django.contrib.auth", level=0) as cm:
|
||||
+ form.save()
|
||||
+
|
||||
+ self.assertEqual(len(mail.outbox), 0)
|
||||
+ self.assertEqual(len(cm.output), 1)
|
||||
+ errors = cm.output[0].split("\n")
|
||||
+ pk = user.pk
|
||||
+ self.assertEqual(
|
||||
+ errors[0],
|
||||
+ f"ERROR:django.contrib.auth:Failed to send password reset email to {pk}:",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ errors[-1], "ValueError: FailingEmailBackend is doomed to fail."
|
||||
+ )
|
||||
+
|
||||
@override_settings(AUTH_USER_MODEL='auth_tests.CustomEmailField')
|
||||
def test_custom_email_field(self):
|
||||
email = 'test@mail.com'
|
||||
Index: Django-2.2.28/tests/mail/custombackend.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/mail/custombackend.py
|
||||
+++ Django-2.2.28/tests/mail/custombackend.py
|
||||
@@ -13,3 +13,8 @@ class EmailBackend(BaseEmailBackend):
|
||||
# Messages are stored in an instance variable for testing.
|
||||
self.test_outbox.extend(email_messages)
|
||||
return len(email_messages)
|
||||
+
|
||||
+
|
||||
+class FailingEmailBackend(BaseEmailBackend):
|
||||
+ def send_messages(self, email_messages):
|
||||
+ raise ValueError("FailingEmailBackend is doomed to fail.")
|
||||
91
CVE-2024-53907.patch
Normal file
91
CVE-2024-53907.patch
Normal file
@ -0,0 +1,91 @@
|
||||
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
|
||||
|
||||
293
CVE-2024-56374.patch
Normal file
293
CVE-2024-56374.patch
Normal file
@ -0,0 +1,293 @@
|
||||
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):
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
%global _empty_manifest_terminate_build 0
|
||||
Name: python-django
|
||||
Version: 2.2.27
|
||||
Release: 9
|
||||
Release: 14
|
||||
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,6 +21,23 @@ 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
|
||||
@ -87,6 +104,22 @@ 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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user