diff --git a/debian/changelog b/debian/changelog index fa89c8b21..47e10adb4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,59 @@ +python-django (1:1.10.7-2+deb9u5) stretch-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:42:49 +0100 + python-django (1:1.10.7-2+deb9u4) stretch-security; urgency=high * CVE-2019-3498: Prevent a content-spoofing vulnerability in the default diff --git a/debian/patches/0019-CVE-2019-14232.patch b/debian/patches/0019-CVE-2019-14232.patch new file mode 100644 index 000000000..3bccb924e --- /dev/null +++ b/debian/patches/0019-CVE-2019-14232.patch @@ -0,0 +1,89 @@ +From: Chris Lamb +Date: Thu, 8 Aug 2019 10:30:35 +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 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