[Python-modules-commits] [python-django] 09/10: merge debian/patched-jessie-updates into debian/jessie-updates

Raphaël Hertzog hertzog at moszumanska.debian.org
Mon Jul 25 07:57:04 UTC 2016


This is an automated email from the git hooks/post-receive script.

hertzog pushed a commit to branch debian/jessie-updates
in repository python-django.

commit def4d878e5000a904ee3314096b9e83a5ff22d9b
Merge: a6c7a94 39cf1a3
Author: Raphaël Hertzog <hertzog at debian.org>
Date:   Mon Jul 25 09:31:29 2016 +0200

    merge debian/patched-jessie-updates into debian/jessie-updates

 debian/.git-dpm                                    |   4 +-
 ...creation-of-_sources-directory-by-Sphinx.patch} |  10 +-
 ...l-page-to-refer-to-django-admin-instead-.patch} |  11 +-
 ...se-Debian-GeoIP-database-path-as-default.patch} |  18 +-
 ...2-Prevented-spoofing-is_safe_url-with-ba.patch} |  14 +-
 ...crashes-with-a-byestring-URL-on-Python-2.patch} |  15 +-
 ...3-Fixed-user-enumeration-timing-attack-d.patch} | 224 +++++++++++----------
 ...6-Fixed-XSS-in-admin-s-add-change-relate.patch} |  31 +--
 debian/patches/series                              |  14 +-
 9 files changed, 187 insertions(+), 154 deletions(-)

diff --cc debian/.git-dpm
index 27d87b4,0000000..1f5df47
mode 100644,000000..100644
--- a/debian/.git-dpm
+++ b/debian/.git-dpm
@@@ -1,11 -1,0 +1,11 @@@
 +# see git-dpm(1) from git-dpm package
- e4cd95fc1d5322f9bff209890b71f57ee9d36e62
- a471ae74d0b79b8896dc5411f40840ffa1737dc6
++39cf1a3b760df98cccba0278371f314f34686fde
++39cf1a3b760df98cccba0278371f314f34686fde
 +2d07f4b16101fcc8973128c4e4920b41f87175ee
 +2d07f4b16101fcc8973128c4e4920b41f87175ee
 +python-django_1.7.11.orig.tar.gz
 +f9abaf7eacec73bc1c5e6080e2778a7174ebf9d4
 +7586798
 +debianTag="debian/%e%v"
 +patchedTag="patched/%e%v"
 +upstreamTag="upstream/%e%u"
diff --cc debian/patches/0001-Disable-creation-of-_sources-directory-by-Sphinx.patch
index 1de2dd1,0000000..9848f11
mode 100644,000000..100644
--- a/debian/patches/0001-Disable-creation-of-_sources-directory-by-Sphinx.patch
+++ b/debian/patches/0001-Disable-creation-of-_sources-directory-by-Sphinx.patch
@@@ -1,35 -1,0 +1,31 @@@
- From 09dc3a85f5a42a5695687c8105cc39f884c19931 Mon Sep 17 00:00:00 2001
++From c994e4a87151ce844fe860c58cc4e335719eb62c Mon Sep 17 00:00:00 2001
 +From: =?UTF-8?q?Rapha=C3=ABl=20Hertzog?= <hertzog at debian.org>
- Date: Sun, 11 Oct 2015 12:06:13 +1100
++Date: Thu, 21 Jul 2016 04:32:58 +0200
 +Subject: Disable creation of _sources directory by Sphinx
 +
 + We do this to save some space as the sources of the documentation
 + are not really useful in a binary package.
 + .
 + This is a Debian specific patch.
- 
 +Forwarded: not-needed
- Author: Raphaël Hertzog <hertzog at debian.org>
 +Origin: vendor
- 
- Patch-Name: 02_disable-sources-in-sphinxdoc.diff
 +---
 + docs/conf.py | 5 ++++-
 + 1 file changed, 4 insertions(+), 1 deletion(-)
 +
 +diff --git a/docs/conf.py b/docs/conf.py
 +index 6df8dd8..90e4d69 100644
 +--- a/docs/conf.py
 ++++ b/docs/conf.py
- @@ -196,7 +196,10 @@ html_additional_pages = {}
++@@ -200,7 +200,10 @@ html_additional_pages = {}
 + #html_split_index = False
 + 
 + # If true, links to the reST sources are added to the pages.
 +-#html_show_sourcelink = True
 ++html_show_sourcelink = False
 ++
 ++# Do not ship a copy of the sources
 ++html_copy_source = False
 + 
 + # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
 + #html_show_sphinx = True
diff --cc debian/patches/0002-Update-manual-page-to-refer-to-django-admin-instead-.patch
index 098f767,0000000..37d35d3
mode 100644,000000..100644
--- a/debian/patches/0002-Update-manual-page-to-refer-to-django-admin-instead-.patch
+++ b/debian/patches/0002-Update-manual-page-to-refer-to-django-admin-instead-.patch
@@@ -1,29 -1,0 +1,32 @@@
- From fc3577d95827449a95635f3c733b164b8bbd43cd Mon Sep 17 00:00:00 2001
++From 7c0f2ff1f85febc62118aa15dd1b911a29e758b2 Mon Sep 17 00:00:00 2001
 +From: Brett Parker <iDunno at sommitrealweird.co.uk>
- Date: Sun, 11 Oct 2015 12:06:14 +1100
++Date: Thu, 21 Jul 2016 04:33:00 +0200
 +Subject: Update manual page to refer to django-admin instead of
 + django-admin.py
 +
 + Update the manual page to speak of django-admin instead of
 + django-admin.py as that's the name used by the Debian package.
 + .
 + This is a Debian specific patch.
- 
 +Forwarded: not-needed
- Author: Brett Parker <iDunno at sommitrealweird.co.uk>
 +Origin: vendor
++---
++ docs/man/django-admin.1 | 6 +++---
++ 1 file changed, 3 insertions(+), 3 deletions(-)
 +
