English edition

2019年12月18日に修正されたDjangoの脆弱性CVE-2019-19844について解説します。

今回の脆弱性はアカウントの乗っ取りが可能になるものです

今回の脆弱性はアカウントの乗っ取りが可能になるものです

公式サイトでのリリース情報は以下を参照してください。

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

影響を受けるバージョン

以下のバージョンが影響を受けます。

  • Django master branch
  • Django 3.0
  • Django 2.2
  • Django 1.11

脆弱性の内容(Potential account hijack via password reset form)

パスワードリセット機能を悪用して、攻撃者が既存ユーザーのパスワードを任意の値に変更することができます。

脆弱性を利用した攻撃の例

今回はコード量が多いので、以下GitHubリポジトリを用意しました。

ryu22e/django_cve_2019_19844_poc

上記アプリケーションを動かすには、以下の環境が必要です。

  • PostgreSQL 9.5以上
  • Python 3.7.x

また、以下の前提で作られています。

  • 既存ユーザー
    • ユーザー名mike123、メールアドレスmike@example.org
  • 攻撃者のメールアドレス
    • mıke@example.org1

READMEの「Setup」に従って既存ユーザーを作るところまで進めてください。

次に、READMEの「Procedure For Reproducing」に従ってパスワードリセットを実行してください。

攻撃者が用意したメールアドレスmıke@example.orgmike123ユーザーのパスワードリセット用メールが送られ、そこからmike123ユーザーのパスワードを勝手に変更することができたはずです。

どのコードに問題があったか・どのように修正されたか

実際の変更内容は以下のGitHubコードを参照してください。

まず、修正前のコードを確認してみましょう。django.contrib.auth.forms.PasswordResetForm.saveメソッドからパスワードリセット用メールが送られる流れは以下のとおりです。

  1. フォームから入力されたメールアドレスがデータベースに存在するか確認
  2. 1.でデータが存在していたら)フォームから入力されたメールアドレスにメールを送信

1.に該当するコードを以下に引用します。

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

一見問題がないように見えますが、iexactで大文字・小文字を区別しないようにしている点に罠が潜んでいます。先述のryu22e/django_cve_2019_19844_pocの「Setup」までを済ませた状態で、shellコマンド上で以下コードを実行してみると分かります。

1
2
3
4
5
6
7
8
>>> from django.contrib.auth import get_user_model
>>> email = 'mıke@example.org'  # 攻撃者のメールアドレス
>>> UserModel = get_user_model()
>>> UserModel._default_manager.filter(**{
...     '%s__iexact' % UserModel.get_email_field_name(): email,
...     'is_active': True,
... })
<QuerySet [<User: mike123>]>  # 既存ユーザー

本来、mike@example.orgmıke@example.orgは違うメールアドレスですが、iexactではiıが同一文字と判断されるために、該当データが見つかってしまいます。

さらに、2.では「フォームから入力されたメールアドレス」にメールを送信するため、データベース上に存在しないメールアドレスmıke@example.orgにメールが送られます。

これらの問題に対して、2点の変更がありました。公式サイトのリリースノート「To resolve this, two changes were made in Django:」に書かれている説明を以下に引用します。

  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.

順番に解説します。

  1. After retrieving a list of …

データベースから取得したメールアドレスとフォームから入力されたメールアドレスを比較して、値が一致しないものは無視するようになりました。

値の比較はUnicode Technical Report 36, section 2.11.2(B)(2).が推奨する方法に従って、unicodedata.normalizeでUnicode正規化し、str.casefoldで小文字化した文字列を対象にしています。

これで、大文字・小文字を区別せずメールアドレスを入力できる機能は残しつつ、iexactによる想定外の値を取り除くことができるようになりました。

  1. When generating password-reset emails, …

メール送信はフォームに入力されたメールアドレスではなく、データベースに登録されているものに対して行われるようになりました。これにより、もし1.にバグがあっても攻撃者にメールが届くことはなくなりました。


  1. 左から2番目の文字はトルコ語・アゼルバイジャン語の「点のない ı」。 ↩︎