commit 067770b2c420277ad9bcdf075cdbcaf8a7c3d25c
Author: Florian Apolloner <florian@apolloner.eu>
Date:   Mon, 15 Jul 2019 11:46:09 +0200

    [1.8.X] Fixed CVE-2019-14232 -- Adjusted regex to avoid backtracking issues when truncating HTML.
    
    Thanks to Guido Vranken for initial report.

diff --git a/django/utils/text.py b/django/utils/text.py
index 49b6d91fb0..db7b5e00ad 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -25,8 +25,8 @@ capfirst = lambda x: x and force_text(x)[0].upper() + force_text(x)[1:]
 capfirst = allow_lazy(capfirst, six.text_type)
 
 # Set up regular expressions
-re_words = re.compile(r'<.*?>|((?:\w[-\w]*|&.*?;)+)', re.U | re.S)
-re_chars = re.compile(r'<.*?>|(.)', re.U | re.S)
+re_words = re.compile(r'<[^>]+?>|([^<>\s]+)', re.S)
+re_chars = re.compile(r'<[^>]+?>|(.)', re.S)
 re_tag = re.compile(r'<(/)?(\S+?)(?:(\s*/)|\s.*?)?>', re.S)
 re_newlines = re.compile(r'\r\n|\r')  # Used in normalize_newlines
 re_camel_case = re.compile(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
diff --git a/docs/releases/1.8.19.txt b/docs/releases/1.8.19.txt
index 96410a331c..bd64941485 100644
--- a/docs/releases/1.8.19.txt
+++ b/docs/releases/1.8.19.txt
@@ -28,3 +28,17 @@ expression. The ``chars()`` and ``words()`` methods are used to implement the
 thus vulnerable.
 
 The backtracking problem in the regular expression is fixed.
+
+CVE-2019-14232: Denial-of-service possibility in ``django.utils.text.Truncator``
+================================================================================
+
+If ``django.utils.text.Truncator``'s ``chars()`` and ``words()`` methods
+were passed the ``html=True`` argument, they were extremely slow to evaluate
+certain inputs due to a catastrophic backtracking vulnerability in a regular
+expression. The ``chars()`` and ``words()`` methods are used to implement the
+:tfilter:`truncatechars_html` and :tfilter:`truncatewords_html` template
+filters, which were thus vulnerable.
+
+The regular expressions used by ``Truncator`` have been simplified in order to
+avoid potential backtracking issues. As a consequence, trailing punctuation may
+now at times be included in the truncated output.
diff --git a/tests/template_tests/filter_tests/test_truncatewords_html.py b/tests/template_tests/filter_tests/test_truncatewords_html.py
index aec2abf2d4..3c73442fbe 100644
--- a/tests/template_tests/filter_tests/test_truncatewords_html.py
+++ b/tests/template_tests/filter_tests/test_truncatewords_html.py
@@ -19,13 +19,13 @@ class FunctionTests(SimpleTestCase):
     def test_truncate2(self):
         self.assertEqual(
             truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 4),
-            '<p>one <a href="#">two - three <br>four ...</a></p>',
+            '<p>one <a href="#">two - three ...</a></p>',
         )
 
     def test_truncate3(self):
         self.assertEqual(
             truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 5),
-            '<p>one <a href="#">two - three <br>four</a> five</p>',
+            '<p>one <a href="#">two - three <br>four ...</a></p>',
         )
 
     def test_truncate4(self):
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
index a66f9258c9..5fe66e7b90 100644
--- a/tests/utils_tests/test_text.py
+++ b/tests/utils_tests/test_text.py
@@ -98,6 +98,16 @@ class TestUtilsText(SimpleTestCase):
         # a length shorter than the ellipsis shouldn't break
         self.assertEqual('...', text.Truncator('asdf').chars(1))
 
+    def test_truncate_chars_html(self):
+        perf_test_values = [
+            (('</a' + '\t' * 50000) + '//>', None),
+            ('&' * 50000, '&' * 7 + '...'),
+            ('_X<<<<<<<<<<<>', None),
+        ]
+        for value, expected in perf_test_values:
+            truncator = text.Truncator(value)
+            self.assertEqual(expected if expected else value, truncator.chars(10, html=True))
+
     def test_truncate_words(self):
         truncator = text.Truncator('The quick brown fox jumped over the lazy '
             'dog.')
@@ -141,12 +151,17 @@ class TestUtilsText(SimpleTestCase):
         self.assertEqual('<i>Buenos d&iacute;as! &#x00bf;C&oacute;mo...</i>',
             truncator.words(3, '...', html=True))
         truncator = text.Truncator('<p>I &lt;3 python, what about you?</p>')
-        self.assertEqual('<p>I &lt;3 python...</p>',
+        self.assertEqual('<p>I &lt;3 python,...</p>',
             truncator.words(3, '...', html=True))
 
-        re_tag_catastrophic_test = ('</a' + '\t' * 50000) + '//>'
-        truncator = text.Truncator(re_tag_catastrophic_test)
-        self.assertEqual(re_tag_catastrophic_test, truncator.words(500, html=True))
+        perf_test_values = [
+            ('</a' + '\t' * 50000) + '//>',
+            '&' * 50000,
+            '_X<<<<<<<<<<<>',
+        ]
+        for value in perf_test_values:
+            truncator = text.Truncator(value)
+            self.assertEqual(value, truncator.words(50, html=True))
 
     def test_wrap(self):
         digits = '1234 67 9'
