diff --git a/debian/changelog b/debian/changelog index b048bd0ec..e16ca1821 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,59 @@ +python-django (1:1.11.22-1+deb10u1) buster-security; urgency=high + + * Backport four security patches from upstream. (Closes: #934026) + + + - 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 + truncatechars_html and 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. + + - CVE-2019-14233: Denial-of-service possibility in strip_tags() + + Due to the behavior of the underlying HTMLParser, + django.utils.html.strip_tags() would be extremely slow to evaluate + certain inputs containing large sequences of nested incomplete HTML + entities. The strip_tags() method is used to implement the corresponding + striptags template filter, which was thus also vulnerable. + + strip_tags() now avoids recursive calls to HTMLParser when progress + removing tags, but necessarily incomplete HTML entities, stops being + made. + + Remember that absolutely NO guarantee is provided about the results of + strip_tags() being HTML safe. So NEVER mark safe the result of a + strip_tags() call without escaping it first, for example with + django.utils.html.escape(). + + - CVE-2019-14234: SQL injection possibility in key and index lookups for + JSONField/HStoreField + + Key and index lookups for django.contrib.postgres.fields.JSONField and + key lookups for django.contrib.postgres.fields.HStoreField were subject + to SQL injection, using a suitably crafted dictionary, with dictionary + expansion, as the **kwargs passed to QuerySet.filter(). + + - CVE-2019-14235: Potential memory exhaustion in + django.utils.encoding.uri_to_iri() + + If passed certain inputs, django.utils.encoding.uri_to_iri could lead to + significant memory usage due to excessive recursion when + re-percent-encoding invalid UTF-8 octet sequences. + + uri_to_iri() now avoids recursion when re-percent-encoding invalid UTF-8 + octet sequences. + + -- Chris Lamb Thu, 08 Aug 2019 10:11:42 +0100 + python-django (1:1.11.22-1~deb10u1) buster-security; urgency=high * No-change update for buster-security. diff --git a/debian/patches/0007-CVE-2019-14232.patch b/debian/patches/0007-CVE-2019-14232.patch new file mode 100644 index 000000000..729f775fa --- /dev/null +++ b/debian/patches/0007-CVE-2019-14232.patch @@ -0,0 +1,90 @@ +From: Chris Lamb +Date: Thu, 8 Aug 2019 10:05:28 +0100 +Subject: CVE-2019-14232 + +Backported from + + +--- + 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