Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ccdade1

Browse files
committed
[3.2.x] Fixed CVE-2023-43665 -- Mitigated potential DoS in django.utils.text.Truncator when truncating HTML text.
Thanks Wenchao Li of Alibaba Group for the report.
1 parent 6caf7b3 commit ccdade1

File tree

4 files changed

+80
-11
lines changed

4 files changed

+80
-11
lines changed

django/utils/text.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,14 @@ def _generator():
6060
class Truncator(SimpleLazyObject):
6161
"""
6262
An object used to truncate text, either by characters or words.
63+
64+
When truncating HTML text (either chars or words), input will be limited to
65+
at most `MAX_LENGTH_HTML` characters.
6366
"""
67+
68+
# 5 million characters are approximately 4000 text pages or 3 web pages.
69+
MAX_LENGTH_HTML = 5_000_000
70+
6471
def __init__(self, text):
6572
super().__init__(lambda: str(text))
6673

@@ -157,6 +164,11 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
157164
if words and length <= 0:
158165
return ''
159166

167+
size_limited = False
168+
if len(text) > self.MAX_LENGTH_HTML:
169+
text = text[: self.MAX_LENGTH_HTML]
170+
size_limited = True
171+
160172
html4_singlets = (
161173
'br', 'col', 'link', 'base', 'img',
162174
'param', 'area', 'hr', 'input'
@@ -206,10 +218,14 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
206218
# Add it to the start of the open tags list
207219
open_tags.insert(0, tagname)
208220

221+
truncate_text = self.add_truncation_text("", truncate)
222+
209223
if current_len <= length:
224+
if size_limited and truncate_text:
225+
text += truncate_text
210226
return text
227+
211228
out = text[:end_text_pos]
212-
truncate_text = self.add_truncation_text('', truncate)
213229
if truncate_text:
214230
out += truncate_text
215231
# Close any tags still open

docs/ref/templates/builtins.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2348,6 +2348,16 @@ If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
23482348

23492349
Newlines in the HTML content will be preserved.
23502350

2351+
.. admonition:: Size of input string
2352+
2353+
Processing large, potentially malformed HTML strings can be
2354+
resource-intensive and impact service performance. ``truncatechars_html``
2355+
limits input to the first five million characters.
2356+
2357+
.. versionchanged:: 3.2.22
2358+
2359+
In older versions, strings over five million characters were processed.
2360+
23512361
.. templatefilter:: truncatewords
23522362

23532363
``truncatewords``
@@ -2386,6 +2396,16 @@ If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
23862396

23872397
Newlines in the HTML content will be preserved.
23882398

2399+
.. admonition:: Size of input string
2400+
2401+
Processing large, potentially malformed HTML strings can be
2402+
resource-intensive and impact service performance. ``truncatewords_html``
2403+
limits input to the first five million characters.
2404+
2405+
.. versionchanged:: 3.2.22
2406+
2407+
In older versions, strings over five million characters were processed.
2408+
23892409
.. templatefilter:: unordered_list
23902410

23912411
``unordered_list``

docs/releases/3.2.22.txt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,20 @@ Django 3.2.22 release notes
66

77
Django 3.2.22 fixes a security issue with severity "moderate" in 3.2.21.
88

9-
...
9+
CVE-2023-43665: Denial-of-service possibility in ``django.utils.text.Truncator``
10+
================================================================================
11+
12+
Following the fix for :cve:`2019-14232`, the regular expressions used in the
13+
implementation of ``django.utils.text.Truncator``'s ``chars()`` and ``words()``
14+
methods (with ``html=True``) were revised and improved. However, these regular
15+
expressions still exhibited linear backtracking complexity, so when given a
16+
very long, potentially malformed HTML input, the evaluation would still be
17+
slow, leading to a potential denial of service vulnerability.
18+
19+
The ``chars()`` and ``words()`` methods are used to implement the
20+
:tfilter:`truncatechars_html` and :tfilter:`truncatewords_html` template
21+
filters, which were thus also vulnerable.
22+
23+
The input processed by ``Truncator``, when operating in HTML mode, has been
24+
limited to the first five million characters in order to avoid potential
25+
performance and memory issues.

tests/utils_tests/test_text.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import sys
3+
from unittest.mock import patch
34

45
from django.core.exceptions import SuspiciousFileOperation
56
from django.test import SimpleTestCase, ignore_warnings
@@ -90,11 +91,17 @@ def test_truncate_chars(self):
9091
# lazy strings are handled correctly
9192
self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(10), 'The quick…')
9293

93-
def test_truncate_chars_html(self):
94+
@patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
95+
def test_truncate_chars_html_size_limit(self):
96+
max_len = text.Truncator.MAX_LENGTH_HTML
97+
bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
98+
valid_html = "<p>Joel is a slug</p>" # 14 chars
9499
perf_test_values = [
95-
(('</a' + '\t' * 50000) + '//>', None),
96-
('&' * 50000, '&' * 9 + '…'),
97-
('_X<<<<<<<<<<<>', None),
100+
("</a" + "\t" * (max_len - 6) + "//>", None),
101+
("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * 6 + "…"),
102+
("&" * bigger_len, "&" * 9 + "…"),
103+
("_X<<<<<<<<<<<>", None),
104+
(valid_html * bigger_len, "<p>Joel is a…</p>"), # 10 chars
98105
]
99106
for value, expected in perf_test_values:
100107
with self.subTest(value=value):
@@ -152,15 +159,25 @@ def test_truncate_html_words(self):
152159
truncator = text.Truncator('<p>I &lt;3 python, what about you?</p>')
153160
self.assertEqual('<p>I &lt;3 python,…</p>', truncator.words(3, html=True))
154161

162+
@patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
163+
def test_truncate_words_html_size_limit(self):
164+
max_len = text.Truncator.MAX_LENGTH_HTML
165+
bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
166+
valid_html = "<p>Joel is a slug</p>" # 4 words
155167
perf_test_values = [
156-
('</a' + '\t' * 50000) + '//>',
157-
'&' * 50000,
158-
'_X<<<<<<<<<<<>',
168+
("</a" + "\t" * (max_len - 6) + "//>", None),
169+
("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * (max_len - 3) + "…"),
170+
("&" * max_len, None), # no change
171+
("&" * bigger_len, "&" * max_len + "…"),
172+
("_X<<<<<<<<<<<>", None),
173+
(valid_html * bigger_len, valid_html * 12 + "<p>Joel is…</p>"), # 50 words
159174
]
160-
for value in perf_test_values:
175+
for value, expected in perf_test_values:
161176
with self.subTest(value=value):
162177
truncator = text.Truncator(value)
163-
self.assertEqual(value, truncator.words(50, html=True))
178+
self.assertEqual(
179+
expected if expected else value, truncator.words(50, html=True)
180+
)
164181

165182
def test_wrap(self):
166183
digits = '1234 67 9'

0 commit comments

Comments
 (0)