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

Skip to content

Commit 3f3d887

Browse files
committed
[1.4.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 75d2bcd commit 3f3d887

File tree

3 files changed

+136
-16
lines changed

3 files changed

+136
-16
lines changed

django/contrib/auth/forms.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
from django.contrib.auth import authenticate
1010
from django.contrib.auth.models import User
11-
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, get_hasher
11+
from django.contrib.auth.hashers import (
12+
MAXIMUM_PASSWORD_LENGTH, UNUSABLE_PASSWORD,
13+
is_password_usable, get_hasher
14+
)
1215
from django.contrib.auth.tokens import default_token_generator
1316
from django.contrib.sites.models import get_current_site
1417

@@ -70,10 +73,11 @@ class UserCreationForm(forms.ModelForm):
7073
'invalid': _("This value may contain only letters, numbers and "
7174
"@/./+/-/_ characters.")})
7275
password1 = forms.CharField(label=_("Password"),
73-
widget=forms.PasswordInput)
76+
widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
7477
password2 = forms.CharField(label=_("Password confirmation"),
7578
widget=forms.PasswordInput,
76-
help_text = _("Enter the same password as above, for verification."))
79+
max_length=MAXIMUM_PASSWORD_LENGTH,
80+
help_text=_("Enter the same password as above, for verification."))
7781

7882
class Meta:
7983
model = User
@@ -137,7 +141,11 @@ class AuthenticationForm(forms.Form):
137141
username/password logins.
138142
"""
139143
username = forms.CharField(label=_("Username"), max_length=30)
140-
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
144+
password = forms.CharField(
145+
label=_("Password"),
146+
widget=forms.PasswordInput,
147+
max_length=MAXIMUM_PASSWORD_LENGTH,
148+
)
141149

142150
error_messages = {
143151
'invalid_login': _("Please enter a correct username and password. "
@@ -250,10 +258,16 @@ class SetPasswordForm(forms.Form):
250258
error_messages = {
251259
'password_mismatch': _("The two password fields didn't match."),
252260
}
253-
new_password1 = forms.CharField(label=_("New password"),
254-
widget=forms.PasswordInput)
255-
new_password2 = forms.CharField(label=_("New password confirmation"),
256-
widget=forms.PasswordInput)
261+
new_password1 = forms.CharField(
262+
label=_("New password"),
263+
widget=forms.PasswordInput,
264+
max_length=MAXIMUM_PASSWORD_LENGTH,
265+
)
266+
new_password2 = forms.CharField(
267+
label=_("New password confirmation"),
268+
widget=forms.PasswordInput,
269+
max_length=MAXIMUM_PASSWORD_LENGTH,
270+
)
257271

258272
def __init__(self, user, *args, **kwargs):
259273
self.user = user
@@ -284,8 +298,11 @@ class PasswordChangeForm(SetPasswordForm):
284298
'password_incorrect': _("Your old password was entered incorrectly. "
285299
"Please enter it again."),
286300
})
287-
old_password = forms.CharField(label=_("Old password"),
288-
widget=forms.PasswordInput)
301+
old_password = forms.CharField(
302+
label=_("Old password"),
303+
widget=forms.PasswordInput,
304+
max_length=MAXIMUM_PASSWORD_LENGTH,
305+
)
289306

290307
def clean_old_password(self):
291308
"""
@@ -307,10 +324,16 @@ class AdminPasswordChangeForm(forms.Form):
307324
error_messages = {
308325
'password_mismatch': _("The two password fields didn't match."),
309326
}
310-
password1 = forms.CharField(label=_("Password"),
311-
widget=forms.PasswordInput)
312-
password2 = forms.CharField(label=_("Password (again)"),
313-
widget=forms.PasswordInput)
327+
password1 = forms.CharField(
328+
label=_("Password"),
329+
widget=forms.PasswordInput,
330+
max_length=MAXIMUM_PASSWORD_LENGTH,
331+
)
332+
password2 = forms.CharField(
333+
label=_("Password (again)"),
334+
widget=forms.PasswordInput,
335+
max_length=MAXIMUM_PASSWORD_LENGTH,
336+
)
314337

315338
def __init__(self, user, *args, **kwargs):
316339
self.user = user

django/contrib/auth/hashers.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import functools
12
import hashlib
23

34
from django.conf import settings
@@ -11,10 +12,23 @@
1112

1213

1314
UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash
15+
MAXIMUM_PASSWORD_LENGTH = 4096 # The maximum length a password can be to prevent DoS
1416
HASHERS = None # lazily loaded from PASSWORD_HASHERS
1517
PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS
1618

1719

