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

Skip to content

Commit c7b7024

Browse files
committed
[4.1.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 910df41 commit c7b7024

File tree

5 files changed

+95
-11
lines changed

5 files changed

+95
-11
lines changed

django/utils/text.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,14 @@ def _generator():
6565
class Truncator(SimpleLazyObject):
6666
"""
6767
An object used to truncate text, either by characters or words.
68+
69+
When truncating HTML text (either chars or words), input will be limited to
70+
at most `MAX_LENGTH_HTML` characters.
6871
"""
6972

73+
# 5 million characters are approximately 4000 text pages or 3 web pages.
74+
MAX_LENGTH_HTML = 5_000_000
75+
7076
def __init__(self, text):
7177
super().__init__(lambda: str(text))
7278

@@ -162,6 +168,11 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
162168
if words and length <= 0:
163169
return ""
164170

171+
size_limited = False
172+
if len(text) > self.MAX_LENGTH_HTML:
173+
text = text[: self.MAX_LENGTH_HTML]
174+
size_limited = True
175+
165176
html4_singlets = (
166177
"br",
167178
"col",
@@ -218,10 +229,14 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
218229
# Add it to the start of the open tags list
219230
open_tags.insert(0, tagname)
220231

232+
truncate_text = self.add_truncation_text("", truncate)
233+
221234
if current_len <= length:
235+
if size_limited and truncate_text:
236+
text += truncate_text
222237
return text
238+
223239
out = text[:end_text_pos]
224-
truncate_text = self.add_truncation_text("", truncate)
225240
if truncate_text:
226241
out += truncate_text
227242
# Close any tags still open

docs/ref/templates/builtins.txt

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

23542354
Newlines in the HTML content will be preserved.
23552355

2356+
.. admonition:: Size of input string
2357+
2358+
Processing large, potentially malformed HTML strings can be
2359+
resource-intensive and impact service performance. ``truncatechars_html``
2360+
limits input to the first five million characters.
2361+
2362+
.. versionchanged:: 3.2.22
2363+
2364+
In older versions, strings over five million characters were processed.
2365+
23562366
.. templatefilter:: truncatewords
23572367

23582368
``truncatewords``
@@ -2391,6 +2401,16 @@ If ``value`` is ``"<p>Joel is a slug</p>"``, the output will be
23912401

23922402
Newlines in the HTML content will be preserved.
23932403

2404+
.. admonition:: Size of input string
2405+
2406+
Processing large, potentially malformed HTML strings can be
2407+
resource-intensive and impact service performance. ``truncatewords_html``
2408+
limits input to the first five million characters.
2409+
2410+
.. versionchanged:: 3.2.22
2411+
2412+
In older versions, strings over five million characters were processed.
2413+
23942414
.. templatefilter:: unordered_list
23952415

23962416
``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.

docs/releases/4.1.12.txt

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

77
Django 4.1.12 fixes a security issue with severity "moderate" in 4.1.11.
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: 25 additions & 8 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
@@ -94,11 +95,17 @@ def test_truncate_chars(self):
9495
text.Truncator(lazystr("The quick brown fox")).chars(10), "The quick…"
9596
)
9697

97-
def test_truncate_chars_html(self):
98+
@patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
99+
def test_truncate_chars_html_size_limit(self):
100+
max_len = text.Truncator.MAX_LENGTH_HTML
101+
bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
102+
valid_html = "<p>Joel is a slug</p>" # 14 chars
98103
perf_test_values = [
99-
(("</a" + "\t" * 50000) + "//>", None),
100-
("&" * 50000, "&" * 9 + "…"),
104+
("</a" + "\t" * (max_len - 6) + "//>", None),
105+
("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * 6 + "…"),
106+
("&" * bigger_len, "&" * 9 + "…"),
101107
("_X<<<<<<<<<<<>", None),
108+
(valid_html * bigger_len, "<p>Joel is a…</p>"), # 10 chars
102109
]
103110
for value, expected in perf_test_values:
104111
with self.subTest(value=value):
@@ -176,15 +183,25 @@ def test_truncate_html_words(self):
176183
truncator = text.Truncator("<p>I &lt;3 python, what about you?</p>")
177184
self.assertEqual("<p>I &lt;3 python,…</p>", truncator.words(3, html=True))
178185

186+
@patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
187+
def test_truncate_words_html_size_limit(self):
188+
max_len = text.Truncator.MAX_LENGTH_HTML
189+
bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
190+
valid_html = "<p>Joel is a slug</p>" # 4 words
179191
perf_test_values = [
180-
("</a" + "\t" * 50000) + "//>",
181-
"&" * 50000,
182-
"_X<<<<<<<<<<<>",
192+
("</a" + "\t" * (max_len - 6) + "//>", None),
193+
("</p" + "\t" * bigger_len + "//>", "</p" + "\t" * (max_len - 3) + "…"),
194+
("&" * max_len, None), # no change
195+
("&" * bigger_len, "&" * max_len + "…"),
196+
("_X<<<<<<<<<<<>", None),
197+
(valid_html * bigger_len, valid_html * 12 + "<p>Joel is…</p>"), # 50 words
183198
]
184-
for value in perf_test_values:
199+
for value, expected in perf_test_values:
185200
with self.subTest(value=value):
186201
truncator = text.Truncator(value)
187-
self.assertEqual(value, truncator.words(50, html=True))
202+
self.assertEqual(
203+
expected if expected else value, truncator.words(50, html=True)
204+
)
188205

189206
def test_wrap(self):
190207
digits = "1234 67 9"

0 commit comments

Comments
 (0)