Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #20593 -- Allow blank passwords in check_password() and set_pas…

…sword()
  • Loading branch information...
commit 2c4fe761a0e2b28e2c5c3b4bc506ee06824a443d 1 parent 3128f3d
Erik Romijn authored timgraham committed
12  django/contrib/auth/hashers.py
@@ -47,7 +47,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
47 47
     If setter is specified, it'll be called when you need to
48 48
     regenerate the password.
49 49
     """
50  
-    if not password or not is_password_usable(encoded):
  50
+    if not is_password_usable(encoded):
51 51
         return False
52 52
 
53 53
     preferred = get_hasher(preferred)
@@ -65,10 +65,10 @@ def make_password(password, salt=None, hasher='default'):
65 65
     Turn a plain-text password into a hash for database storage
66 66
 
67 67
     Same as encode() but generates a new random salt.  If
68  
-    password is None or blank then UNUSABLE_PASSWORD will be
  68
+    password is None then UNUSABLE_PASSWORD will be
69 69
     returned which disallows logins.
70 70
     """
71  
-    if not password:
  71
+    if password is None:
72 72
         return UNUSABLE_PASSWORD
73 73
 
74 74
     hasher = get_hasher(hasher)
@@ -222,7 +222,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
222 222
     digest = hashlib.sha256
223 223
 
224 224
     def encode(self, password, salt, iterations=None):
225  
-        assert password
  225
+        assert password is not None
226 226
         assert salt and '$' not in salt
227 227
         if not iterations:
228 228
             iterations = self.iterations
@@ -350,7 +350,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
350 350
     algorithm = "sha1"
351 351
 
352 352
     def encode(self, password, salt):
353  
-        assert password
  353
+        assert password is not None
354 354
         assert salt and '$' not in salt
355 355
         hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()
356 356
         return "%s$%s$%s" % (self.algorithm, salt, hash)
@@ -378,7 +378,7 @@ class MD5PasswordHasher(BasePasswordHasher):
378 378
     algorithm = "md5"
379 379
 
380 380
     def encode(self, password, salt):
381  
-        assert password
  381
+        assert password is not None
382 382
         assert salt and '$' not in salt
383 383
         hash = hashlib.md5(force_bytes(salt + password)).hexdigest()
384 384
         return "%s$%s$%s" % (self.algorithm, salt, hash)
53  django/contrib/auth/tests/test_hashers.py
@@ -32,6 +32,12 @@ def test_simple(self):
32 32
         self.assertTrue(is_password_usable(encoded))
33 33
         self.assertTrue(check_password('lètmein', encoded))
34 34
         self.assertFalse(check_password('lètmeinz', encoded))
  35
+        # Blank passwords
  36
+        blank_encoded = make_password('')
  37
+        self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
  38
+        self.assertTrue(is_password_usable(blank_encoded))
  39
+        self.assertTrue(check_password('', blank_encoded))
  40
+        self.assertFalse(check_password(' ', blank_encoded))
35 41
 
36 42
     def test_pkbdf2(self):
37 43
         encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
@@ -41,6 +47,12 @@ def test_pkbdf2(self):
41 47
         self.assertTrue(check_password('lètmein', encoded))
42 48
         self.assertFalse(check_password('lètmeinz', encoded))
43 49
         self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
  50
+        # Blank passwords
  51
+        blank_encoded = make_password('', 'seasalt', 'pbkdf2_sha256')
  52
+        self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
  53
+        self.assertTrue(is_password_usable(blank_encoded))
  54
+        self.assertTrue(check_password('', blank_encoded))
  55
+        self.assertFalse(check_password(' ', blank_encoded))
44 56
 
45 57
     def test_sha1(self):
46 58
         encoded = make_password('lètmein', 'seasalt', 'sha1')
@@ -50,6 +62,12 @@ def test_sha1(self):
50 62
         self.assertTrue(check_password('lètmein', encoded))
51 63
         self.assertFalse(check_password('lètmeinz', encoded))
52 64
         self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
  65
+        # Blank passwords
  66
+        blank_encoded = make_password('', 'seasalt', 'sha1')
  67
+        self.assertTrue(blank_encoded.startswith('sha1$'))
  68
+        self.assertTrue(is_password_usable(blank_encoded))
  69
+        self.assertTrue(check_password('', blank_encoded))
  70
+        self.assertFalse(check_password(' ', blank_encoded))
53 71
 
54 72
     def test_md5(self):