20+
def password_max_length(max_length):
21+
def inner(fn):
22+
@functools.wraps(fn)
23+
def wrapper(self, password, *args, **kwargs):
24+
if len(password) > max_length:
25+
raise ValueError("Invalid password; Must be less than or equal"
26+
" to %d bytes" % max_length)
27+
return fn(self, password, *args, **kwargs)
28+
return wrapper
29+
return inner
30+
31+
1832
def is_password_usable(encoded):
1933
return (encoded is not None and encoded != UNUSABLE_PASSWORD)
2034

@@ -202,6 +216,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
202216
iterations = 10000
203217
digest = hashlib.sha256
204218

219+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
205220
def encode(self, password, salt, iterations=None):
206221
assert password
207222
assert salt and '$' not in salt
@@ -211,6 +226,7 @@ def encode(self, password, salt, iterations=None):
211226
hash = hash.encode('base64').strip()
212227
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
213228

229+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
214230
def verify(self, password, encoded):
215231
algorithm, iterations, salt, hash = encoded.split('$', 3)
216232
assert algorithm == self.algorithm
@@ -256,11 +272,13 @@ def salt(self):
256272
bcrypt = self._load_library()
257273
return bcrypt.gensalt(self.rounds)
258274

275+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
259276
def encode(self, password, salt):
260277
bcrypt = self._load_library()
261278
data = bcrypt.hashpw(password, salt)
262279
return "%s$%s" % (self.algorithm, data)
263280

281+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
264282
def verify(self, password, encoded):
265283
algorithm, data = encoded.split('$', 1)
266284
assert algorithm == self.algorithm
@@ -285,12 +303,14 @@ class SHA1PasswordHasher(BasePasswordHasher):
285303
"""
286304
algorithm = "sha1"
287305

306+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
288307
def encode(self, password, salt):
289308
assert password
290309
assert salt and '$' not in salt
291310
hash = hashlib.sha1(salt + password).hexdigest()
292311
return "%s$%s$%s" % (self.algorithm, salt, hash)
293312

313+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
294314
def verify(self, password, encoded):
295315
algorithm, salt, hash = encoded.split('$', 2)
296316
assert algorithm == self.algorithm
@@ -313,12 +333,14 @@ class MD5PasswordHasher(BasePasswordHasher):
313333
"""
314334
algorithm = "md5"
315335

336+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
316337
def encode(self, password, salt):
317338
assert password
318339
assert salt and '$' not in salt
319340
hash = hashlib.md5(salt + password).hexdigest()
320341
return "%s$%s$%s" % (self.algorithm, salt, hash)
321342

343+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
322344
def verify(self, password, encoded):
323345
algorithm, salt, hash = encoded.split('$', 2)
324346
assert algorithm == self.algorithm
@@ -349,11 +371,13 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
349371
def salt(self):
350372
return ''
351373

374+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
352375
def encode(self, password, salt):
353376
assert salt == ''
354377
hash = hashlib.sha1(password).hexdigest()
355378
return 'sha1$$%s' % hash
356379

380+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
357381
def verify(self, password, encoded):
358382
encoded_2 = self.encode(password, '')
359383
return constant_time_compare(encoded, encoded_2)
@@ -383,10 +407,12 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
383407
def salt(self):
384408
return ''
385409

410+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
386411
def encode(self, password, salt):
387412
assert salt == ''
388413
return hashlib.md5(password).hexdigest()
389414

415+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
390416
def verify(self, password, encoded):
391417
if len(encoded) == 37 and encoded.startswith('md5$$'):
392418
encoded = encoded[5:]
@@ -412,13 +438,15 @@ class CryptPasswordHasher(BasePasswordHasher):
412438
def salt(self):
413439
return get_random_string(2)
414440

441+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
415442
def encode(self, password, salt):
416443
crypt = self._load_library()
417444
assert len(salt) == 2
418445
data = crypt.crypt(password, salt)
419446
# we don't need to store the salt, but Django used to do this
420447
return "%s$%s$%s" % (self.algorithm, '', data)
421448

449+
@password_max_length(MAXIMUM_PASSWORD_LENGTH)
422450
def verify(self, password, encoded):
423451
crypt = self._load_library()
424452
algorithm, salt, data = encoded.split('$', 2)
@@ -433,4 +461,3 @@ def safe_summary(self, encoded):
433461
(_('salt'), salt),
434462
(_('hash'), mask_hash(data, show=3)),
435463
])
436-

django/contrib/auth/tests/hashers.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
22
from django.contrib.auth.hashers import (is_password_usable,
33
check_password, make_password, PBKDF2PasswordHasher, load_hashers,
4-
PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD)
4+
PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD,
5+
MAXIMUM_PASSWORD_LENGTH, password_max_length)
56
from django.utils import unittest
67
from django.utils.unittest import skipUnless
78
from django.test.utils import override_settings
@@ -28,6 +29,12 @@ def test_simple(self):
2829
self.assertTrue(is_password_usable(encoded))
2930
self.assertTrue(check_password(u'letmein', encoded))
3031
self.assertFalse(check_password('letmeinz', encoded))
32+
# Long password
33+
self.assertRaises(
34+
ValueError,
35+
make_password,
36+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
37+
)
3138

