File CVE-2026-1285.patch of Package python-Django.19338
From c0f469262da084fb3486c29c21323a8004433870 Mon Sep 17 00:00:00 2001
From: Natalia <124304+nessita@users.noreply.github.com>
Date: Wed, 21 Jan 2026 15:24:55 -0300
Subject: [PATCH 4/7] [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 1bb08f0d6e5b86c49203872de92a8263b5dfea9e from main.
---
django/utils/text.py | 14 +++++---------
docs/releases/4.2.28.txt | 14 ++++++++++++++
tests/utils_tests/test_text.py | 10 ++++++++++
3 files changed, 29 insertions(+), 9 deletions(-)
Index: Django-2.2.28/django/utils/text.py
===================================================================
--- Django-2.2.28.orig/django/utils/text.py
+++ Django-2.2.28/django/utils/text.py
@@ -251,15 +251,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)
Index: Django-2.2.28/tests/utils_tests/test_text.py
===================================================================
--- Django-2.2.28.orig/tests/utils_tests/test_text.py
+++ Django-2.2.28/tests/utils_tests/test_text.py
@@ -88,6 +88,16 @@ class TestUtilsText(SimpleTestCase):
# lazy strings are handled correctly
self.assertEqual(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