diff --git a/debian/changelog b/debian/changelog index fa89c8b21..5bb1d6625 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +python-django (1:1.10.7-2+deb9u5) stretch-security; urgency=high + + * CVE-2019-6975: Fix memory exhaustion in utils.numberformat.format. + (Closes: #922027) + * CVE-2019-12308: Prevent a XSS vulnerability in the Django admin via the + AdminURLFieldWidget. (Closes: #929927) + * CVE-2019-12781: Prevent incorrect HTTPS detection with reverse-proxies + connecting via HTTPS. (Closes: #931316) + + -- Chris Lamb Tue, 02 Jul 2019 23:07:21 -0300 + 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/0018-CVE-2019-6975.patch b/debian/patches/0018-CVE-2019-6975.patch new file mode 100644 index 000000000..39c2f864c --- /dev/null +++ b/debian/patches/0018-CVE-2019-6975.patch @@ -0,0 +1,69 @@ +From: Carlton Gibson +Date: Mon, 11 Feb 2019 11:15:45 +0100 +Subject: Fixed CVE-2019-6975 -- Fixed memory exhaustion in + utils.numberformat.format(). + +Thanks Sjoerd Job Postmus for the report and initial patch. +Thanks Michael Manfre, Tim Graham, and Florian Apolloner for review. + +Backport of 402c0caa851e265410fbcaa55318f22d2bf22ee2 from master. +--- + django/utils/numberformat.py | 15 ++++++++++++++- + tests/utils_tests/test_numberformat.py | 18 ++++++++++++++++++ + 2 files changed, 32 insertions(+), 1 deletion(-) + +diff --git a/django/utils/numberformat.py b/django/utils/numberformat.py +index 6667d82..8b4d228 100644 +--- a/django/utils/numberformat.py ++++ b/django/utils/numberformat.py +@@ -27,7 +27,20 @@ def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='', + # sign + sign = '' + if isinstance(number, Decimal): +- str_number = '{:f}'.format(number) ++ # Format values with more than 200 digits (an arbitrary cutoff) using ++ # scientific notation to avoid high memory usage in {:f}'.format(). ++ _, digits, exponent = number.as_tuple() ++ if abs(exponent) + len(digits) > 200: ++ number = '{:e}'.format(number) ++ coefficient, exponent = number.split('e') ++ # Format the coefficient. ++ coefficient = format( ++ coefficient, decimal_sep, decimal_pos, grouping, ++ thousand_sep, force_grouping, ++ ) ++ return '{}e{}'.format(coefficient, exponent) ++ else: ++ str_number = '{:f}'.format(number) + else: + str_number = six.text_type(number) + if str_number[0] == '-': +diff --git a/tests/utils_tests/test_numberformat.py b/tests/utils_tests/test_numberformat.py +index 3dd1b06..769406c 100644 +--- a/tests/utils_tests/test_numberformat.py ++++ b/tests/utils_tests/test_numberformat.py +@@ -60,6 +60,24 @@ class TestNumberFormat(TestCase): + self.assertEqual(nformat(Decimal('1234'), '.', grouping=2, thousand_sep=',', force_grouping=True), '12,34') + self.assertEqual(nformat(Decimal('-1234.33'), '.', decimal_pos=1), '-1234.3') + self.assertEqual(nformat(Decimal('0.00000001'), '.', decimal_pos=8), '0.00000001') ++ # Very large & small numbers. ++ tests = [ ++ ('9e9999', None, '9e+9999'), ++ ('9e9999', 3, '9.000e+9999'), ++ ('9e201', None, '9e+201'), ++ ('9e200', None, '9e+200'), ++ ('1.2345e999', 2, '1.23e+999'), ++ ('9e-999', None, '9e-999'), ++ ('1e-7', 8, '0.00000010'), ++ ('1e-8', 8, '0.00000001'), ++ ('1e-9', 8, '0.00000000'), ++ ('1e-10', 8, '0.00000000'), ++ ('1e-11', 8, '0.00000000'), ++ ('1' + ('0' * 300), 3, '1.000e+300'), ++ ('0.{}1234'.format('0' * 299), 3, '1.234e-300'), ++ ] ++ for value, decimal_pos, expected_value in tests: ++ self.assertEqual(nformat(Decimal(value), '.', decimal_pos), expected_value) + + def test_decimal_subclass(self): + class EuroDecimal(Decimal): diff --git a/debian/patches/0019-CVE-2019-12308.patch b/debian/patches/0019-CVE-2019-12308.patch new file mode 100644 index 000000000..d3e73f45d --- /dev/null +++ b/debian/patches/0019-CVE-2019-12308.patch @@ -0,0 +1,77 @@ +From: Chris Lamb +Date: Tue, 2 Jul 2019 22:47:00 -0300 +Subject: CVE-2019-12308 + +Backported from https://github.com/django/django/commit/c238701859a52d584f349cce15d56c8e8137c52b + +--- + django/contrib/admin/widgets.py | 11 +++++++++-- + tests/admin_widgets/tests.py | 16 ++++++---------- + 2 files changed, 15 insertions(+), 12 deletions(-) + +diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py +index c228a3c..26de9c7 100644 +--- a/django/contrib/admin/widgets.py ++++ b/django/contrib/admin/widgets.py +@@ -7,6 +7,8 @@ import copy + + from django import forms + from django.db.models.deletion import CASCADE ++from django.core.exceptions import ValidationError ++from django.core.validators import URLValidator + from django.forms.utils import flatatt + from django.forms.widgets import RadioFieldRenderer + from django.template.loader import render_to_string +@@ -369,15 +371,20 @@ class AdminEmailInputWidget(forms.EmailInput): + + + class AdminURLFieldWidget(forms.URLInput): +- def __init__(self, attrs=None): ++ def __init__(self, attrs=None, validator_class=URLValidator): + final_attrs = {'class': 'vURLField'} + if attrs is not None: + final_attrs.update(attrs) + super(AdminURLFieldWidget, self).__init__(attrs=final_attrs) ++ self.validator = validator_class() + + def render(self, name, value, attrs=None): + html = super(AdminURLFieldWidget, self).render(name, value, attrs) +- if value: ++ try: ++ self.validator(value if value else '') ++ except ValidationError: ++ pass ++ else: + value = force_text(self.format_value(value)) + final_attrs = {'href': smart_urlquote(value)} + html = format_html( +diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py +index 308ec17..5a75c67 100644 +--- a/tests/admin_widgets/tests.py ++++ b/tests/admin_widgets/tests.py +@@ -377,19 +377,15 @@ class AdminURLWidgetTest(SimpleTestCase): + w = widgets.AdminURLFieldWidget() + self.assertEqual( + w.render('test', 'http://example.com/some text'), +- '

Currently: ' +- '' +- 'http://example.com/<sometag>some text</sometag>
' +- 'Change:

' ++ '' + ) + self.assertEqual( + w.render('test', 'http://example-äüö.com/some text'), +- '

Currently: ' +- '' +- 'http://example-äüö.com/<sometag>some text</sometag>
' +- 'Change:

' ++ '' + ) + self.assertEqual( + w.render('test', 'http://www.example.com/%C3%A4">"'), diff --git a/debian/patches/0020-CVE-2019-12781.patch b/debian/patches/0020-CVE-2019-12781.patch new file mode 100644 index 000000000..2d3e22c26 --- /dev/null +++ b/debian/patches/0020-CVE-2019-12781.patch @@ -0,0 +1,55 @@ +From: Chris Lamb +Date: Tue, 2 Jul 2019 23:02:23 -0300 +Subject: CVE-2019-12781 + +Backport of https://github.com/django/django/commit/32124fc41e75074141b05f10fc55a4f01ff7f050 +--- + django/http/request.py | 7 ++++--- + tests/settings_tests/tests.py | 12 ++++++++++++ + 2 files changed, 16 insertions(+), 3 deletions(-) + +diff --git a/django/http/request.py b/django/http/request.py +index 8c32af5..87b4fca 100644 +--- a/django/http/request.py ++++ b/django/http/request.py +@@ -199,13 +199,14 @@ class HttpRequest(object): + def scheme(self): + if settings.SECURE_PROXY_SSL_HEADER: + try: +- header, value = settings.SECURE_PROXY_SSL_HEADER ++ header, secure_value = settings.SECURE_PROXY_SSL_HEADER + except ValueError: + raise ImproperlyConfigured( + 'The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.' + ) +- if self.META.get(header) == value: +- return 'https' ++ header_value = self.META.get(header) ++ if header_value is not None: ++ return 'https' if header_value == secure_value else 'http' + return self._get_scheme() + + def is_secure(self): +diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py +index 97d734e..f0f1fe5 100644 +--- a/tests/settings_tests/tests.py ++++ b/tests/settings_tests/tests.py +@@ -419,6 +419,18 @@ class SecureProxySslHeaderTest(SimpleTestCase): + req.META['HTTP_X_FORWARDED_PROTOCOL'] = 'https' + self.assertIs(req.is_secure(), True) + ++ @override_settings(SECURE_PROXY_SSL_HEADER=('HTTP_X_FORWARDED_PROTOCOL', 'https')) ++ def test_xheader_preferred_to_underlying_request(self): ++ class ProxyRequest(HttpRequest): ++ def _get_scheme(self): ++ """Proxy always connecting via HTTPS""" ++ return 'https' ++ ++ # Client connects via HTTP. ++ req = ProxyRequest() ++ req.META['HTTP_X_FORWARDED_PROTOCOL'] = 'http' ++ self.assertIs(req.is_secure(), False) ++ + + class IsOverriddenTest(SimpleTestCase): + def test_configure(self): diff --git a/debian/patches/series b/debian/patches/series index 5bda383eb..96a095e53 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -9,3 +9,6 @@ fix-test-middleware-classes-headers.patch 0016-CVE-2017-12794.patch 0006-Default-to-supporting-Spatialite-4.2.patch 0017-CVE-2019-3498.patch +0018-CVE-2019-6975.patch +0019-CVE-2019-12308.patch +0020-CVE-2019-12781.patch