3239
def test_pkbdf2(self):
3340
encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256')
@@ -36,6 +43,14 @@ def test_pkbdf2(self):
3643
self.assertTrue(is_password_usable(encoded))
3744
self.assertTrue(check_password(u'letmein', encoded))
3845
self.assertFalse(check_password('letmeinz', encoded))
46+
# Long password
47+
self.assertRaises(
48+
ValueError,
49+
make_password,
50+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
51+
"seasalt",
52+
"pbkdf2_sha256",
53+
)
3954

4055
def test_sha1(self):
4156
encoded = make_password('letmein', 'seasalt', 'sha1')
@@ -44,6 +59,14 @@ def test_sha1(self):
4459
self.assertTrue(is_password_usable(encoded))
4560
self.assertTrue(check_password(u'letmein', encoded))
4661
self.assertFalse(check_password('letmeinz', encoded))
62+
# Long password
63+
self.assertRaises(
64+
ValueError,
65+
make_password,
66+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
67+
"seasalt",
68+
"sha1",
69+
)
4770

4871
def test_md5(self):
4972
encoded = make_password('letmein', 'seasalt', 'md5')
@@ -52,6 +75,14 @@ def test_md5(self):
5275
self.assertTrue(is_password_usable(encoded))
5376
self.assertTrue(check_password(u'letmein', encoded))
5477
self.assertFalse(check_password('letmeinz', encoded))
78+
# Long password
79+
self.assertRaises(
80+
ValueError,
81+
make_password,
82+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
83+
"seasalt",
84+
"md5",
85+
)
5586

5687
def test_unsalted_md5(self):
5788
encoded = make_password('letmein', '', 'unsalted_md5')
@@ -64,6 +95,14 @@ def test_unsalted_md5(self):
6495
self.assertTrue(is_password_usable(alt_encoded))
6596
self.assertTrue(check_password(u'letmein', alt_encoded))
6697
self.assertFalse(check_password('letmeinz', alt_encoded))
98+
# Long password
99+
self.assertRaises(
100+
ValueError,
101+
make_password,
102+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
103+
"",
104+
"unsalted_md5",
105+
)
67106

68107
def test_unsalted_sha1(self):
69108
encoded = make_password('letmein', '', 'unsalted_sha1')
@@ -74,6 +113,14 @@ def test_unsalted_sha1(self):
74113
# Raw SHA1 isn't acceptable
75114
alt_encoded = encoded[6:]
76115
self.assertRaises(ValueError, check_password, 'letmein', alt_encoded)
116+
# Long password
117+
self.assertRaises(
118+
ValueError,
119+
make_password,
120+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
121+
"",
122+
"unslated_sha1",
123+
)
77124

78125
@skipUnless(crypt, "no crypt module to generate password.")
79126
def test_crypt(self):
@@ -82,6 +129,14 @@ def test_crypt(self):
82129
self.assertTrue(is_password_usable(encoded))
83130
self.assertTrue(check_password(u'letmein', encoded))
84131
self.assertFalse(check_password('letmeinz', encoded))
132+
# Long password
133+
self.assertRaises(
134+
ValueError,
135+
make_password,
136+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
137+
"seasalt",
138+
"crypt",
139+
)
85140

86141
@skipUnless(bcrypt, "py-bcrypt not installed")
87142
def test_bcrypt(self):
@@ -90,6 +145,13 @@ def test_bcrypt(self):
90145
self.assertTrue(encoded.startswith('bcrypt$'))
91146
self.assertTrue(check_password(u'letmein', encoded))
92147
self.assertFalse(check_password('letmeinz', encoded))
148+
# Long password
149+
self.assertRaises(
150+
ValueError,
151+
make_password,
152+
b"1" * (MAXIMUM_PASSWORD_LENGTH + 1),
153+
hasher="bcrypt",
154+
)
93155

94156
def test_unusable(self):
95157
encoded = make_password(None)
@@ -105,6 +167,14 @@ def doit():
105167
make_password('letmein', hasher='lolcat')
106168
self.assertRaises(ValueError, doit)
107169

170+
def test_max_password_length_decorator(self):
171+
@password_max_length(10)
172+
def encode(s, password, salt):
173+
return True
174+
175+
self.assertTrue(encode(None, b"1234", b"1234"))
176+
self.assertRaises(ValueError, encode, None, b"1234567890A", b"1234")
177+
108178
def test_low_level_pkbdf2(self):
109179
hasher = PBKDF2PasswordHasher()
110180
encoded = hasher.encode('letmein', 'seasalt')

0 commit comments

Comments
 (0)