+---
+ django/utils/text.py | 4 ++--
+ .../filter_tests/test_truncatewords_html.py | 4 ++--
+ tests/utils_tests/test_text.py | 23 ++++++++++++++++++----
+ 3 files changed, 23 insertions(+), 8 deletions(-)
+
+diff --git a/django/utils/text.py b/django/utils/text.py
+index 5e4dd3d..a69cf7a 100644
+--- a/django/utils/text.py
++++ b/django/utils/text.py
+@@ -24,8 +24,8 @@ def capfirst(x):
+ capfirst = keep_lazy_text(capfirst)
+
+ # 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/tests/template_tests/filter_tests/test_truncatewords_html.py b/tests/template_tests/filter_tests/test_truncatewords_html.py
+index aec2abf..3c73442 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('one two - three
four five
', 4),
+- 'one two - three
four ...
',
++ 'one two - three ...
',
+ )
+
+ def test_truncate3(self):
+ self.assertEqual(
+ truncatewords_html('one two - three
four five
', 5),
+- 'one two - three
four five
',
++ 'one two - three
four ...
',
+ )
+
+ def test_truncate4(self):
+diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
+index 2e1bc70..7e56bbf 100644
+--- a/tests/utils_tests/test_text.py
++++ b/tests/utils_tests/test_text.py
+@@ -90,6 +90,16 @@ class TestUtilsText(SimpleTestCase):
+ # Ensure that lazy strings are handled correctly
+ self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(12), 'The quick...')
+
++ def test_truncate_chars_html(self):
++ perf_test_values = [
++ (('', 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.')
+ self.assertEqual('The quick brown fox jumped over the lazy dog.', truncator.words(10))
+@@ -139,11 +149,16 @@ class TestUtilsText(SimpleTestCase):
+ truncator = text.Truncator('Buenos días! ¿Cómo está?')
+ self.assertEqual('Buenos días! ¿Cómo...', truncator.words(3, '...', html=True))
+ truncator = text.Truncator('I <3 python, what about you?
')
+- self.assertEqual('I <3 python...
', truncator.words(3, '...', html=True))
++ self.assertEqual('I <3 python,...
', truncator.words(3, '...', html=True))
+
+- re_tag_catastrophic_test = (''
+- truncator = text.Truncator(re_tag_catastrophic_test)
+- self.assertEqual(re_tag_catastrophic_test, truncator.words(500, html=True))
++ perf_test_values = [
++ ('',
++ '&' * 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'
diff --git a/debian/patches/0020-CVE-2019-14233.patch b/debian/patches/0020-CVE-2019-14233.patch
new file mode 100644
index 000000000..9fff26ef0
--- /dev/null
+++ b/debian/patches/0020-CVE-2019-14233.patch
@@ -0,0 +1,39 @@
+From: Chris Lamb
+Date: Thu, 8 Aug 2019 10:31:08 +0100
+Subject: CVE-2019-14233
+
+Backported from
+
+---
+ django/utils/html.py | 4 ++--
+ tests/utils_tests/test_html.py | 2 ++
+ 2 files changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/django/utils/html.py b/django/utils/html.py
+index 5a9f735..3fb791c 100644
+--- a/django/utils/html.py
++++ b/django/utils/html.py
+@@ -175,8 +175,8 @@ def strip_tags(value):
+ value = force_text(value)
+ while '<' in value and '>' in value:
+ new_value = _strip_once(value)
+- if len(new_value) >= len(value):
+- # _strip_once was not able to detect more tags or length increased
++ if len(new_value) >= len(value) or value.count('<') == new_value.count('<'):
++ # _strip_once wasn't able to detect more tags, or line length increased.
+ # due to http://bugs.python.org/issue20288
+ # (affects Python 2 < 2.7.7 and Python 3 < 3.3.5)
+ break
+diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
+index 8b683c1..56c380c 100644
+--- a/tests/utils_tests/test_html.py
++++ b/tests/utils_tests/test_html.py
+@@ -86,6 +86,8 @@ class TestUtilsHtml(SimpleTestCase):
+ # caused infinite loop on Pythons not patched with
+ # http://bugs.python.org/issue20288
+ ('&gotcha<>', '&gotcha<>'),
++ ('>br>br>br>X', 'XX'),
+ )
+ for value, output in items:
+ self.check_output(f, value, output)
diff --git a/debian/patches/0021-CVE-2019-14234.patch b/debian/patches/0021-CVE-2019-14234.patch
new file mode 100644
index 000000000..c7096f37a
--- /dev/null
+++ b/debian/patches/0021-CVE-2019-14234.patch
@@ -0,0 +1,116 @@
+From: Chris Lamb
+Date: Thu, 8 Aug 2019 10:35:56 +0100
+Subject: CVE-2019-14234
+
+Backported from
+
+---
+ django/contrib/postgres/fields/hstore.py | 2 +-
+ django/contrib/postgres/fields/jsonb.py | 8 +++-----
+ tests/postgres_tests/test_hstore.py | 14 ++++++++++++++
+ tests/postgres_tests/test_json.py | 15 ++++++++++++++-
+ 4 files changed, 32 insertions(+), 7 deletions(-)
+
+diff --git a/django/contrib/postgres/fields/hstore.py b/django/contrib/postgres/fields/hstore.py
+index 8322d81..6d6f542 100644
+--- a/django/contrib/postgres/fields/hstore.py
++++ b/django/contrib/postgres/fields/hstore.py
+@@ -85,7 +85,7 @@ class KeyTransform(Transform):
+
+ def as_sql(self, compiler, connection):
+ lhs, params = compiler.compile(self.lhs)
+- return "(%s -> '%s')" % (lhs, self.key_name), params
++ return '(%s -> %%s)' % lhs, [self.key_name] + params
+
+
+ class KeyTransformFactory(object):
+diff --git a/django/contrib/postgres/fields/jsonb.py b/django/contrib/postgres/fields/jsonb.py
+index ae83d9e..6d30b7a 100644
+--- a/django/contrib/postgres/fields/jsonb.py
++++ b/django/contrib/postgres/fields/jsonb.py
+@@ -75,12 +75,10 @@ class KeyTransform(Transform):
+ if len(key_transforms) > 1:
+ return "{} #> %s".format(lhs), [key_transforms] + params
+ try:
+- int(self.key_name)
++ lookup = int(self.key_name)
+ except ValueError:
+- lookup = "'%s'" % self.key_name
+- else:
+- lookup = "%s" % self.key_name
+- return "%s -> %s" % (lhs, lookup), params
++ lookup = self.key_name
++ return '(%s %s %%s)' % (lhs, self.operator), [lookup] + params
+
+
+ class KeyTransformFactory(object):
+diff --git a/tests/postgres_tests/test_hstore.py b/tests/postgres_tests/test_hstore.py
+index 0afa630..fbcd32c 100644
+--- a/tests/postgres_tests/test_hstore.py
++++ b/tests/postgres_tests/test_hstore.py
+@@ -3,8 +3,10 @@ from __future__ import unicode_literals
+
+ import json
+
++from django.db import connection
+ from django.core import exceptions, serializers
+ from django.forms import Form
++from django.test.utils import CaptureQueriesContext
+
+ from . import PostgreSQLTestCase
+ from .models import HStoreModel
+@@ -163,6 +165,18 @@ class TestQuerying(PostgreSQLTestCase):
+ self.objs[:2]
+ )
+
++ def test_key_sql_injection(self):
++ with CaptureQueriesContext(connection) as queries:
++ self.assertFalse(
++ HStoreModel.objects.filter(**{
++ "field__test' = 'a') OR 1 = 1 OR ('d": 'x',
++ }).exists()
++ )
++ self.assertIn(
++ """."field" -> 'test'' = ''a'') OR 1 = 1 OR (''d') = 'x' """,
++ queries[0]['sql'],
++ )
++
+
+ class TestSerialization(PostgreSQLTestCase):
+ test_data = ('[{"fields": {"field": "{\\"a\\": \\"b\\"}"}, '
+diff --git a/tests/postgres_tests/test_json.py b/tests/postgres_tests/test_json.py
+index 1978552..216fe77 100644
+--- a/tests/postgres_tests/test_json.py
++++ b/tests/postgres_tests/test_json.py
+@@ -1,10 +1,11 @@
+ import datetime
+ import unittest
+
+-from django.core import exceptions, serializers
+ from django.db import connection
++from django.core import exceptions, serializers
+ from django.forms import CharField, Form
+ from django.test import TestCase
++from django.test.utils import CaptureQueriesContext
+ from django.utils.html import escape
+
+ from . import PostgreSQLTestCase
+@@ -236,6 +237,18 @@ class TestValidation(PostgreSQLTestCase):
+ self.assertEqual(cm.exception.code, 'invalid')
+ self.assertEqual(cm.exception.message % cm.exception.params, "Value must be valid JSON.")
+
++ def test_key_sql_injection(self):
++ with CaptureQueriesContext(connection) as queries:
++ self.assertFalse(
++ JSONModel.objects.filter(**{
++ """field__test' = '"a"') OR 1 = 1 OR ('d""": 'x',
++ }).exists()
++ )
++ self.assertIn(
++ """."field" -> 'test'' = ''"a"'') OR 1 = 1 OR (''d') = '"x"' """,
++ queries[0]['sql'],
++ )
++
+
+ class TestFormField(PostgreSQLTestCase):
+
diff --git a/debian/patches/0022-CVE-2019-14235.patch b/debian/patches/0022-CVE-2019-14235.patch
new file mode 100644
index 000000000..eba780db4
--- /dev/null
+++ b/debian/patches/0022-CVE-2019-14235.patch
@@ -0,0 +1,74 @@
+From: Chris Lamb
+Date: Thu, 8 Aug 2019 10:36:25 +0100
+Subject: CVE-2019-14235
+
+Backported from
+
+---
+ django/utils/encoding.py | 17 ++++++++++-------
+ tests/utils_tests/test_encoding.py | 12 +++++++++++-
+ 2 files changed, 21 insertions(+), 8 deletions(-)
+
+diff --git a/django/utils/encoding.py b/django/utils/encoding.py
+index 66077e2..2a03d10 100644
+--- a/django/utils/encoding.py
++++ b/django/utils/encoding.py
+@@ -236,13 +236,16 @@ def repercent_broken_unicode(path):
+ we need to re-percent-encode any octet produced that is not part of a
+ strictly legal UTF-8 octet sequence.
+ """
+- try:
+- path.decode('utf-8')
+- except UnicodeDecodeError as e:
+- repercent = quote(path[e.start:e.end], safe=b"/#%[]=:;$&()+,!?*@'~")
+- path = repercent_broken_unicode(
+- path[:e.start] + force_bytes(repercent) + path[e.end:])
+- return path
++ while True:
++ try:
++ path.decode('utf-8')
++ except UnicodeDecodeError as e:
++ # CVE-2019-14235: A recursion shouldn't be used since the exception
++ # handling uses massive amounts of memory
++ repercent = quote(path[e.start:e.end], safe=b"/#%[]=:;$&()+,!?*@'~")
++ path = path[:e.start] + force_bytes(repercent) + path[e.end:]
++ else:
++ return path
+
+
+ def filepath_to_uri(path):
+diff --git a/tests/utils_tests/test_encoding.py b/tests/utils_tests/test_encoding.py
+index 5ddb18d..df4cc9d 100644
+--- a/tests/utils_tests/test_encoding.py
++++ b/tests/utils_tests/test_encoding.py
+@@ -2,12 +2,13 @@
+ from __future__ import unicode_literals
+
+ import datetime
++import sys
+ import unittest
+
+ from django.utils import six
+ from django.utils.encoding import (
+ escape_uri_path, filepath_to_uri, force_bytes, force_text, iri_to_uri,
+- smart_text, uri_to_iri,
++ repercent_broken_unicode, smart_text, uri_to_iri,
+ )
+ from django.utils.functional import SimpleLazyObject
+ from django.utils.http import urlquote_plus
+@@ -76,6 +77,15 @@ class TestEncodingUtils(unittest.TestCase):
+ self.assertEqual(smart_text(1), '1')
+ self.assertEqual(smart_text('foo'), 'foo')
+
++ def test_repercent_broken_unicode_recursion_error(self):
++ # Prepare a string long enough to force a recursion error if the tested
++ # function uses recursion.
++ data = b'\xfc' * sys.getrecursionlimit()
++ try:
++ self.assertEqual(repercent_broken_unicode(data), b'%FC' * sys.getrecursionlimit())
++ except RecursionError:
++ self.fail('Unexpected RecursionError raised.')
++
+
+ class TestRFC3987IEncodingUtils(unittest.TestCase):
+
diff --git a/debian/patches/series b/debian/patches/series
index ad6685673..38e455694 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -10,3 +10,7 @@ fix-test-middleware-classes-headers.patch
0006-Default-to-supporting-Spatialite-4.2.patch
0017-CVE-2019-3498.patch
0018-CVE-2019-6975.patch
+0019-CVE-2019-14232.patch
+0020-CVE-2019-14233.patch
+0021-CVE-2019-14234.patch
+0022-CVE-2019-14235.patch