This article describes the Django vulnerability CVE-2019-19844, which was fixed on December 18, 2019.
Please refer to the following for release information on the official website.
The following versions are affected.
- Django master branch
- Django 3.0
- Django 2.2
- Django 1.11
Vulnerability Description(Potential Account Hijack via Password Reset Form)
By using the password reset function, an attacker can change the password of an existing user to an arbitrary value.
Proof of Concept
The following GitHub repository is prepared.
The following environment is required to run the above application.
- PostgreSQL 9.5 or higher
- Python 3.7.x
It is made on the following assumptions.
- Existing users
- Email address of attacker
Follow the “Setup” in the README to create an existing user.
Next, reset the password according to “Procedure For Reproducing” in the README.
The password reset password of the
mike123 user was sent to the email address
mıke@example.org prepared by the attacker, and from that the password of the
mike123 user could have been changed without permission.
An email will be sent to
mıke@example.org (Email address of attacker), and you must have been able to change the
mike123 user’s password without permission.
Which Code Was Wrong and How It Was Fixed
See the GitHub code below for the actual changes.
First, let’s check the code before modification. The flow of sending a password reset email from the
django.contrib.auth.forms.PasswordResetForm.save method is as follows.
- Check if the email address entered from the form exists in the database
- Send email to the email address entered from the form (if data exists in
The code corresponding to
1. is quoted below.
At first glance it looks fine, but the trap lies in the fact that
iexact is case insensitive.
After completing the above ryu22e/django_cve_2019_19844_poc “Setup”, you can see by executing the following code on the
mıke@example.org are different email addresses, but
iexact will find the corresponding data because
ı are judged to be the same character.
In addition, the mail is sent to the mail address
mıke@example.org that does not exist in the database. because “Send email to the email address entered from the form” (in Section
There have been two changes to these issues. The description in the official website release note “To resolve this, two changes were made in Django:" is quoted below.
- After retrieving a list of potentially-matching accounts from the database, Django’s password reset functionality now also checks the email address for equivalence in Python, using the recommended identifier-comparison process from Unicode Technical Report 36, section 2.11.2(B)(2).
- When generating password-reset emails, Django now sends to the email address retrieved from the database, rather than the email address submitted in the password-reset request form.
I will explain in order.
- After retrieving a list of …
The email address obtained from the database is compared with the email address entered from the form, and those with mismatched values are ignored.
To compare the values, use the method recommended by “Unicode Technical Report 36, section 2.11.2 (B) (2).” To convert the character string normalized by unicodedata.normalize and lowercased by str.casefold. It is targeted.
This allows you to remove unexpected values from
iexact, while retaining the ability to enter email addresses without regard to case.
- When generating password-reset emails, …
Emails are now sent to the database entry, not the email address entered in the form. As a result, even if there is a bug in
1., the attacker will not receive an email.