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 6ee37ada32.

Thanks Seokchan Yoon for the report, and Jake Howard and Jacob Walls for
reviews.
This commit is contained in:
Natalia
2026-01-21 09:53:10 -03:00
committed by Jacob Walls
parent 81aa529296
commit a33540b3e2
5 changed files with 51 additions and 4 deletions

View File

@@ -126,10 +126,11 @@ class TruncateHTMLParser(HTMLParser):
def handle_endtag(self, tag):
if tag not in self.void_elements:
self.output.append(f"</{tag}>")
try:
self.tags.remove(tag)
except ValueError:
pass
# Remove from the stack only if the tag matches the most recently
# opened tag (LIFO). This avoids O(n) linear scans for unmatched
# end tags if `deque.remove()` would be called.
if self.tags and self.tags[0] == tag:
self.tags.popleft()
def handle_data(self, data):
data, output = self.process(data)

View File

@@ -41,3 +41,15 @@ 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>`.
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 :ref:`Django security
policy <security-disclosure>`.

View File

@@ -41,3 +41,15 @@ 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>`.
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 :ref:`Django security
policy <security-disclosure>`.

View File

@@ -42,6 +42,18 @@ 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>`.
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 :ref:`Django security
policy <security-disclosure>`.
Bugfixes
========

View File

@@ -202,6 +202,16 @@ class TestUtilsText(SimpleTestCase):
truncator = text.Truncator("<p>I &lt;3 python, what about you?</p>")
self.assertEqual("<p>I &lt;3 python, wh…</p>", truncator.chars(16, html=True))
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>",
)
def test_truncate_words(self):
truncator = text.Truncator("The quick brown fox jumped over the lazy dog.")
self.assertEqual(