+
+---
+ 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 a6172c4..f221747 100644
+--- a/django/utils/text.py
++++ b/django/utils/text.py
+@@ -27,8 +27,8 @@ def capfirst(x):
+
+
+ # 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 50d1805..bfc1b4e 100644
+--- a/tests/utils_tests/test_text.py
++++ b/tests/utils_tests/test_text.py
+@@ -88,6 +88,16 @@ class TestUtilsText(SimpleTestCase):
+ # 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))
+@@ -137,11 +147,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/0008-CVE-2019-14233.patch b/debian/patches/0008-CVE-2019-14233.patch
new file mode 100644
index 000000000..43c140100
--- /dev/null
+++ b/debian/patches/0008-CVE-2019-14233.patch
@@ -0,0 +1,40 @@
+From: Chris Lamb
+Date: Thu, 8 Aug 2019 10:05:56 +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 9c38cde..30a6a2f 100644
+--- a/django/utils/html.py
++++ b/django/utils/html.py
+@@ -169,8 +169,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 1bebe94..6122b69 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/0009-CVE-2019-14234.patch b/debian/patches/0009-CVE-2019-14234.patch
new file mode 100644
index 000000000..8b3db95d2
--- /dev/null
+++ b/debian/patches/0009-CVE-2019-14234.patch
@@ -0,0 +1,115 @@
+From: Chris Lamb
+Date: Thu, 8 Aug 2019 10:06:24 +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 | 15 ++++++++++++++-
+ tests/postgres_tests/test_json.py | 14 ++++++++++++++
+ 4 files changed, 32 insertions(+), 7 deletions(-)
+
+diff --git a/django/contrib/postgres/fields/hstore.py b/django/contrib/postgres/fields/hstore.py
+index 605deaf..b77d1b1 100644
+--- a/django/contrib/postgres/fields/hstore.py
++++ b/django/contrib/postgres/fields/hstore.py
+@@ -86,7 +86,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 0722a05..d7a2259 100644
+--- a/django/contrib/postgres/fields/jsonb.py
++++ b/django/contrib/postgres/fields/jsonb.py
+@@ -104,12 +104,10 @@ class KeyTransform(Transform):
+ if len(key_transforms) > 1:
+ return "(%s %s %%s)" % (lhs, self.nested_operator), [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 %s)" % (lhs, self.operator, lookup), params
++ lookup = self.key_name
++ return '(%s %s %%s)' % (lhs, self.operator), [lookup] + params
+
+
+ class KeyTextTransform(KeyTransform):
+diff --git a/tests/postgres_tests/test_hstore.py b/tests/postgres_tests/test_hstore.py
+index 0fc427f..dd8e642 100644
+--- a/tests/postgres_tests/test_hstore.py
++++ b/tests/postgres_tests/test_hstore.py
+@@ -4,8 +4,9 @@ from __future__ import unicode_literals
+ import json
+
+ from django.core import exceptions, serializers
++from django.db import connection
+ from django.forms import Form
+-from django.test.utils import modify_settings
++from django.test.utils import CaptureQueriesContext, modify_settings
+
+ from . import PostgreSQLTestCase
+ from .models import HStoreModel
+@@ -167,6 +168,18 @@ class TestQuerying(HStoreTestCase):
+ 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(HStoreTestCase):
+ test_data = ('[{"fields": {"field": "{\\"a\\": \\"b\\"}"}, '
+diff --git a/tests/postgres_tests/test_json.py b/tests/postgres_tests/test_json.py
+index 4e8851d..925e800 100644
+--- a/tests/postgres_tests/test_json.py
++++ b/tests/postgres_tests/test_json.py
+@@ -6,8 +6,10 @@ from decimal import Decimal
+
+ from django.core import exceptions, serializers
+ from django.core.serializers.json import DjangoJSONEncoder
++from django.db import connection
+ from django.forms import CharField, Form, widgets
+ from django.test import skipUnlessDBFeature
++from django.test.utils import CaptureQueriesContext
+ from django.utils.html import escape
+
+ from . import PostgreSQLTestCase
+@@ -263,6 +265,18 @@ class TestQuerying(PostgreSQLTestCase):
+ def test_iregex(self):
+ self.assertTrue(JSONModel.objects.filter(field__foo__iregex=r'^bAr$').exists())
+
++ 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'],
++ )
++
+
+ @skipUnlessDBFeature('has_jsonb_datatype')
+ class TestSerialization(PostgreSQLTestCase):
diff --git a/debian/patches/0010-CVE-2019-14235.patch b/debian/patches/0010-CVE-2019-14235.patch
new file mode 100644
index 000000000..efd5be152
--- /dev/null
+++ b/debian/patches/0010-CVE-2019-14235.patch
@@ -0,0 +1,75 @@
+From: Chris Lamb
+Date: Thu, 8 Aug 2019 10:06:48 +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 999ffae..a29ef2b 100644
+--- a/django/utils/encoding.py
++++ b/django/utils/encoding.py
+@@ -237,13 +237,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 688b461..2b4bcff 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 59611e9f1..df4527b6e 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -4,3 +4,7 @@
0004-Fix-QuerySet.defer-with-super-and-subclass-fields.patch
0006-Default-to-supporting-Spatialite-4.2.patch
0007-Fixed-29182-Adjusted-SQLite-schema-table-alteration-.patch
+0007-CVE-2019-14232.patch
+0008-CVE-2019-14233.patch
+0009-CVE-2019-14234.patch
+0010-CVE-2019-14235.patch