++diff --git a/docs/man/django-admin.1 b/docs/man/django-admin.1
++index c9932ac..bdb6438 100644
 +--- a/docs/man/django-admin.1
 ++++ b/docs/man/django-admin.1
 +@@ -1,8 +1,8 @@
 +-.TH "django-admin.py" "1" "March 2008" "Django Project" ""
 ++.TH "django-admin" "1" "March 2008" "Django Project" ""
 + .SH "NAME"
 +-django\-admin.py \- Utility script for the Django Web framework
 ++django\-admin \- Utility script for the Django Web framework
 + .SH "SYNOPSIS"
 +-.B django\-admin.py
 ++.B django\-admin
 + .I <action>
 + .B [options]
 + .sp
diff --cc debian/patches/0003-Use-Debian-GeoIP-database-path-as-default.patch
index 2ab652a,0000000..db86873
mode 100644,000000..100644
--- a/debian/patches/0003-Use-Debian-GeoIP-database-path-as-default.patch
+++ b/debian/patches/0003-Use-Debian-GeoIP-database-path-as-default.patch
@@@ -1,65 -1,0 +1,67 @@@
- From e4cd95fc1d5322f9bff209890b71f57ee9d36e62 Mon Sep 17 00:00:00 2001
++From e06be82ee9864262a67d222ce117a02002a00626 Mon Sep 17 00:00:00 2001
 +From: Tapio Rantala <tapio.rantala at iki.fi>
- Date: Sun, 11 Oct 2015 12:06:15 +1100
++Date: Thu, 21 Jul 2016 04:33:01 +0200
 +Subject: Use Debian GeoIP database path as default
 +
 + Default to Debian standard path for GeoIP directory and for GeoIP city
 + file. Avoids the need to declare them in each project.
 + .
 + This is a Debian specific patch.
- 
 +Bug-Debian: http://bugs.debian.org/645094
 +Forwarded: not-needed
- Author: Tapio Rantala <tapio.rantala at iki.fi>
++---
++ django/contrib/gis/geoip/base.py | 19 ++++++++++---------
++ 1 file changed, 10 insertions(+), 9 deletions(-)
 +
++diff --git a/django/contrib/gis/geoip/base.py b/django/contrib/gis/geoip/base.py
++index 9295030..0b05f43 100644
 +--- a/django/contrib/gis/geoip/base.py
 ++++ b/django/contrib/gis/geoip/base.py
 +@@ -67,7 +67,8 @@ class GeoIP(object):
 +         * path: Base directory to where GeoIP data is located or the full path
 +             to where the city or country data files (*.dat) are located.
 +             Assumes that both the city and country data sets are located in
 +-            this directory; overrides the GEOIP_PATH settings attribute.
 ++            this directory. Overrides the GEOIP_PATH settings attribute.
 ++            If neither is set, defaults to '/usr/share/GeoIP'.
 + 
 +         * cache: The cache settings when opening up the GeoIP datasets,
 +             and may be an integer in (0, 1, 2, 4, 8) corresponding to
 +@@ -76,11 +77,13 @@ class GeoIP(object):
 +             settings,  respectively.  Defaults to 0, meaning that the data is read
 +             from the disk.
 + 
 +-        * country: The name of the GeoIP country data file.  Defaults to
 +-            'GeoIP.dat'; overrides the GEOIP_COUNTRY settings attribute.
- -
- -        * city: The name of the GeoIP city data file.  Defaults to
- -            'GeoLiteCity.dat'; overrides the GEOIP_CITY settings attribute.
 ++        * country: The name of the GeoIP country data file. Overrides
 ++            the GEOIP_COUNTRY settings attribute. If neither is set,
 ++            defaults to 'GeoIP.dat'
