summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNatalia <124304+nessita@users.noreply.github.com>2026-01-21 15:24:55 -0300
committerJacob Walls <jacobtylerwalls@gmail.com>2026-02-03 08:25:31 -0500
commitb40cfc6052ced26dcd8166a58ea6f841d0d2cac8 (patch)
treeb9b1cfa7998a9065b0b0066a668a81107a48e875
parenta14363102d98fa29b8cced578eb3a0fadaa5bcb7 (diff)
[4.2.x] Fixed CVE-2026-1285 -- Mitigated potential DoS in django.utils.text.Truncator for HTML input.
The `TruncateHTMLParser` used `deque.remove()` to remove tags from the stack when processing end tags. With crafted input containing many unmatched end tags, this caused repeated full scans of the tag stack, leading to quadratic time complexity. The fix uses LIFO semantics, only removing a tag from the stack when it matches the most recently opened tag. This avoids linear scans for unmatched end tags and reduces complexity to linear time. Refs #30686 and 6ee37ada3241ed263d8d1c2901b030d964cbd161. Thanks Seokchan Yoon for the report. Backport of a33540b3e20b5d759aa8b2e4b9ca0e8edd285344 from main.
-rw-r--r--django/utils/text.py14
-rw-r--r--docs/releases/4.2.28.txt14
-rw-r--r--tests/utils_tests/test_text.py10
3 files changed, 29 insertions, 9 deletions
diff --git a/django/utils/text.py b/django/utils/text.py
index b018f2601f..694baf1ac3 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -272,15 +272,11 @@ class Truncator(SimpleLazyObject):
if self_closing or tagname in html4_singlets:
pass
elif closing_tag:
- # Check for match in open tags list
- try:
- i = open_tags.index(tagname)
- except ValueError:
- pass
- else:
- # SGML: An end tag closes, back to the matching start tag,
- # all unclosed intervening start tags with omitted end tags
- open_tags = open_tags[i + 1 :]
+ # Remove from the list only if the tag matches the most
+ # recently opened tag (LIFO). This avoids O(n) linear scans
+ # for unmatched end tags if `list.index()` would be called.
+ if open_tags and open_tags[0] == tagname:
+ open_tags = open_tags[1:]
else:
# Add it to the start of the open tags list
open_tags.insert(0, tagname)
diff --git a/docs/releases/4.2.28.txt b/docs/releases/4.2.28.txt
index aa06882806..3c9529a4f8 100644
--- a/docs/releases/4.2.28.txt
+++ b/docs/releases/4.2.28.txt
@@ -41,3 +41,17 @@ As a reminder, all untrusted user input should be validated before use.
This issue has severity "high" according to the :ref:`Django security policy
<security-disclosure>`.
+Django 4.2.28 fixes two security issues with severity "moderate", three
+security issues with severity "moderate", and one security issue with severity
+"low" in 4.2.27.
+
+CVE-2026-1285: Potential denial-of-service vulnerability in ``django.utils.text.Truncator`` HTML methods
+========================================================================================================
+
+``django.utils.text.Truncator.chars()`` and ``Truncator.words()`` methods (with
+``html=True``) and the :tfilter:`truncatechars_html` and
+:tfilter:`truncatewords_html` template filters were subject to a potential
+denial-of-service attack via certain inputs with a large number of unmatched
+HTML end tags, which could cause quadratic time complexity during HTML parsing.
+
+This issue has severity "moderate" according to the Django security policy.
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
index d1890e7b6d..5a018d7320 100644
--- a/tests/utils_tests/test_text.py
+++ b/tests/utils_tests/test_text.py
@@ -95,6 +95,16 @@ class TestUtilsText(SimpleTestCase):
text.Truncator(lazystr("The quick brown fox")).chars(10), "The quick…"
)
+ def test_truncate_chars_html_with_misnested_tags(self):
+ # LIFO removal keeps all tags when a middle tag is closed out of order.
+ # With <a><b><c></b>, the </b> doesn't match <c>, so all tags remain
+ # in the stack and are properly closed at truncation.
+ truncator = text.Truncator("<a><b><c></b>XXXX")
+ self.assertEqual(
+ truncator.chars(2, html=True, truncate=""),
+ "<a><b><c></b>XX</c></b></a>",
+ )
+
@patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
def test_truncate_chars_html_size_limit(self):
max_len = text.Truncator.MAX_LENGTH_HTML