Japanese edition

This article describes the Django vulnerability CVE-2019-19844, which was fixed on December 18, 2019.

This vulnerability allows for account hijacking

This vulnerability allows for account hijacking

Please refer to the following for release information on the official website.

Django security releases issued: 3.0.1, 2.2.9, and 1.11.27 | Weblog | Django

Affected Versions

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.

ryu22e/django_cve_2019_19844_poc

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
    • Username: mike123, Email: mike@example.org
  • Email address of attacker
    • mıke@example.org1

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.

  1. Check if the email address entered from the form exists in the database
  2. Send email to the email address entered from the form (if data exists in 1.)

The code corresponding to 1. is quoted below.

272
273
274
275
active_users = UserModel._default_manager.filter(**{
    '%s__iexact' % UserModel.get_email_field_name(): email,
    'is_active': True,
})
https://github.com/django/django/blob/3.0/django/contrib/auth/forms.py#L272-L275

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 shell command.

1
2
3
4
5
6
7
8
>>> from django.contrib.auth import get_user_model
>>> email = 'mıke@example.org'  # Email address of attacker
>>> UserModel = get_user_model()
>>> UserModel._default_manager.filter(**{
...     '%s__iexact' % UserModel.get_email_field_name(): email,
...     'is_active': True,
... })
<QuerySet [<User: mike123>]>  # Existing users

Originally, mike@example.org and mıke@example.org are different email addresses, but iexact will find the corresponding data because i and ı 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 2.).

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.

  1. 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).
  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.

  1. 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.

  1. 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.


  1. The second character from the left is “dotless ı” in Turkish and Azerbaijani. ↩︎