- +
++ 
++-        * city: The name of the GeoIP city data file.  Defaults to
++-            'GeoLiteCity.dat'; overrides the GEOIP_CITY settings attribute.
 ++        * city: The name of the GeoIP city data file. Overrides the
 ++            GEOIP_CITY settings attribute. If neither is set, defaults
 ++            to 'GeoIPCity.dat'.
 +         """
 +         # Checking the given cache option.
 +         if cache in self.cache_options:
 +@@ -90,9 +93,7 @@ class GeoIP(object):
 + 
 +         # Getting the GeoIP data path.
 +         if not path:
 +-            path = GEOIP_SETTINGS.get('GEOIP_PATH', None)
 +-            if not path:
 +-                raise GeoIPException('GeoIP path must be provided via parameter or the GEOIP_PATH setting.')
 ++            path = GEOIP_SETTINGS.get('GEOIP_PATH', '/usr/share/GeoIP')
 +         if not isinstance(path, six.string_types):
 +             raise TypeError('Invalid path type: %s' % type(path).__name__)
 + 
 +@@ -105,7 +106,7 @@ class GeoIP(object):
 +                 self._country = GeoIP_open(force_bytes(country_db), cache)
 +                 self._country_file = country_db
 + 
 +-            city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat'))
 ++            city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoIPCity.dat'))
 +             if os.path.isfile(city_db):
 +                 self._city = GeoIP_open(force_bytes(city_db), cache)
 +                 self._city_file = city_db
diff --cc debian/patches/0004-CVE-2016-2512-Prevented-spoofing-is_safe_url-with-ba.patch
index 8507bbc,0000000..fd38143
mode 100644,000000..100644
--- a/debian/patches/0004-CVE-2016-2512-Prevented-spoofing-is_safe_url-with-ba.patch
+++ b/debian/patches/0004-CVE-2016-2512-Prevented-spoofing-is_safe_url-with-ba.patch
@@@ -1,61 -1,0 +1,67 @@@
- Description: CVE-2016-2512: Prevented spoofing is_safe_url() with basic auth
++From ce819b24d49aca2f481d8e4abb4a263318d609c6 Mon Sep 17 00:00:00 2001
++From: Mark Striemer <mstriemer at mozilla.com>
++Date: Thu, 21 Jul 2016 04:33:10 +0200
++Subject: CVE-2016-2512: Prevented spoofing is_safe_url() with basic auth
++
 +Origin: upstream, https://github.com/django/django/commit/382ab137312961ad62feb8109d70a5a581fe8350
 +Bug-Debian: https://bugs.debian.org/816434
 +Forwarded: not-needed
- Author: Mark Striemer <mstriemer at mozilla.com>
 +Reviewed-by: Salvatore Bonaccorso <carnil at debian.org>
 +Last-Update: 2016-03-12
 +Applied-Upstream: 1.8.10
- 
 +---
 + django/utils/http.py           |  8 ++++++--
 + tests/utils_tests/test_http.py | 12 ++++++++++++
-  3 files changed, 34 insertions(+), 2 deletions(-)
++ 2 files changed, 18 insertions(+), 2 deletions(-)
 +
++diff --git a/django/utils/http.py b/django/utils/http.py
++index ef88f65..007edd4 100644
 +--- a/django/utils/http.py
 ++++ b/django/utils/http.py
 +@@ -274,8 +274,12 @@ def is_safe_url(url, host=None):
 +         url = url.strip()
 +     if not url:
 +         return False
 +-    # Chrome treats \ completely as /
 +-    url = url.replace('\\', '/')
 ++    # Chrome treats \ completely as / in paths but it could be part of some
 ++    # basic auth credentials so we need to check both URLs.
 ++    return _is_safe_url(url, host) and _is_safe_url(url.replace('\\', '/'), host)
 ++
 ++
 ++def _is_safe_url(url, host):
 +     # Chrome considers any URL with more than two slashes to be absolute, but
 +     # urlparse is not so flexible. Treat any url with three slashes as unsafe.
 +     if url.startswith('///'):
++diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
++index 3b367a4..c8fe0b3 100644
 +--- a/tests/utils_tests/test_http.py
 ++++ b/tests/utils_tests/test_http.py
 +@@ -110,6 +110,11 @@ class TestUtilsHttp(unittest.TestCase):
 +                         'javascript:alert("XSS")',
 +                         '\njavascript:alert(x)',
 +                         '\x08//example.com',
 ++                        r'http://otherserver\@example.com',
 ++                        r'http:\\testserver\@example.com',
 ++                        r'http://testserver\me:pass@example.com',
 ++                        r'http://testserver\@example.com',
 ++                        r'http:\\testserver\confirm\me at example.com',
 +                         '\n'):
 +             self.assertFalse(http.is_safe_url(bad_url, host='testserver'), "%s should be blocked" % bad_url)
 +         for good_url in ('/view/?param=http://example.com',
 +@@ -119,8 +124,15 @@ class TestUtilsHttp(unittest.TestCase):
 +                      'https://testserver/',
 +                      'HTTPS://testserver/',
 +                      '//testserver/',
 ++                     'http://testserver/confirm?email=me@example.com',
 +                      '/url%20with%20spaces/'):
 +             self.assertTrue(http.is_safe_url(good_url, host='testserver'), "%s should be allowed" % good_url)
 ++        # Valid basic auth credentials are allowed.
 ++        self.assertTrue(http.is_safe_url(r'http://user:pass@testserver/', host='user:pass at testserver'))
 ++        # A path without host is allowed.
 ++        self.assertTrue(http.is_safe_url('/confirm/me at example.com'))
 ++        # Basic auth without host is not allowed.
 ++        self.assertFalse(http.is_safe_url(r'http://testserver\@example.com'))
 + 
 +     def test_urlsafe_base64_roundtrip(self):
 +         bytestring = b'foo'
diff --cc debian/patches/0005-is_safe_url-crashes-with-a-byestring-URL-on-Python-2.patch
index 92f66d4,0000000..8782013
mode 100644,000000..100644
--- a/debian/patches/0005-is_safe_url-crashes-with-a-byestring-URL-on-Python-2.patch
+++ b/debian/patches/0005-is_safe_url-crashes-with-a-byestring-URL-on-Python-2.patch
@@@ -1,50 -1,0 +1,61 @@@
- Description: is_safe_url() crashes with a byestring URL on Python 2
++From 41e37f8587976b35971dcbdbd22122b2285f4810 Mon Sep 17 00:00:00 2001
++From: Claude Paroz <claude at 2xlibre.net>
++Date: Thu, 21 Jul 2016 04:33:11 +0200
++Subject: is_safe_url() crashes with a byestring URL on Python 2
++
 +Origin: upstream, https://github.com/django/django/commit/ada7a4aefb9bec4c34667b511022be6057102f98,
 + https://github.com/django/django/commit/beb392b85e71fdd41209d323126181d74090fecb
 +Bug: https://code.djangoproject.com/ticket/26308
 +Forwarded: not-needed
- Author: Claude Paroz <claude at 2xlibre.net>
 +Reviewed-by: Salvatore Bonaccorso <carnil at debian.org>
 +Last-Update: 2016-03-12
 +Applied-Upstream: 1.8.11
++---
++ django/utils/http.py           |  5 +++++
++ tests/utils_tests/test_http.py | 13 +++++++++++++
++ 2 files changed, 18 insertions(+)
 +
++diff --git a/django/utils/http.py b/django/utils/http.py
++index 007edd4..972760e 100644
 +--- a/django/utils/http.py
 ++++ b/django/utils/http.py
 +@@ -274,6 +274,11 @@ def is_safe_url(url, host=None):
 +         url = url.strip()
 +     if not url:
 +         return False
 ++    if six.PY2:
 ++        try:
 ++            url = force_text(url)
 ++        except UnicodeDecodeError:
 ++            return False
 +     # Chrome treats \ completely as / in paths but it could be part of some
 +     # basic auth credentials so we need to check both URLs.
 +     return _is_safe_url(url, host) and _is_safe_url(url.replace('\\', '/'), host)
++diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
++index c8fe0b3..769f163 100644
 +--- a/tests/utils_tests/test_http.py
 ++++ b/tests/utils_tests/test_http.py
 +@@ -1,3 +1,5 @@
 ++# -*- encoding: utf-8 -*-
 ++from __future__ import unicode_literals
 + from datetime import datetime
 + import sys
 + import unittest
 +@@ -127,6 +129,17 @@ class TestUtilsHttp(unittest.TestCase):
 +                      'http://testserver/confirm?email=me@example.com',
 +                      '/url%20with%20spaces/'):
 +             self.assertTrue(http.is_safe_url(good_url, host='testserver'), "%s should be allowed" % good_url)
 ++
 ++        if six.PY2:
 ++            # Check binary URLs, regression tests for #26308
 ++            self.assertTrue(
 ++                http.is_safe_url(b'https://testserver/', host='testserver'),
 ++                "binary URLs should be allowed on Python 2"
 ++            )
 ++            self.assertFalse(http.is_safe_url(b'\x08//example.com', host='testserver'))
 ++            self.assertTrue(http.is_safe_url('àview/'.encode('utf-8'), host='testserver'))
 ++            self.assertFalse(http.is_safe_url('àview'.encode('latin-1'), host='testserver'))
 ++
 +         # Valid basic auth credentials are allowed.
 +         self.assertTrue(http.is_safe_url(r'http://user:pass@testserver/', host='user:pass at testserver'))
 +         # A path without host is allowed.
diff --cc debian/patches/0006-CVE-2016-2513-Fixed-user-enumeration-timing-attack-d.patch
index 326a704,0000000..0b97049
mode 100644,000000..100644
--- a/debian/patches/0006-CVE-2016-2513-Fixed-user-enumeration-timing-attack-d.patch
+++ b/debian/patches/0006-CVE-2016-2513-Fixed-user-enumeration-timing-attack-d.patch
@@@ -1,387 -1,0 +1,395 @@@
- Description: CVE-2016-2513: Fixed user enumeration timing attack during login
++From 22de96c2cbc953dfb07dd93c93686f40289fa169 Mon Sep 17 00:00:00 2001
++From: Florian Apolloner <florian at apolloner.eu>
++Date: Thu, 21 Jul 2016 04:33:12 +0200
++Subject: CVE-2016-2513: Fixed user enumeration timing attack during login
++
 +Origin: upstream, https://github.com/django/django/commit/f4e6e02f7713a6924d16540be279909ff4091eb6
 +Forwarded: not-needed
- Author: Florian Apolloner <florian at apolloner.eu>
 +Reviewed-by: Salvatore Bonaccorso <carnil at debian.org>
 +Last-Update: 2016-03-12
 +Applied-Upstream: 1.8.10
- 
 +---
-  django/contrib/auth/hashers.py   | 77 +++++++++++++++++++++++++++++-----------
-  docs/topics/auth/passwords.txt   | 30 ++++++++++++++++
-  tests/auth_tests/test_hashers.py | 58 +++++++++++++++++++++++++++++-
-  4 files changed, 177 insertions(+), 21 deletions(-)
++ django/contrib/auth/hashers.py            |  77 ++++++++++++++------
++ django/contrib/auth/tests/test_hashers.py |  60 ++++++++++++++++
++ docs/topics/auth/passwords.txt            | 113 ++++++++++++++++++++++++++++++
++ 3 files changed, 230 insertions(+), 20 deletions(-)
 +
++diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
++index e459f39..1ad4bcb 100644
 +--- a/django/contrib/auth/hashers.py
 ++++ b/django/contrib/auth/hashers.py
 +@@ -5,6 +5,7 @@ import binascii
 + from collections import OrderedDict
 + import hashlib
 + import importlib
 ++import warnings
 + 
 + from django.dispatch import receiver
 + from django.conf import settings
- @@ -55,10 +56,17 @@ def check_password(password, encoded, se
++@@ -55,10 +56,17 @@ def check_password(password, encoded, setter=None, preferred='default'):
 +     preferred = get_hasher(preferred)
 +     hasher = identify_hasher(encoded)
 + 
 +-    must_update = hasher.algorithm != preferred.algorithm
 +-    if not must_update:
 +-        must_update = preferred.must_update(encoded)
 ++    hasher_changed = hasher.algorithm != preferred.algorithm
 ++    must_update = hasher_changed or preferred.must_update(encoded)
 +     is_correct = hasher.verify(password, encoded)
 ++
 ++    # If the hasher didn't change (we don't protect against enumeration if it
 ++    # does) and the password should get updated, try to close the timing gap
 ++    # between the work factor of the current encoded password and the default
 ++    # work factor.
 ++    if not is_correct and not hasher_changed and must_update:
 ++        hasher.harden_runtime(password, encoded)
 ++
 +     if setter and is_correct and must_update:
 +         setter(password)
 +     return is_correct
 +@@ -217,6 +225,19 @@ class BasePasswordHasher(object):
 +     def must_update(self, encoded):
 +         return False
 + 
 ++    def harden_runtime(self, password, encoded):
 ++        """
 ++        Bridge the runtime gap between the work factor supplied in `encoded`
 ++        and the work factor suggested by this hasher.
 ++
 ++        Taking PBKDF2 as an example, if `encoded` contains 20000 iterations and
 ++        `self.iterations` is 30000, this method should run password through
 ++        another 10000 iterations of PBKDF2. Similar approaches should exist
 ++        for any hasher that has a work factor. If not, this method should be
 ++        defined as a no-op to silence the warning.
 ++        """
 ++        warnings.warn('subclasses of BasePasswordHasher should provide a harden_runtime() method')
 ++
 + 
 + class PBKDF2PasswordHasher(BasePasswordHasher):
 +     """
