Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 22b74fa

Browse files
committed
[1.5.x] Ensure that passwords are never long enough for a DoS.
* Limit the password length to 4096 bytes * Password hashers will raise a ValueError * django.contrib.auth forms will fail validation * Document in release notes that this is a backwards incompatible change Thanks to Josh Wright for the report, and Donald Stufft for the patch. This is a security fix; disclosure to follow shortly. Backport of aae5a96 from master.
1 parent e66fe35 commit 22b74fa

File tree

3 files changed

+134
-15
lines changed

3 files changed

+134
-15
lines changed

django/contrib/auth/forms.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212

1313
from django.contrib.auth import authenticate, get_user_model
1414
from django.contrib.auth.models import User
15-
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher
15+
from django.contrib.auth.hashers import (
16+
MAXIMUM_PASSWORD_LENGTH, UNUSABLE_PASSWORD, identify_hasher,
17+
)
1618
from django.contrib.auth.tokens import default_token_generator
1719
from django.contrib.sites.models import get_current_site
1820

@@ -75,9 +77,10 @@ class UserCreationForm(forms.ModelForm):
7577
'invalid': _("This value may contain only letters, numbers and "
7678
"@/./+/-/_ characters.")})
7779
password1 = forms.CharField(label=_("Password"),
78-
widget=forms.PasswordInput)
80+
widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
7981
password2 = forms.CharField(label=_("Password confirmation"),
8082
widget=forms.PasswordInput,
83+
max_length=MAXIMUM_PASSWORD_LENGTH,
8184
help_text=_("Enter the same password as above, for verification."))
8285

8386
class Meta:
@@ -145,7 +148,11 @@ class AuthenticationForm(forms.Form):
145148
username/password logins.
146149
"""
147150
username = forms.CharField(max_length=254)
148-
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
151+
password = forms.CharField(
152+
label=_("Password"),
153+
widget=forms.PasswordInput,
154+
max_length=MAXIMUM_PASSWORD_LENGTH,
155+
)
149156

150157
error_messages = {
151158
'invalid_login': _("Please enter a correct %(username)s and password. "
@@ -269,10 +276,16 @@ class SetPasswordForm(forms.Form):
269276
error_messages = {
270277
'password_mismatch': _("The two password fields didn't match."),
271278
}
272-
new_password1 = forms.CharField(label=_("New password"),
273-
widget=forms.PasswordInput)
274-
new_password2 = forms.CharField(label=_("New password confirmation"),
275-
widget=forms.PasswordInput)
279+
new_password1 = forms.CharField(
280+
label=_("New password"),
281+
widget=forms.PasswordInput,
282+
max_length=MAXIMUM_PASSWORD_LENGTH,
283+
)
284+
new_password2 = forms.CharField(
285+
label=_("New password confirmation"),
286+
widget=forms.PasswordInput,
287+
max_length=MAXIMUM_PASSWORD_LENGTH,
288+
)
276289

277290
def __init__(self, user, *args, **kwargs):
278291
self.user = user
@@ -303,8 +316,11 @@ class PasswordChangeForm(SetPasswordForm):
303316
'password_incorrect': _("Your old password was entered incorrectly. "
304317
"Please enter it again."),
305318
})
306-
old_password = forms.CharField(label=_("Old password"),
307-
widget=forms.PasswordInput)
319+
old_password = forms.CharField(
320+
label=_("Old password"),
321+
widget=forms.PasswordInput,
322+
max_length=MAXIMUM_PASSWORD_LENGTH,
323+
)
308324

309325
def clean_old_password(self):
310326
"""
@@ -329,10 +345,16 @@ class AdminPasswordChangeForm(forms.Form):
329345
error_messages = {
330346
'password_mismatch': _("The two password fields didn't match."),
331347
}
332-
password1 = forms.CharField(label=_("Password"),
333-
widget=forms.PasswordInput)
334-
password2 = forms.CharField(label=_("Password (again)"),
335-
widget=forms.PasswordInput)
348+
password1 = forms.CharField(
349+
label=_("Password"),
350+
widget=forms.PasswordInput,
351+
max_length=MAXIMUM_PASSWORD_LENGTH,
352+
)
353+
password2 = forms.CharField(
354+
label=_("Password (again)"),
355+
widget=forms.PasswordInput,
356+
max_length=MAXIMUM_PASSWORD_LENGTH,
357+
)
336358

337359
def __init__(self, user, *args, **kwargs):
338360
self.user = user

django/contrib/auth/hashers.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import unicode_literals
22

33
import base64
4+
import functools
45
import hashlib
56

67
from django.dispatch import receiver
@@ -16,6 +17,7 @@
1617

1718

1819
UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash
20+
MAXIMUM_PASSWORD_LENGTH = 4096 # The maximum length a password can be to prevent DoS
1921
HASHERS = None # lazily loaded from PASSWORD_HASHERS
2022
PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS
2123

