+86
-6
lines changedFilter options
+86
-6
lines changed Original file line number Diff line number Diff line change
@@ -16,12 +16,27 @@
16
16
from django.template import loader
17
17
from django.utils.encoding import force_bytes
18
18
from django.utils.http import urlsafe_base64_encode
19
+
from django.utils.six import PY3
19
20
from django.utils.text import capfirst
20
21
from django.utils.translation import ugettext, ugettext_lazy as _
21
22
22
23
UserModel = get_user_model()
23
24
24
25
26
+
def _unicode_ci_compare(s1, s2):
27
+
"""
28
+
Perform case-insensitive comparison of two identifiers, using the
29
+
recommended algorithm from Unicode Technical Report 36, section
30
+
2.11.2(B)(2).
31
+
"""
32
+
normalized1 = unicodedata.normalize('NFKC', s1)
33
+
normalized2 = unicodedata.normalize('NFKC', s2)
34
+
if PY3:
35
+
return normalized1.casefold() == normalized2.casefold()
36
+
# lower() is the best alternative available on Python 2.
37
+
return normalized1.lower() == normalized2.lower()
38
+
39
+
25
40
class ReadOnlyPasswordHashWidget(forms.Widget):
26
41
template_name = 'auth/widgets/read_only_password_hash.html'
27
42
@@ -249,11 +264,16 @@ def get_users(self, email):
249
264
that prevent inactive users and users with unusable passwords from
250
265
resetting their password.
251
266
"""
267
+
email_field_name = UserModel.get_email_field_name()
252
268
active_users = UserModel._default_manager.filter(**{
253
-
'%s__iexact' % UserModel.get_email_field_name(): email,
269
+
'%s__iexact' % email_field_name: email,
254
270
'is_active': True,
255
271
})
256
-
return (u for u in active_users if u.has_usable_password())
272
+
return (
273
+
u for u in active_users
274
+
if u.has_usable_password() and
275
+
_unicode_ci_compare(email, getattr(u, email_field_name))
276
+
)
257
277
258
278
def save(self, domain_override=None,
259
279
subject_template_name='registration/password_reset_subject.txt',
@@ -266,15 +286,17 @@ def save(self, domain_override=None,
266
286
user.
267
287
"""
268
288
email = self.cleaned_data["email"]
289
+
email_field_name = UserModel.get_email_field_name()
269
290
for user in self.get_users(email):
270
291
if not domain_override:
271
292
current_site = get_current_site(request)
272
293
site_name = current_site.name
273
294
domain = current_site.domain
274
295
else:
275
296
site_name = domain = domain_override
297
+
user_email = getattr(user, email_field_name)
276
298
context = {
277
-
'email': email,
299
+
'email': user_email,
278
300
'domain': domain,
279
301
'site_name': site_name,
280
302
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
@@ -286,7 +308,7 @@ def save(self, domain_override=None,
286
308
context.update(extra_email_context)
287
309
self.send_mail(
288
310
subject_template_name, email_template_name, context, from_email,
289
-
email, html_email_template_name=html_email_template_name,
311
+
user_email, html_email_template_name=html_email_template_name,
290
312
)
291
313
292
314
Original file line number Diff line number Diff line change
@@ -2,9 +2,25 @@
2
2
Django 1.11.27 release notes
3
3
============================
4
4
5
-
*Expected January 2, 2020*
5
+
*December 18, 2019*
6
6
7
-
Django 1.11.27 fixes a data loss bug in 1.11.26.
7
+
Django 1.11.27 fixes a security issue and a data loss bug in 1.11.26.
8
+
9
+
CVE-2019-19844: Potential account hijack via password reset form
10
+
================================================================
11
+
12
+
By submitting a suitably crafted email address making use of Unicode
13
+
characters, that compared equal to an existing user email when lower-cased for
14
+
comparison, an attacker could be sent a password reset token for the matched
15
+
account.
16
+
17
+
In order to avoid this vulnerability, password reset requests now compare the
18
+
submitted email using the stricter, recommended algorithm for case-insensitive
19
+
comparison of two identifiers from `Unicode Technical Report 36, section
20
+
2.11.2(B)(2)`__. Upon a match, the email containing the reset token will be
21
+
sent to the email address on record rather than the submitted address.
22
+
23
+
.. __: https://www.unicode.org/reports/tr36/#Recommendations_General
8
24
9
25
Bugfixes
10
26
========
Original file line number Diff line number Diff line change
@@ -694,6 +694,48 @@ def test_invalid_email(self):
694
694
self.assertFalse(form.is_valid())
695
695
self.assertEqual(form['email'].errors, [_('Enter a valid email address.')])
696
696
697
+
def test_user_email_unicode_collision(self):
698
+
User.objects.create_user('mike123', 'mike@example.org', 'test123')
699
+
User.objects.create_user('mike456', 'mıke@example.org', 'test123')
700
+
data = {'email': 'mıke@example.org'}
701
+
form = PasswordResetForm(data)
702
+
if six.PY2:
703
+
self.assertFalse(form.is_valid())
704
+
else:
705
+
self.assertTrue(form.is_valid())
706
+
form.save()
707
+
self.assertEqual(len(mail.outbox), 1)
708
+
self.assertEqual(mail.outbox[0].to, ['mıke@example.org'])
709
+
710
+
def test_user_email_domain_unicode_collision(self):
711
+
User.objects.create_user('mike123', 'mike@ixample.org', 'test123')
712
+
User.objects.create_user('mike456', 'mike@ıxample.org', 'test123')
713
+
data = {'email': 'mike@ıxample.org'}
714
+
form = PasswordResetForm(data)
715
+
self.assertTrue(form.is_valid())
716
+
form.save()
717
+
self.assertEqual(len(mail.outbox), 1)
718
+
self.assertEqual(mail.outbox[0].to, ['mike@ıxample.org'])
719
+
720
+
def test_user_email_unicode_collision_nonexistent(self):
721
+
User.objects.create_user('mike123', 'mike@example.org', 'test123')
722
+
data = {'email': 'mıke@example.org'}
723
+
form = PasswordResetForm(data)
724
+
if six.PY2:
725
+
self.assertFalse(form.is_valid())
726
+
else:
727
+
self.assertTrue(form.is_valid())
728
+
form.save()
729
+
self.assertEqual(len(mail.outbox), 0)
730
+
731
+
def test_user_email_domain_unicode_collision_nonexistent(self):
732
+
User.objects.create_user('mike123', 'mike@ixample.org', 'test123')
733
+
data = {'email': 'mike@ıxample.org'}
734
+
form = PasswordResetForm(data)
735
+
self.assertTrue(form.is_valid())
736
+
form.save()
737
+
self.assertEqual(len(mail.outbox), 0)
738
+
697
739
def test_nonexistent_email(self):
698
740
"""
699
741
Test nonexistent email address. This should not fail because it would
You can’t perform that action at this time.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4