- @@ -259,6 +280,12 @@ class PBKDF2PasswordHasher(BasePasswordH
++@@ -259,6 +280,12 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
 +         algorithm, iterations, salt, hash = encoded.split('$', 3)
 +         return int(iterations) != self.iterations
 + 
 ++    def harden_runtime(self, password, encoded):
 ++        algorithm, iterations, salt, hash = encoded.split('$', 3)
 ++        extra_iterations = self.iterations - int(iterations)
 ++        if extra_iterations > 0:
 ++            self.encode(password, salt, extra_iterations)
 ++
 + 
 + class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
 +     """
- @@ -309,23 +336,8 @@ class BCryptSHA256PasswordHasher(BasePas
++@@ -309,23 +336,8 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
 +     def verify(self, password, encoded):
 +         algorithm, data = encoded.split('$', 1)
 +         assert algorithm == self.algorithm
 +-        bcrypt = self._load_library()
 +-
 +-        # Hash the password prior to using bcrypt to prevent password truncation
 +-        #   See: https://code.djangoproject.com/ticket/20138
 +-        if self.digest is not None:
 +-            # We use binascii.hexlify here because Python3 decided that a hex encoded
 +-            #   bytestring is somehow a unicode.
 +-            password = binascii.hexlify(self.digest(force_bytes(password)).digest())
 +-        else:
 +-            password = force_bytes(password)
 +-
 +-        # Ensure that our data is a bytestring
 +-        data = force_bytes(data)
 +-        # force_bytes() necessary for py-bcrypt compatibility
 +-        hashpw = force_bytes(bcrypt.hashpw(password, data))
 +-
 +-        return constant_time_compare(data, hashpw)
 ++        encoded_2 = self.encode(password, force_bytes(data))
 ++        return constant_time_compare(encoded, encoded_2)
 + 
 +     def safe_summary(self, encoded):
 +         algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
- @@ -338,6 +350,16 @@ class BCryptSHA256PasswordHasher(BasePas
++@@ -338,6 +350,16 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
 +             (_('checksum'), mask_hash(checksum)),
 +         ])
 + 
 ++    def harden_runtime(self, password, encoded):
 ++        _, data = encoded.split('$', 1)
 ++        salt = data[:29]  # Length of the salt in bcrypt.
 ++        rounds = data.split('$')[2]
 ++        # work factor is logarithmic, adding one doubles the load.
 ++        diff = 2**(self.rounds - int(rounds)) - 1
 ++        while diff > 0:
 ++            self.encode(password, force_bytes(salt))
 ++            diff -= 1
 ++
 + 
 + class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
 +     """
- @@ -385,6 +407,9 @@ class SHA1PasswordHasher(BasePasswordHas
++@@ -385,6 +407,9 @@ class SHA1PasswordHasher(BasePasswordHasher):
 +             (_('hash'), mask_hash(hash)),
 +         ])
 + 
 ++    def harden_runtime(self, password, encoded):
 ++        pass
 ++
 + 
 + class MD5PasswordHasher(BasePasswordHasher):
 +     """
- @@ -413,6 +438,9 @@ class MD5PasswordHasher(BasePasswordHash
++@@ -413,6 +438,9 @@ class MD5PasswordHasher(BasePasswordHasher):
 +             (_('hash'), mask_hash(hash)),
 +         ])
 + 
 ++    def harden_runtime(self, password, encoded):
 ++        pass
 ++
 + 
 + class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
 +     """