@@ -27,6 +29,18 @@ def reset_hashers(**kwargs):
2729
PREFERRED_HASHER = None
2830

2931

32+
def password_max_length(max_length):
33+
def inner(fn):
34+
@functools.wraps(fn)
35+
def wrapper(self, password, *args, **kwargs):
36+
if len(password) > max_length:
37+
raise ValueError("Invalid password; Must be less than or equal"
38+
" to %d bytes" % max_length)
39+
return fn(self, password, *args, **kwargs)
40+
return wrapper
41+
return inner
42+
43+
3044
def is_password_usable(encoded):
3145
if encoded is None or encoded == UNUSABLE_PASSWORD:
3246
return False
@@ -225,6 +239,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
225239
iterations = 10000
226240
digest = hashlib.sha256
227241

242+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
228243
def encode(self, password, salt, iterations=None):
229244
assert password
230245
assert salt and '$' not in salt
@@ -234,6 +249,7 @@ def encode(self, password, salt, iterations=None):
234249
hash = base64.b64encode(hash).decode('ascii').strip()
235250
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
236251

252+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
237253
def verify(self, password, encoded):
238254
algorithm, iterations, salt, hash = encoded.split('$', 3)
239255
assert algorithm == self.algorithm
@@ -279,13 +295,15 @@ def salt(self):
279295
bcrypt = self._load_library()
280296
return bcrypt.gensalt(self.rounds)
281297

298+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
282299
def encode(self, password, salt):
283300
bcrypt = self._load_library()
284301
# Need to reevaluate the force_bytes call once bcrypt is supported on
285302
# Python 3
286303
data = bcrypt.hashpw(force_bytes(password), salt)
287304
return "%s$%s" % (self.algorithm, data)
288305

306+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
289307
def verify(self, password, encoded):
290308
algorithm, data = encoded.split('$', 1)
291309
assert algorithm == self.algorithm
@@ -310,12 +328,14 @@ class SHA1PasswordHasher(BasePasswordHasher):
310328
"""
311329
algorithm = "sha1"
312330

331+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
313332
def encode(self, password, salt):
314333
assert password
315334
assert salt and '$' not in salt
316335
hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()
317336
return "%s$%s$%s" % (self.algorithm, salt, hash)
318337

338+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
319339
def verify(self, password, encoded):
320340
algorithm, salt, hash = encoded.split('$', 2)
321341
assert algorithm == self.algorithm
@@ -338,12 +358,14 @@ class MD5PasswordHasher(BasePasswordHasher):
338358
"""
339359
algorithm = "md5"
340360

361+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
341362
def encode(self, password, salt):
342363
assert password
343364
assert salt and '$' not in salt
344365
hash = hashlib.md5(force_bytes(salt + password)).hexdigest()
345366
return "%s$%s$%s" % (self.algorithm, salt, hash)
346367

368+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
347369
def verify(self, password, encoded):
348370
algorithm, salt, hash = encoded.split('$', 2)
349371
assert algorithm == self.algorithm
@@ -374,11 +396,13 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
374396
def salt(self):
375397
return ''
376398

399+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
377400
def encode(self, password, salt):
378401
assert salt == ''
379402
hash = hashlib.sha1(force_bytes(password)).hexdigest()
380403
return 'sha1$$%s' % hash
381404

405+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
382406
def verify(self, password, encoded):
383407
encoded_2 = self.encode(password, '')
384408
return constant_time_compare(encoded, encoded_2)
@@ -408,10 +432,12 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
408432
def salt(self):
409433
return ''
410434

435+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
411436
def encode(self, password, salt):
412437
assert salt == ''
413438
return hashlib.md5(force_bytes(password)).hexdigest()
414439

440+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
415441
def verify(self, password, encoded):
416442
if len(encoded) == 37 and encoded.startswith('md5$$'):
417443
encoded = encoded[5:]
@@ -437,13 +463,15 @@ class CryptPasswordHasher(BasePasswordHasher):
437463
def salt(self):
438464
return get_random_string(2)
439465

466+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
440467
def encode(self, password, salt):
441468
crypt = self._load_library()
442469
assert len(salt) == 2
443470
data = crypt.crypt(force_str(password), salt)
444471
# we don't need to store the salt, but Django used to do this
445472
return "%s$%s$%s" % (self.algorithm, '', data)
446473

474+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
447475
def verify(self, password, encoded):
448476
crypt = self._load_library()
449477
algorithm, salt, data = encoded.split('$', 2)
@@ -458,4 +486,3 @@ def safe_summary(self, encoded):
458486
(_('salt'), salt),
459487
(_('hash'), mask_hash(data, show=3)),
460488
])
461-

django/contrib/auth/tests/hashers.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
55
from django.contrib.auth.hashers import (is_password_usable,
66
check_password, make_password, PBKDF2PasswordHasher, load_hashers,
7-
PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
7+
PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD,
8+
MAXIMUM_PASSWORD_LENGTH, password_max_length)
89
from django.utils import unittest
910
from django.utils.unittest import skipUnless
1011

