diff --git a/django/__init__.py b/django/__init__.py index 865381de75d0..9207083317e0 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1,6 +1,6 @@ from django.utils.version import get_version -VERSION = (3, 2, 24, 'final', 0) +VERSION = (3, 2, 25, 'final', 0) __version__ = get_version(VERSION) diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 238aaf22535c..dc88e4a694f9 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -75,6 +75,8 @@ def intcomma(value, use_l10n=True): if match: prefix = match[0] prefix_with_commas = re.sub(r"\d{3}", r"\g<0>,", prefix[::-1])[::-1] + # Remove a leading comma, if needed. + prefix_with_commas = re.sub(r"^(-?),", r"\1", prefix_with_commas) result = prefix_with_commas + result[len(prefix) :] return result diff --git a/django/utils/text.py b/django/utils/text.py index 83e258fa81c7..88da9a2c2c6b 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -18,8 +18,61 @@ def capfirst(x): return x and str(x)[0].upper() + str(x)[1:] -# Set up regular expressions -re_words = _lazy_re_compile(r'<[^>]+?>|([^<>\s]+)', re.S) +# ----- Begin security-related performance workaround ----- + +# We used to have, below +# +# re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S) +# +# But it was shown that this regex, in the way we use it here, has some +# catastrophic edge-case performance features. Namely, when it is applied to +# text with only open brackets "<<<...". The class below provides the services +# and correct answers for the use cases, but in these edge cases does it much +# faster. +re_notag = _lazy_re_compile(r"([^<>\s]+)", re.S) +re_prt = _lazy_re_compile(r"<|([^<>\s]+)", re.S) + + +class WordsRegex: + @staticmethod + def search(text, pos): + # Look for "<" or a non-tag word. + partial = re_prt.search(text, pos) + if partial is None or partial[1] is not None: + return partial + + # "<" was found, look for a closing ">". + end = text.find(">", partial.end(0)) + if end < 0: + # ">" cannot be found, look for a word. + return re_notag.search(text, pos + 1) + else: + # "<" followed by a ">" was found -- fake a match. + end += 1 + return FakeMatch(text[partial.start(0): end], end) + + +class FakeMatch: + __slots__ = ["_text", "_end"] + + def end(self, group=0): + assert group == 0, "This specific object takes only group=0" + return self._end + + def __getitem__(self, group): + if group == 1: + return None + assert group == 0, "This specific object takes only group in {0,1}" + return self._text + + def __init__(self, text, end): + self._text, self._end = text, end + + +# ----- End security-related performance workaround ----- + +# Set up regular expressions. +re_words = WordsRegex re_chars = _lazy_re_compile(r'<[^>]+?>|(.)', re.S) re_tag = _lazy_re_compile(r'<(/)?(\S+?)(?:(\s*/)|\s.*?)?>', re.S) re_newlines = _lazy_re_compile(r'\r\n|\r') # Used in normalize_newlines diff --git a/docs/releases/3.2.25.txt b/docs/releases/3.2.25.txt new file mode 100644 index 000000000000..a3a90986ff27 --- /dev/null +++ b/docs/releases/3.2.25.txt @@ -0,0 +1,22 @@ +=========================== +Django 3.2.25 release notes +=========================== + +*March 4, 2024* + +Django 3.2.25 fixes a security issue with severity "moderate" and a regression +in 3.2.24. + +CVE-2024-27351: Potential regular expression denial-of-service in ``django.utils.text.Truncator.words()`` +========================================================================================================= + +``django.utils.text.Truncator.words()`` method (with ``html=True``) and +:tfilter:`truncatewords_html` template filter were subject to a potential +regular expression denial-of-service attack using a suitably crafted string +(follow up to :cve:`2019-14232` and :cve:`2023-43665`). + +Bugfixes +======== + +* Fixed a regression in Django 3.2.24 where ``intcomma`` template filter could + return a leading comma for string representation of floats (:ticket:`35172`). diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 38fc2a47b638..08db69ed58ce 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -25,6 +25,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 3.2.25 3.2.24 3.2.23 3.2.22 diff --git a/docs/releases/security.txt b/docs/releases/security.txt index cf63dafa0dc4..7df74adb82dd 100644 --- a/docs/releases/security.txt +++ b/docs/releases/security.txt @@ -36,6 +36,17 @@ Issues under Django's security process All security issues have been handled under versions of Django's security process. These are listed below. +February 6, 2024 - :cve:`2024-24680` +------------------------------------ + +Potential denial-of-service in ``intcomma`` template filter. +`Full description +`__ + +* Django 5.0 :commit:`(patch) <16a8fe18a3b81250f4fa57e3f93f0599dc4895bc>` +* Django 4.2 :commit:`(patch) <572ea07e84b38ea8de0551f4b4eda685d91d09d2>` +* Django 3.2 :commit:`(patch) ` + November 1, 2023 - :cve:`2023-46695` ------------------------------------ diff --git a/tests/humanize_tests/tests.py b/tests/humanize_tests/tests.py index 3c227873cf2c..d6d7ea02ad47 100644 --- a/tests/humanize_tests/tests.py +++ b/tests/humanize_tests/tests.py @@ -80,12 +80,18 @@ def test_intcomma(self): -1234567.25, "100", "-100", + "100.1", + "-100.1", + "100.13", + "-100.13", "1000", "-1000", "10123", "-10123", "10311", "-10311", + "100000.13", + "-100000.13", "1000000", "-1000000", "1234567.1234567", @@ -114,12 +120,18 @@ def test_intcomma(self): "-1,234,567.25", "100", "-100", + "100.1", + "-100.1", + "100.13", + "-100.13", "1,000", "-1,000", "10,123", "-10,123", "10,311", "-10,311", + "100,000.13", + "-100,000.13", "1,000,000", "-1,000,000", "1,234,567.1234567", diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py index 0a6f0bc3f260..758919c66e81 100644 --- a/tests/utils_tests/test_text.py +++ b/tests/utils_tests/test_text.py @@ -159,6 +159,32 @@ def test_truncate_html_words(self): truncator = text.Truncator('

I <3 python, what about you?

') self.assertEqual('

I <3 python,…

', 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( + """Hello, my dear lady!""" + ) + self.assertEqual( + """Hello, my dear…""", + truncator.words(3, html=True), + ) + + # Tags with special non-latin chars in attrs. + truncator = text.Truncator("""

Hello, my dear lady!

""") + self.assertEqual( + """

Hello, my dear…

""", + 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