- @@ -445,6 +473,9 @@ class UnsaltedSHA1PasswordHasher(BasePas
++@@ -445,6 +473,9 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
 +             (_('hash'), mask_hash(hash)),
 +         ])
 + 
 ++    def harden_runtime(self, password, encoded):
 ++        pass
 ++
 + 
 + class UnsaltedMD5PasswordHasher(BasePasswordHasher):
 +     """
- @@ -478,6 +509,9 @@ class UnsaltedMD5PasswordHasher(BasePass
++@@ -478,6 +509,9 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
 +             (_('hash'), mask_hash(encoded, show=3)),
 +         ])
 + 
 ++    def harden_runtime(self, password, encoded):
 ++        pass
 ++
 + 
 + class CryptPasswordHasher(BasePasswordHasher):
 +     """
- @@ -512,3 +546,6 @@ class CryptPasswordHasher(BasePasswordHa
++@@ -512,3 +546,6 @@ class CryptPasswordHasher(BasePasswordHasher):
 +             (_('salt'), salt),
 +             (_('hash'), mask_hash(data, show=3)),
 +         ])
 ++
 ++    def harden_runtime(self, password, encoded):
 ++        pass
++diff --git a/django/contrib/auth/tests/test_hashers.py b/django/contrib/auth/tests/test_hashers.py
++index 85f1c15..3a06b43 100644
++--- a/django/contrib/auth/tests/test_hashers.py
+++++ b/django/contrib/auth/tests/test_hashers.py
++@@ -9,7 +9,12 @@ from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher,
++     get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
++ from django.test import SimpleTestCase
++ from django.utils import six
+++from django.utils.encoding import force_bytes
++ 
+++try:
+++    from unittest import mock
+++except ImportError:
+++    import mock
++ 
++ try:
++     import crypt
++@@ -176,6 +181,28 @@ class TestUtilsHashPass(SimpleTestCase):
++         self.assertTrue(check_password('', blank_encoded))
++         self.assertFalse(check_password(' ', blank_encoded))
++ 
+++    @skipUnless(bcrypt, "bcrypt not installed")
+++    def test_bcrypt_harden_runtime(self):
+++        hasher = get_hasher('bcrypt')
+++        self.assertEqual('bcrypt', hasher.algorithm)
+++
+++        with mock.patch.object(hasher, 'rounds', 4):
+++            encoded = make_password('letmein', hasher='bcrypt')
+++
+++        with mock.patch.object(hasher, 'rounds', 6), \
+++                mock.patch.object(hasher, 'encode', side_effect=hasher.encode):
+++            hasher.harden_runtime('wrong_password', encoded)
+++
+++            # Increasing rounds from 4 to 6 means an increase of 4 in workload,
+++            # therefore hardening should run 3 times to make the timing the
+++            # same (the original encode() call already ran once).
+++            self.assertEqual(hasher.encode.call_count, 3)
+++
+++            # Get the original salt (includes the original workload factor)
+++            algorithm, data = encoded.split('$', 1)
+++            expected_call = (('wrong_password', force_bytes(data[:29])),)
+++            self.assertEqual(hasher.encode.call_args_list, [expected_call] * 3)
+++
++     def test_unusable(self):
++         encoded = make_password(None)
++         self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
++@@ -283,6 +310,25 @@ class TestUtilsHashPass(SimpleTestCase):
++         finally:
++             hasher.iterations = old_iterations
++ 
+++    def test_pbkdf2_harden_runtime(self):
+++        hasher = get_hasher('default')
+++        self.assertEqual('pbkdf2_sha256', hasher.algorithm)
+++
+++        with mock.patch.object(hasher, 'iterations', 1):
+++            encoded = make_password('letmein')
+++
+++        with mock.patch.object(hasher, 'iterations', 6), \
+++                mock.patch.object(hasher, 'encode', side_effect=hasher.encode):
+++            hasher.harden_runtime('wrong_password', encoded)
+++
+++            # Encode should get called once ...
+++            self.assertEqual(hasher.encode.call_count, 1)
+++
+++            # ... with the original salt and 5 iterations.
+++            algorithm, iterations, salt, hash = encoded.split('$', 3)
+++            expected_call = (('wrong_password', salt, 5),)
+++            self.assertEqual(hasher.encode.call_args, expected_call)
+++
++     def test_pbkdf2_upgrade_new_hasher(self):
++         self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
++         hasher = get_hasher('default')
++@@ -311,6 +357,20 @@ class TestUtilsHashPass(SimpleTestCase):
++             self.assertTrue(check_password('letmein', encoded, setter))
++             self.assertTrue(state['upgraded'])
++ 
+++    def test_check_password_calls_harden_runtime(self):
+++        hasher = get_hasher('default')
+++        encoded = make_password('letmein')
+++
+++        with mock.patch.object(hasher, 'harden_runtime'), \
+++                mock.patch.object(hasher, 'must_update', return_value=True):
+++            # Correct password supplied, no hardening needed
+++            check_password('letmein', encoded)
+++            self.assertEqual(hasher.harden_runtime.call_count, 0)
+++
+++            # Wrong password supplied, hardening needed
+++            check_password('wrong_password', encoded)
+++            self.assertEqual(hasher.harden_runtime.call_count, 1)
+++
++     def test_load_library_no_algorithm(self):
++         with self.assertRaises(ValueError) as e:
++             BasePasswordHasher()._load_library()
++diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt
++index 280405f..00964cf 100644
 +--- a/docs/topics/auth/passwords.txt
 ++++ b/docs/topics/auth/passwords.txt
