1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
|
From: Chris Lamb <lamby@debian.org>
Date: Mon, 6 Jan 2020 17:50:09 +0000
Subject: CVE-2019-19844
---
django/contrib/auth/forms.py | 20 +++++++++++++++++++-
tests/auth_tests/test_forms.py | 42 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 18ea52c..d463ec9 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -19,9 +19,23 @@ from django.utils.encoding import force_bytes
from django.utils.html import format_html, format_html_join
from django.utils.http import urlsafe_base64_encode
from django.utils.safestring import mark_safe
+from django.utils.six import PY3
from django.utils.text import capfirst
from django.utils.translation import ugettext, ugettext_lazy as _
+def _unicode_ci_compare(s1, s2):
+ """
+ Perform case-insensitive comparison of two identifiers, using the
+ recommended algorithm from Unicode Technical Report 36, section
+ 2.11.2(B)(2).
+ """
+ normalized1 = unicodedata.normalize('NFKC', s1)
+ normalized2 = unicodedata.normalize('NFKC', s2)
+ if PY3:
+ return normalized1.casefold() == normalized2.casefold()
+ # lower() is the best alternative available on Python 2.
+ return normalized1.lower() == normalized2.lower()
+
class ReadOnlyPasswordHashWidget(forms.Widget):
def render(self, name, value, attrs):
@@ -255,7 +269,11 @@ class PasswordResetForm(forms.Form):
"""
active_users = get_user_model()._default_manager.filter(
email__iexact=email, is_active=True)
- return (u for u in active_users if u.has_usable_password())
+ return (
+ u for u in active_users
+ if u.has_usable_password() and
+ _unicode_ci_compare(email, u.email)
+ )
def save(self, domain_override=None,
subject_template_name='registration/password_reset_subject.txt',
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index 64c095b..9664d83 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -626,6 +626,48 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
self.assertFalse(form.is_valid())
self.assertEqual(form['email'].errors, [_('Enter a valid email address.')])
+ def test_user_email_unicode_collision(self):
+ User.objects.create_user('mike123', 'mike@example.org', 'test123')
+ User.objects.create_user('mike456', 'mıke@example.org', 'test123')
+ data = {'email': 'mıke@example.org'}
+ form = PasswordResetForm(data)
+ if six.PY2:
+ self.assertFalse(form.is_valid())
+ else:
+ self.assertTrue(form.is_valid())
+ form.save()
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].to, ['mıke@example.org'])
+
+ def test_user_email_domain_unicode_collision(self):
+ User.objects.create_user('mike123', 'mike@ixample.org', 'test123')
+ User.objects.create_user('mike456', 'mike@ıxample.org', 'test123')
+ data = {'email': 'mike@ıxample.org'}
+ form = PasswordResetForm(data)
+ self.assertTrue(form.is_valid())
+ form.save()
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].to, ['mike@ıxample.org'])
+
+ def test_user_email_unicode_collision_nonexistent(self):
+ User.objects.create_user('mike123', 'mike@example.org', 'test123')
+ data = {'email': 'mıke@example.org'}
+ form = PasswordResetForm(data)
+ if six.PY2:
+ self.assertFalse(form.is_valid())
+ else:
+ self.assertTrue(form.is_valid())
+ form.save()
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_user_email_domain_unicode_collision_nonexistent(self):
+ User.objects.create_user('mike123', 'mike@ixample.org', 'test123')
+ data = {'email': 'mike@ıxample.org'}
+ form = PasswordResetForm(data)
+ self.assertTrue(form.is_valid())
+ form.save()
+ self.assertEqual(len(mail.outbox), 0)
+
def test_nonexistent_email(self):
"""
Test nonexistent email address. This should not fail because it would
|