1
0
mirror of https://github.com/django/django.git synced 2025-10-24 22:26:08 +00:00

Fixed CVE-2016-2513 -- Fixed user enumeration timing attack during login.

This is a security fix.
This commit is contained in:
Florian Apolloner
2016-02-13 21:09:46 +01:00
committed by Tim Graham
parent c5544d2892
commit 67b46ba701
5 changed files with 211 additions and 21 deletions

View File

@@ -4,6 +4,7 @@ import base64
import binascii
import hashlib
import importlib
import warnings
from collections import OrderedDict
from django.conf import settings
@@ -46,10 +47,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
@@ -216,6 +224,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):
"""
@@ -258,6 +279,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):
"""
@@ -305,23 +332,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 as described in #20138.
if self.digest is not None:
# Use binascii.hexlify() because a hex encoded bytestring is
# Unicode on Python 3.
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(BasePasswordHasher):
algorithm, empty, algostr, rounds, data = encoded.split('$', 4)
return int(rounds) != self.rounds
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(BasePasswordHasher):
(_('hash'), mask_hash(hash)),
])
def harden_runtime(self, password, encoded):
pass
class MD5PasswordHasher(BasePasswordHasher):
"""
@@ -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(BasePasswordHasher):
(_('hash'), mask_hash(hash)),
])
def harden_runtime(self, password, encoded):
pass
class UnsaltedMD5PasswordHasher(BasePasswordHasher):
"""
@@ -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(BasePasswordHasher):
(_('salt'), salt),
(_('hash'), mask_hash(data, show=3)),
])
def harden_runtime(self, password, encoded):
pass