55 73
         encoded = make_password('lètmein', 'seasalt', 'md5')
@@ -59,6 +77,12 @@ def test_md5(self):
59 77
         self.assertTrue(check_password('lètmein', encoded))
60 78
         self.assertFalse(check_password('lètmeinz', encoded))
61 79
         self.assertEqual(identify_hasher(encoded).algorithm, "md5")
  80
+        # Blank passwords
  81
+        blank_encoded = make_password('', 'seasalt', 'md5')
  82
+        self.assertTrue(blank_encoded.startswith('md5$'))
  83
+        self.assertTrue(is_password_usable(blank_encoded))
  84
+        self.assertTrue(check_password('', blank_encoded))
  85
+        self.assertFalse(check_password(' ', blank_encoded))
62 86
 
63 87
     def test_unsalted_md5(self):
64 88
         encoded = make_password('lètmein', '', 'unsalted_md5')
@@ -72,6 +96,11 @@ def test_unsalted_md5(self):
72 96
         self.assertTrue(is_password_usable(alt_encoded))
73 97
         self.assertTrue(check_password('lètmein', alt_encoded))
74 98
         self.assertFalse(check_password('lètmeinz', alt_encoded))
  99
+        # Blank passwords
  100
+        blank_encoded = make_password('', '', 'unsalted_md5')
  101
+        self.assertTrue(is_password_usable(blank_encoded))
  102
+        self.assertTrue(check_password('', blank_encoded))
  103
+        self.assertFalse(check_password(' ', blank_encoded))
75 104
 
76 105
     def test_unsalted_sha1(self):
77 106
         encoded = make_password('lètmein', '', 'unsalted_sha1')
@@ -83,6 +112,12 @@ def test_unsalted_sha1(self):
83 112
         # Raw SHA1 isn't acceptable
84 113
         alt_encoded = encoded[6:]
85 114
         self.assertFalse(check_password('lètmein', alt_encoded))
  115
+        # Blank passwords
  116
+        blank_encoded = make_password('', '', 'unsalted_sha1')
  117
+        self.assertTrue(blank_encoded.startswith('sha1$'))
  118
+        self.assertTrue(is_password_usable(blank_encoded))
  119
+        self.assertTrue(check_password('', blank_encoded))
  120
+        self.assertFalse(check_password(' ', blank_encoded))
86 121
 
87 122
     @skipUnless(crypt, "no crypt module to generate password.")
88 123
     def test_crypt(self):
@@ -92,6 +127,12 @@ def test_crypt(self):
92 127
         self.assertTrue(check_password('lètmei', encoded))
93 128
         self.assertFalse(check_password('lètmeiz', encoded))
94 129
         self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
  130
+        # Blank passwords
  131
+        blank_encoded = make_password('', 'ab', 'crypt')
  132
+        self.assertTrue(blank_encoded.startswith('crypt$'))
  133
+        self.assertTrue(is_password_usable(blank_encoded))
  134
+        self.assertTrue(check_password('', blank_encoded))
  135
+        self.assertFalse(check_password(' ', blank_encoded))
95 136
 
96 137
     @skipUnless(bcrypt, "bcrypt not installed")
97 138
     def test_bcrypt_sha256(self):
@@ -108,6 +149,12 @@ def test_bcrypt_sha256(self):
108 149
         encoded = make_password(password, hasher='bcrypt_sha256')
109 150
         self.assertTrue(check_password(password, encoded))
110 151
         self.assertFalse(check_password(password[:72], encoded))
  152
+        # Blank passwords
  153
+        blank_encoded = make_password('', hasher='bcrypt_sha256')
  154
+        self.assertTrue(blank_encoded.startswith('bcrypt_sha256$'))
  155
+        self.assertTrue(is_password_usable(blank_encoded))
  156
+        self.assertTrue(check_password('', blank_encoded))
  157
+        self.assertFalse(check_password(' ', blank_encoded))
111 158
 
112 159
     @skipUnless(bcrypt, "bcrypt not installed")
113 160
     def test_bcrypt(self):
@@ -117,6 +164,12 @@ def test_bcrypt(self):
117 164
         self.assertTrue(check_password('lètmein', encoded))
118 165
         self.assertFalse(check_password('lètmeinz', encoded))
119 166
         self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
  167
+        # Blank passwords
  168
+        blank_encoded = make_password('', hasher='bcrypt')
  169
