티스토리 수익 글 보기

티스토리 수익 글 보기

[1.11.x] Fixed CVE-2019-19844 — Used verified user email for passwor… · django/django@f4cff43 · GitHub
Skip to content

Commit f4cff43

Browse files
charettesapollo13
authored andcommitted
[1.11.x] Fixed CVE-2019-19844 — Used verified user email for password reset requests.
Backport of 5b1fbce from master. Co-Authored-By: Florian Apolloner <florian@apolloner.eu>
1 parent a235574 commit f4cff43

File tree

3 files changed

+86
6
lines changed

3 files changed

+86
6
lines changed

django/contrib/auth/forms.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,27 @@
1616
from django.template import loader
1717
from django.utils.encoding import force_bytes
1818
from django.utils.http import urlsafe_base64_encode
19+
from django.utils.six import PY3
1920
from django.utils.text import capfirst
2021
from django.utils.translation import ugettext, ugettext_lazy as _
2122

2223
UserModel = get_user_model()
2324

2425

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+
2540
class ReadOnlyPasswordHashWidget(forms.Widget):
2641
template_name = 'auth/widgets/read_only_password_hash.html'
2742

@@ -249,11 +264,16 @@ def get_users(self, email):
249264
that prevent inactive users and users with unusable passwords from
250265
resetting their password.
251266
"""
267+
email_field_name = UserModel.get_email_field_name()
252268
active_users = UserModel._default_manager.filter(**{
253-
'%s__iexact' % UserModel.get_email_field_name(): email,
269+
'%s__iexact' % email_field_name: email,
254270
'is_active': True,
255271
})
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+
)
257277

258278
def save(self, domain_override=None,
259279
subject_template_name='registration/password_reset_subject.txt',
@@ -266,15 +286,17 @@ def save(self, domain_override=None,
266286
user.
267287
"""
268288
email = self.cleaned_data["email"]
289+
email_field_name = UserModel.get_email_field_name()
269290
for user in self.get_users(email):
270291
if not domain_override:
271292
current_site = get_current_site(request)
272293
site_name = current_site.name
273294
domain = current_site.domain
274295
else:
275296
site_name = domain = domain_override
297+
user_email = getattr(user, email_field_name)
276298
context = {
277-
'email': email,
299+
'email': user_email,
278300
'domain': domain,
279301
'site_name': site_name,
280302
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
@@ -286,7 +308,7 @@ def save(self, domain_override=None,
286308
context.update(extra_email_context)
287309
self.send_mail(
288310
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,
290312
)
291313

292314

docs/releases/1.11.27.txt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,25 @@
22
Django 1.11.27 release notes
33
============================
44

5-
*Expected January 2, 2020*
5+
*December 18, 2019*
66

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
824

925
Bugfixes
1026
========

tests/auth_tests/test_forms.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,48 @@ def test_invalid_email(self):
694694
self.assertFalse(form.is_valid())
695695
self.assertEqual(form['email'].errors, [_('Enter a valid email address.')])
696696

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+
697739
def test_nonexistent_email(self):
698740
"""
699741
Test nonexistent email address. This should not fail because it would

0 commit comments

Comments
 (0)