Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #18144 -- Restored compatibility with SHA1 hashes with empty salt.

Thanks dahool for the report and initial version of the patch.
  • Loading branch information...
commit f1255a3c0904a55ef267fa5f8687a1ce78f6894a 1 parent 509798a
Aymeric Augustin authored
1  django/conf/global_settings.py
@@ -510,6 +510,7 @@
510 510
     'django.contrib.auth.hashers.BCryptPasswordHasher',
511 511
     'django.contrib.auth.hashers.SHA1PasswordHasher',
512 512
     'django.contrib.auth.hashers.MD5PasswordHasher',
  513
+    'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
513 514
     'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
514 515
     'django.contrib.auth.hashers.CryptPasswordHasher',
515 516
 )
52  django/contrib/auth/hashers.py
@@ -127,9 +127,14 @@ def identify_hasher(encoded):
127 127
     get_hasher() to return hasher. Raises ValueError if
128 128
     algorithm cannot be identified, or if hasher is not loaded.
129 129
     """
  130
+    # Ancient versions of Django created plain MD5 passwords and accepted
  131
+    # MD5 passwords with an empty salt.
130 132
     if ((len(encoded) == 32 and '$' not in encoded) or
131 133
             (len(encoded) == 37 and encoded.startswith('md5$$'))):
132 134
         algorithm = 'unsalted_md5'
  135
+    # Ancient versions of Django accepted SHA1 passwords with an empty salt.
  136
+    elif len(encoded) == 46 and encoded.startswith('sha1$$'):
  137
+        algorithm = 'unsalted_sha1'
133 138
     else:
134 139
         algorithm = encoded.split('$', 1)[0]
135 140
     return get_hasher(algorithm)
@@ -350,14 +355,48 @@ def safe_summary(self, encoded):
350 355
         ])
351 356
 
352 357
 
353  
-class UnsaltedMD5PasswordHasher(BasePasswordHasher):
  358
+class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
  359
+    """
  360
+    Very insecure algorithm that you should *never* use; stores SHA1 hashes
  361
+    with an empty salt.
  362
+
  363
+    This class is implemented because Django used to accept such password
  364
+    hashes. Some older Django installs still have these values lingering
  365
+    around so we need to handle and upgrade them properly.
354 366
     """
355  
-    I am an incredibly insecure algorithm you should *never* use;
356  
-    stores unsalted MD5 hashes without the algorithm prefix.
  367
+    algorithm = "unsalted_sha1"
  368
+
  369
+    def salt(self):
  370
+        return ''
357 371
 
358  
-    This class is implemented because Django used to store passwords
359  
-    this way. Some older Django installs still have these values
360  
-    lingering around so we need to handle and upgrade them properly.
  372
+    def encode(self, password, salt):
  373
+        assert salt == ''
  374
+        hash = hashlib.sha1(force_bytes(password)).hexdigest()
  375
+        return 'sha1$$%s' % hash
  376
+
  377
+    def verify(self, password, encoded):
  378
+        encoded_2 = self.encode(password, '')
  379
+        return constant_time_compare(encoded, encoded_2)
  380
+
  381
+    def safe_summary(self, encoded):
  382
+        assert encoded.startswith('sha1$$')
  383
+        hash = encoded[6:]
  384
+        return SortedDict([
  385
+            (_('algorithm'), self.algorithm),
  386
+            (_('hash'), mask_hash(hash)),
  387
+        ])
  388
+
  389
+
  390
+class UnsaltedMD5PasswordHasher(BasePasswordHasher):
  391
+    """
  392
+    Incredibly insecure algorithm that you should *never* use; stores unsalted
  393
+    MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an
  394
+    empty salt.
  395
+
  396
+    This class is implemented because Django used to store passwords this way
  397
+    and to accept such password hashes. Some older Django installs still have
  398
+    these values lingering around so we need to handle and upgrade them
  399
+    properly.
361 400
     """
362 401
     algorithm = "unsalted_md5"
363 402
 
@@ -365,6 +404,7 @@ def salt(self):
365 404
         return ''
366 405
 
367 406
     def encode(self, password, salt):
  407
+        assert salt == ''
368 408
         return hashlib.md5(force_bytes(password)).hexdigest()
369 409
 
370 410
     def verify(self, password, encoded):
17  django/contrib/auth/tests/hashers.py
@@ -2,7 +2,7 @@
2 2
 from __future__ import unicode_literals
3 3
 
4 4
 from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
5  
-from django.contrib.auth.hashers import (is_password_usable, 
  5
+from django.contrib.auth.hashers import (is_password_usable,
6 6
     check_password, make_password, PBKDF2PasswordHasher, load_hashers,
7 7
     PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
8 8
 from django.utils import unittest
@@ -52,7 +52,7 @@ def test_sha1(self):
52 52
 
53 53
     def test_md5(self):
54 54
         encoded = make_password('lètmein', 'seasalt', 'md5')
55  
-        self.assertEqual(encoded, 
  55
+        self.assertEqual(encoded,
56 56
                          'md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3')
57 57
         self.assertTrue(is_password_usable(encoded))
58 58
         self.assertTrue(check_password('lètmein', encoded))
@@ -60,7 +60,7 @@ def test_md5(self):
60 60
         self.assertEqual(identify_hasher(encoded).algorithm, "md5")
61 61
 
62 62
     def test_unsalted_md5(self):
63  
-        encoded = make_password('lètmein', 'seasalt', 'unsalted_md5')
  63
+        encoded = make_password('lètmein', '', 'unsalted_md5')
64 64
         self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
65 65
         self.assertTrue(is_password_usable(encoded))
66 66
         self.assertTrue(check_password('lètmein', encoded))
@@ -72,6 +72,17 @@ def test_unsalted_md5(self):
72 72
         self.assertTrue(check_password('lètmein', alt_encoded))
73 73
         self.assertFalse(check_password('lètmeinz', alt_encoded))
74 74
 
  75
+    def test_unsalted_sha1(self):
  76
+        encoded = make_password('lètmein', '', 'unsalted_sha1')
  77
+        self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
  78
+        self.assertTrue(is_password_usable(encoded))
  79
+        self.assertTrue(check_password('lètmein', encoded))
  80
+        self.assertFalse(check_password('lètmeinz', encoded))
  81
+        self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1")
  82
+        # Raw SHA1 isn't acceptable
  83
+        alt_encoded = encoded[6:]
  84
+        self.assertFalse(check_password('lètmein', alt_encoded))
  85
+
75 86
     @skipUnless(crypt, "no crypt module to generate password.")
76 87
     def test_crypt(self):
77 88
         encoded = make_password('lètmei', 'ab', 'crypt')

0 notes on commit f1255a3

Please sign in to comment.
Something went wrong with that request. Please try again.