+        self.assertTrue(blank_encoded.startswith('bcrypt$'))
  170
+        self.assertTrue(is_password_usable(blank_encoded))
  171
+        self.assertTrue(check_password('', blank_encoded))
  172
+        self.assertFalse(check_password(' ', blank_encoded))
120 173
 
121 174
     def test_unusable(self):
122 175
         encoded = make_password(None)
16  docs/ref/contrib/auth.txt
@@ -132,12 +132,28 @@ Methods
132 132
         password hashing. Doesn't save the
133 133
         :class:`~django.contrib.auth.models.User` object.
134 134
 
  135
+        When the ``raw_password`` is ``None``, the password will be set to an
  136
+        unusable password, as if
  137
+        :meth:`~django.contrib.auth.models.User.set_unusable_password()`
  138
+        were used.
  139
+
  140
+        .. versionchanged:: 1.6
  141
+
  142
+            In Django 1.4 and 1.5, a blank string was unintentionally stored
  143
+            as an unsable password.
  144
+
135 145
     .. method:: check_password(raw_password)
136 146
 
137 147
         Returns ``True`` if the given raw string is the correct password for
138 148
         the user. (This takes care of the password hashing in making the
139 149
         comparison.)
140 150
 
  151
+        .. versionchanged:: 1.6
  152
+
  153
+            In Django 1.4 and 1.5, a blank string was unintentionally
  154
+            considered to be an unusable password, resulting in this method
  155
+            returning ``False`` for such a password.
  156
+
141 157
     .. method:: set_unusable_password()
142 158
 
143 159
         Marks the user as having no password set.  This isn't the same as
9  docs/releases/1.6.txt
@@ -701,6 +701,15 @@ Miscellaneous
701 701
 * :class:`~django.views.generic.base.RedirectView` now has a `pattern_name`
702 702
   attribute which allows it to choose the target by reversing the URL.
703 703
 
  704
+* In Django 1.4 and 1.5, a blank string was unintentionally not considered to
  705
+  be a valid password. This meant
  706
+  :meth:`~django.contrib.auth.models.User.set_password()` would save a blank
  707
+  password as an unusable password like
  708
+  :meth:`~django.contrib.auth.models.User.set_unusable_password()` does, and
  709
+  thus :meth:`~django.contrib.auth.models.User.check_password()` always
  710
+  returned ``False`` for blank passwords. This has been corrected in this
  711
+  release: blank passwords are now valid.
  712
+
704 713
 Features deprecated in 1.6
705 714
 ==========================
706 715
 
16  docs/topics/auth/customizing.txt
@@ -583,12 +583,28 @@ The following methods are available on any subclass of
583 583
         password hashing. Doesn't save the
584 584
         :class:`~django.contrib.auth.models.AbstractBaseUser` object.
585 585
 
  586
+        When the raw_password is ``None``, the password will be set to an
  587
+        unusable password, as if
  588
+        :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()`
  589
+        were used.
  590
+
  591
+        .. versionchanged:: 1.6
  592
+
  593
+            In Django 1.4 and 1.5, a blank string was unintentionally stored
  594
+            as an unsable password as well.
  595
+
586 596
     .. method:: models.AbstractBaseUser.check_password(raw_password)
587 597
 
588 598
         Returns ``True`` if the given raw string is the correct password for
589 599
         the user. (This takes care of the password hashing in making the
590 600
         comparison.)
591 601
 
  602
+        .. versionchanged:: 1.6
  603
+
  604
+            In Django 1.4 and 1.5, a blank string was unintentionally
  605
+            considered to be an unusable password, resulting in this method
  606
+            returning ``False`` for such a password.
  607
+
592 608
     .. method:: models.AbstractBaseUser.set_unusable_password()
593 609
 
594 610
         Marks the user as having no password set.  This isn't the same as
6  docs/topics/auth/passwords.txt
@@ -206,6 +206,12 @@ from the ``User`` model.
206 206
     database to check against, and returns ``True`` if they match, ``False``
207 207
     otherwise.
208 208
 
  209
+    .. versionchanged:: 1.6
  210
+
  211
+        In Django 1.4 and 1.5, a blank string was unintentionally considered
  212
+        to be an unusable password, resulting in this method returning
  213
+        ``False`` for such a password.
  214
+
209 215
 .. function:: make_password(password[, salt, hashers])
210 216
 
211 217
     Creates a hashed password in the format used by this application. It takes

0 notes on commit 2c4fe76

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