@@ -31,6 +32,12 @@ def test_simple(self):
3132
self.assertTrue(is_password_usable(encoded))
3233
self.assertTrue(check_password('lètmein', encoded))
3334
self.assertFalse(check_password('lètmeinz', encoded))
35+
# Long password
36+
self.assertRaises(
37+
ValueError,
38+
make_password,
39+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
40+
)
3441

3542
def test_pkbdf2(self):
3643
encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
@@ -40,6 +47,14 @@ def test_pkbdf2(self):
4047
self.assertTrue(check_password('lètmein', encoded))
4148
self.assertFalse(check_password('lètmeinz', encoded))
4249
self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
50+
# Long password
51+
self.assertRaises(
52+
ValueError,
53+
make_password,
54+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
55+
"seasalt",
56+
"pbkdf2_sha256",
57+
)
4358

4459
def test_sha1(self):
4560
encoded = make_password('lètmein', 'seasalt', 'sha1')
@@ -49,6 +64,14 @@ def test_sha1(self):
4964
self.assertTrue(check_password('lètmein', encoded))
5065
self.assertFalse(check_password('lètmeinz', encoded))
5166
self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
67+
# Long password
68+
self.assertRaises(
69+
ValueError,
70+
make_password,
71+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
72+
"seasalt",
73+
"sha1",
74+
)
5275

5376
def test_md5(self):
5477
encoded = make_password('lètmein', 'seasalt', 'md5')
@@ -58,6 +81,14 @@ def test_md5(self):
5881
self.assertTrue(check_password('lètmein', encoded))
5982
self.assertFalse(check_password('lètmeinz', encoded))
6083
self.assertEqual(identify_hasher(encoded).algorithm, "md5")
84+
# Long password
85+
self.assertRaises(
86+
ValueError,
87+
make_password,
88+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
89+
"seasalt",
90+
"md5",
91+
)
6192

6293
def test_unsalted_md5(self):
6394
encoded = make_password('lètmein', '', 'unsalted_md5')
@@ -71,6 +102,14 @@ def test_unsalted_md5(self):
71102
self.assertTrue(is_password_usable(alt_encoded))
72103
self.assertTrue(check_password('lètmein', alt_encoded))
73104
self.assertFalse(check_password('lètmeinz', alt_encoded))
105+
# Long password
106+
self.assertRaises(
107+
ValueError,
108+
make_password,
109+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
110+
"",
111+
"unsalted_md5",
112+
)
74113

75114
def test_unsalted_sha1(self):
76115
encoded = make_password('lètmein', '', 'unsalted_sha1')
@@ -82,6 +121,14 @@ def test_unsalted_sha1(self):
82121
# Raw SHA1 isn't acceptable
83122
alt_encoded = encoded[6:]
84123
self.assertFalse(check_password('lètmein', alt_encoded))
124+
# Long password
125+
self.assertRaises(
126+
ValueError,
127+
make_password,
128+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
129+
"",
130+
"unslated_sha1",
131+
)
85132

86133
@skipUnless(crypt, "no crypt module to generate password.")
87134
def test_crypt(self):
@@ -91,6 +138,14 @@ def test_crypt(self):
91138
self.assertTrue(check_password('lètmei', encoded))
92139
self.assertFalse(check_password('lètmeiz', encoded))
93140
self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
141+
# Long password
142+
self.assertRaises(
143+
ValueError,
144+
make_password,
145+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
146+
"seasalt",
147+
"crypt",
148+
)
94149

95150
@skipUnless(bcrypt, "py-bcrypt not installed")
96151
def test_bcrypt(self):
@@ -100,6 +155,13 @@ def test_bcrypt(self):
100155
self.assertTrue(check_password('lètmein', encoded))
101156
self.assertFalse(check_password('lètmeinz', encoded))
102157
self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
158+
# Long password
159+
self.assertRaises(
160+
ValueError,
161+
make_password,
162+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
163+
hasher="bcrypt",
164+
)
103165

104166
def test_unusable(self):
105167
encoded = make_password(None)
@@ -121,6 +183,14 @@ def test_bad_encoded(self):
121183
self.assertFalse(is_password_usable('lètmein_badencoded'))
122184
self.assertFalse(is_password_usable(''))
123185

186+
def test_max_password_length_decorator(self):
187+
@password_max_length(10)
188+
def encode(s, password, salt):
189+
return True
190+
191+
self.assertTrue(encode(None, b"1234", b"1234"))
192+
self.assertRaises(ValueError, encode, None, b"1234567890A", b"1234")
193+
124194
def test_low_level_pkbdf2(self):
125195
hasher = PBKDF2PasswordHasher()
126196
encoded = hasher.encode('lètmein', 'seasalt')

0 commit comments

Comments
 (0)