[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