- @@ -189,12 +189,125 @@ unmentioned algorithms won't be able to
++@@ -197,12 +197,125 @@ unmentioned algorithms won't be able to upgrade.
 + 
 +     Passwords will be upgraded when changing the PBKDF2 iteration count.
 + 
 ++Be aware that if all the passwords in your database aren't encoded in the
 ++default hasher's algorithm, you may be vulnerable to a user enumeration timing
 ++attack due to a difference between the duration of a login request for a user
 ++with a password encoded in a non-default algorithm and the duration of a login
 ++request for a nonexistent user (which runs the default hasher). You may be able
 ++to mitigate this by :ref:`upgrading older password hashes
 ++<wrapping-password-hashers>`.
 ++
 ++.. _wrapping-password-hashers:
 ++
 ++Password upgrading without requiring a login
 ++--------------------------------------------
 ++
 ++If you have an existing database with an older, weak hash such as MD5 or SHA1,
 ++you might want to upgrade those hashes yourself instead of waiting for the
 ++upgrade to happen when a user logs in (which may never happen if a user doesn't
 ++return to your site). In this case, you can use a "wrapped" password hasher.
 ++
 ++For this example, we'll migrate a collection of SHA1 hashes to use
 ++PBKDF2(SHA1(password)) and add the corresponding password hasher for checking
 ++if a user entered the correct password on login. We assume we're using the
 ++built-in ``User`` model and that our project has an ``accounts`` app. You can
 ++modify the pattern to work with any algorithm or with a custom user model.
 ++
 ++First, we'll add the custom hasher:
 ++
 ++.. snippet::
 ++    :filename: accounts/hashers.py
 ++
 ++    from django.contrib.auth.hashers import (
 ++        PBKDF2PasswordHasher, SHA1PasswordHasher,
 ++    )
 ++
 ++
 ++    class PBKDF2WrappedSHA1PasswordHasher(PBKDF2PasswordHasher):
 ++        algorithm = 'pbkdf2_wrapped_sha1'
 ++
 ++        def encode_sha1_hash(self, sha1_hash, salt, iterations=None):
 ++            return super(PBKDF2WrappedSHA1PasswordHasher, self).encode(sha1_hash, salt, iterations)
 ++
 ++        def encode(self, password, salt, iterations=None):
 ++            _, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split('$', 2)
 ++            return self.encode_sha1_hash(sha1_hash, salt, iterations)
 ++
 ++The data migration might look something like:
 ++
 ++.. snippet::
 ++    :filename: accounts/migrations/0002_migrate_sha1_passwords.py
 ++
 ++    from django.db import migrations
 ++
 ++    from ..hashers import PBKDF2WrappedSHA1PasswordHasher
 ++
 ++
 ++    def forwards_func(apps, schema_editor):
 ++        User = apps.get_model('auth', 'User')
 ++        users = User.objects.filter(password__startswith='sha1$')
 ++        hasher = PBKDF2WrappedSHA1PasswordHasher()
 ++        for user in users:
 ++            algorithm, salt, sha1_hash = user.password.split('$', 2)
 ++            user.password = hasher.encode_sha1_hash(sha1_hash, salt)
 ++            user.save(update_fields=['password'])
 ++
 ++
 ++    class Migration(migrations.Migration):
 ++
 ++        dependencies = [
 ++            ('accounts', '0001_initial'),
 ++            # replace this with the latest migration in contrib.auth
 ++            ('auth', '####_migration_name'),
 ++        ]
 ++
 ++        operations = [
 ++            migrations.RunPython(forwards_func),
 ++        ]
 ++
 ++Be aware that this migration will take on the order of several minutes for
 ++several thousand users, depending on the speed of your hardware.
 ++
 ++Finally, we'll add a :setting:`PASSWORD_HASHERS` setting:
 ++
 ++.. snippet::
 ++    :filename: mysite/settings.py
 ++
 ++    PASSWORD_HASHERS = [
 ++        'django.contrib.auth.hashers.PBKDF2PasswordHasher',
 ++        'accounts.hashers.PBKDF2WrappedSHA1PasswordHasher',
 ++    ]
 ++
 ++Include any other hashers that your site uses in this list.
 ++
 + .. _sha1: http://en.wikipedia.org/wiki/SHA1
 + .. _pbkdf2: http://en.wikipedia.org/wiki/PBKDF2
 + .. _nist: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
 + .. _bcrypt: http://en.wikipedia.org/wiki/Bcrypt
 + .. _`bcrypt library`: https://pypi.python.org/pypi/bcrypt/
 + 
 ++.. _write-your-own-password-hasher:
 ++
 ++Writing your own hasher
 ++-----------------------
 ++
 ++.. versionadded:: 1.8.10
 ++
 ++If you write your own password hasher that contains a work factor such as a
 ++number of iterations, you should implement a
 ++``harden_runtime(self, password, encoded)`` method to bridge the runtime gap
 ++between the work factor supplied in the ``encoded`` password and the default
 ++work factor of the hasher. This prevents a user enumeration timing attack due
 ++to  difference between a login request for a user with a password encoded in an
 ++older number of iterations and a nonexistent user (which runs the default
 ++hasher's default number of iterations).
 ++
 ++Taking PBKDF2 as example, if ``encoded`` contains 20,000 iterations and the
 ++hasher's default ``iterations`` is 30,000, the method should run ``password``
 ++through another 10,000 iterations of PBKDF2.
 ++
 ++If your hasher doesn't have a work factor, implement the method as a no-op
 ++(``pass``).
 + 
 + Manually managing a user's password
 + ===================================
- --- a/django/contrib/auth/tests/test_hashers.py
- +++ b/django/contrib/auth/tests/test_hashers.py
- @@ -9,7 +9,12 @@ from django.contrib.auth.hashers import
-      get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
-  from django.test import SimpleTestCase
-  from django.utils import six
- +from django.utils.encoding import force_bytes
-  
- +try:
- +    from unittest import mock
- +except ImportError:
- +    import mock
-  
-  try:
-      import crypt
- @@ -176,6 +181,28 @@ class TestUtilsHashPass(SimpleTestCase):
-          self.assertTrue(check_password('', blank_encoded))
-          self.assertFalse(check_password(' ', blank_encoded))
-  
- +    @skipUnless(bcrypt, "bcrypt not installed")
- +    def test_bcrypt_harden_runtime(self):
- +        hasher = get_hasher('bcrypt')
- +        self.assertEqual('bcrypt', hasher.algorithm)
- +
- +        with mock.patch.object(hasher, 'rounds', 4):
- +            encoded = make_password('letmein', hasher='bcrypt')
- +
- +        with mock.patch.object(hasher, 'rounds', 6), \
- +                mock.patch.object(hasher, 'encode', side_effect=hasher.encode):
- +            hasher.harden_runtime('wrong_password', encoded)
- +
- +            # Increasing rounds from 4 to 6 means an increase of 4 in workload,
- +            # therefore hardening should run 3 times to make the timing the
- +            # same (the original encode() call already ran once).
- +            self.assertEqual(hasher.encode.call_count, 3)
- +
- +            # Get the original salt (includes the original workload factor)
- +            algorithm, data = encoded.split('$', 1)
- +            expected_call = (('wrong_password', force_bytes(data[:29])),)
- +            self.assertEqual(hasher.encode.call_args_list, [expected_call] * 3)
- +
-      def test_unusable(self):
-          encoded = make_password(None)
-          self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
- @@ -283,6 +310,25 @@ class TestUtilsHashPass(SimpleTestCase):
-          finally:
-              hasher.iterations = old_iterations
-  
- +    def test_pbkdf2_harden_runtime(self):
- +        hasher = get_hasher('default')
- +        self.assertEqual('pbkdf2_sha256', hasher.algorithm)
- +
- +        with mock.patch.object(hasher, 'iterations', 1):
- +            encoded = make_password('letmein')
- +
- +        with mock.patch.object(hasher, 'iterations', 6), \
- +                mock.patch.object(hasher, 'encode', side_effect=hasher.encode):
- +            hasher.harden_runtime('wrong_password', encoded)
- +
- +            # Encode should get called once ...
- +            self.assertEqual(hasher.encode.call_count, 1)
- +
- +            # ... with the original salt and 5 iterations.
- +            algorithm, iterations, salt, hash = encoded.split('$', 3)
- +            expected_call = (('wrong_password', salt, 5),)
- +            self.assertEqual(hasher.encode.call_args, expected_call)
- +
-      def test_pbkdf2_upgrade_new_hasher(self):
-          self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
-          hasher = get_hasher('default')
- @@ -311,6 +357,20 @@ class TestUtilsHashPass(SimpleTestCase):
-              self.assertTrue(check_password('letmein', encoded, setter))
-              self.assertTrue(state['upgraded'])
-  
- +    def test_check_password_calls_harden_runtime(self):
- +        hasher = get_hasher('default')
- +        encoded = make_password('letmein')
- +
- +        with mock.patch.object(hasher, 'harden_runtime'), \
- +                mock.patch.object(hasher, 'must_update', return_value=True):
- +            # Correct password supplied, no hardening needed
- +            check_password('letmein', encoded)
- +            self.assertEqual(hasher.harden_runtime.call_count, 0)
- +
- +            # Wrong password supplied, hardening needed
- +            check_password('wrong_password', encoded)
- +            self.assertEqual(hasher.harden_runtime.call_count, 1)
- +
-      def test_load_library_no_algorithm(self):
-          with self.assertRaises(ValueError) as e:
-              BasePasswordHasher()._load_library()
diff --cc debian/patches/0007-CVE-2016-6186-Fixed-XSS-in-admin-s-add-change-relate.patch
index b9e1d96,0000000..3c7ba7b
mode 100644,000000..100644
--- a/debian/patches/0007-CVE-2016-6186-Fixed-XSS-in-admin-s-add-change-relate.patch
+++ b/debian/patches/0007-CVE-2016-6186-Fixed-XSS-in-admin-s-add-change-relate.patch
@@@ -1,65 -1,0 +1,72 @@@
- Description: CVE-2016-6186: Fixed XSS in admin's add/change related popup.
++From 39cf1a3b760df98cccba0278371f314f34686fde Mon Sep 17 00:00:00 2001
++From: Tim Graham <timograham at gmail.com>
++Date: Thu, 21 Jul 2016 04:33:15 +0200
++Subject: CVE-2016-6186: Fixed XSS in admin's add/change related popup.
++
 +Origin: upstream, https://github.com/django/django/commit/8462b3fa9c0a59221a8b5583025c3a9fff637d85
 +Forwarded: not-needed
- Author: Tim Graham <timograham at gmail.com>
 +Reviewed-by: Luke Faraone <lfaraone at debian.org>
 +Last-Update: 2016-07-16
 +Applied-Upstream: 1.8.14
- 
 +---
-  django/views/debug.py       |  4 ++--
-  tests/admin_views/admin.py  |  3 ++-
-  tests/admin_views/models.py |  4 ++++
-  tests/admin_views/tests.py  | 38 ++++++++++++++++++++++++++++++++++++++
-  4 files changed, 46 insertions(+), 3 deletions(-)
++ django/views/debug.py       | 4 ++--
++ tests/admin_views/admin.py  | 3 ++-
++ tests/admin_views/models.py | 4 ++++
++ 3 files changed, 8 insertions(+), 3 deletions(-)
 +
++diff --git a/django/views/debug.py b/django/views/debug.py
++index 13992e9..7d8ea05 100644
 +--- a/django/views/debug.py
 ++++ b/django/views/debug.py
- @@ -637,13 +637,13 @@
++@@ -637,13 +637,13 @@ TECHNICAL_500_TEMPLATE = """
 +       var s = link.getElementsByTagName('span')[0];
 +       var uarr = String.fromCharCode(0x25b6);
 +       var darr = String.fromCharCode(0x25bc);
 +-      s.innerHTML = s.innerHTML == uarr ? darr : uarr;
 ++      s.textContent = s.textContent == uarr ? darr : uarr;
 +       return false;
 +     }
 +     function switchPastebinFriendly(link) {
 +       s1 = "Switch to copy-and-paste view";
 +       s2 = "Switch back to interactive view";
 +-      link.innerHTML = link.innerHTML == s1 ? s2: s1;
 ++      link.textContent = link.textContent.trim() == s1 ? s2: s1;
 +       toggle('browserTraceback', 'pastebinTraceback');
 +       return false;
 +     }
++diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
++index 15c1384..36187af 100644
 +--- a/tests/admin_views/admin.py
 ++++ b/tests/admin_views/admin.py
- @@ -83,7 +83,8 @@
++@@ -83,7 +83,8 @@ class ChapterXtra1Admin(admin.ModelAdmin):
 + 
 + class ArticleAdmin(admin.ModelAdmin):
 +     list_display = ('content', 'date', callable_year, 'model_year',
 +-                    'modeladmin_year', 'model_year_reversed')
 ++                    'modeladmin_year', 'model_year_reversed', 'section')
 ++    list_editable = ('section',)
 +     list_filter = ('date', 'section')
 +     view_on_site = False
 +     fieldsets = (
++diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
++index f41b4de..79dd56c 100644
 +--- a/tests/admin_views/models.py
 ++++ b/tests/admin_views/models.py
- @@ -15,6 +15,7 @@
++@@ -15,6 +15,7 @@ from django.db import models
 + from django.utils.encoding import python_2_unicode_compatible
 + 
 + 
 ++ at python_2_unicode_compatible
 + class Section(models.Model):
 +     """
 +     A simple section that links to articles, to test linking to related items
- @@ -22,6 +23,9 @@
++@@ -22,6 +23,9 @@ class Section(models.Model):
 +     """
 +     name = models.CharField(max_length=100)
 + 
 ++    def __str__(self):
 ++        return self.name
 ++
 +     @property
 +     def name_property(self):
 +         """
diff --cc debian/patches/series
index 35c5250,0000000..c20eb3b
mode 100644,000000..100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@@ -1,7 -1,0 +1,7 @@@
- 02_disable-sources-in-sphinxdoc.diff
- 03_manpage.diff
- 06_use_debian_geoip_database_as_default.diff
- CVE-2016-2512.diff
- CVE-2016-2512-regression.diff
- CVE-2016-2513.diff
- CVE-2016-6186.diff
++0001-Disable-creation-of-_sources-directory-by-Sphinx.patch
++0002-Update-manual-page-to-refer-to-django-admin-instead-.patch
++0003-Use-Debian-GeoIP-database-path-as-default.patch
++0004-CVE-2016-2512-Prevented-spoofing-is_safe_url-with-ba.patch
++0005-is_safe_url-crashes-with-a-byestring-URL-on-Python-2.patch
++0006-CVE-2016-2513-Fixed-user-enumeration-timing-attack-d.patch
++0007-CVE-2016-6186-Fixed-XSS-in-admin-s-add-change-relate.patch

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django.git



More information about the Python-modules-commits mailing list