Djangoはパスワードをどうやって保存しているのか

Ryuji Tsutsui
CreditEngine Tech
Published in
12 min readOct 25, 2018

--

Django Software Foundation / CC BY-SA 3.0

こんにちは、筒井です。このブログでは、日々の業務で得た知見や個人的に興味がある分野について記事を書いていきたいと思います。よろしくお願いします。

今回は、Djangoの内部構造についての解説記事を書きます。

Djnagoにはパスワードをセキュアに保存する仕組みがあります。
この仕組みのおかげで、プログラマーはあれこれ考えずにセキュリティ上のリスクを回避できます。この記事では、Djangoがプログラマーの代わりに考えてくれる「あれこれ」の部分について解説します。
ソースコードの中でパスワードをどのよう扱っているか、それがなぜセキュアなのかを明らかにします。

なお、この記事で対象としているDjangoのバージョンは1.11 LTSです。

Table of Contents

ウェブアプリケーションにおけるパスワード保存の原則

Djangoの話の前に、一般論としてウェブアプリケーションでパスワードをセキュアに保存する方法について解説します。

プログラマーの経験が浅いとありがちですが、ユーザーが入力したパスワードをデータベースにそのまま保存している(平文で保存している)ウェブアプリケーションは時々あります。そのような実装には以下の手段によりパスワードが外部に漏洩するリスクがあります。

  • SQLインジェクション
  • バックアップメディアの盗難・持ち出し
  • ハードディスクの盗難・持ち出し
  • 内部のオペレータによる持ち出し

これらを完璧に防げればいいのですが、人間にミスは付き物です。気をつけていてもトラブルは起こります。そうなってもアカウントを奪われることがないよう、パスワードは平文で保存しないことが重要です。

ウェブアプリケーションでパスワードをセキュアに保存するには、以下の方法が原則です。

  1. 「入力されたパスワード」と「ソルト値(ランダムな文字列)」を結合
  2. 「ハッシュ関数を一定回数繰り返し使って 1.の値をハッシュ化」(ストレッチング)してからデータベースに保存

上記の方法により、データベースに保存されたパスワードは元の平文に戻すことができなくなり、たとえパスワードを盗まれてもアカウントを奪えません。

さらに詳細な解説は、「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践」471ページ「5.1.3パスワードの保存方法」を参照してください。

Djangoでパスワードを保存するロジックの場所と内容の解説

次に、Djangoでは前述の方法をどのような形で実装しているのかを見てみましょう。

Djangoでユーザーのパスワードを設定する際に呼ばれるのはdjango.contrib.auth.models.User.set_passwordメソッドです。まずここから辿ってみます。

django/contrib/auth/base_user.py

django.contrib.auth.hashers.make_passwordという関数で何かをやっていそうです。中身はこうなっています。

django/contrib/auth/hashers.py(日本語のコメントは筆者が追記したものです)

最後の hasher.encode(password, salt)は何をやっているのでしょうか? 前述の「ウェブアプリケーションにおけるパスワード保存の原則」に従うなら、入力されたパスワードとソルト値を結合してストレッチングしているはずですが、実際の定義を見てみましょう。

encode メソッドの定義は複数箇所にあります。
hasher 変数の中身は get_hasher 関数によって作られるオブジェクトで、複数のクラス定義があります。
(本記事では、このオブジェクトを「hasher」と呼ぶことにします)
クラスはPASSWORD_HASHERS に設定されているものが使われます。
(本記事では、このクラスを「hasherクラス」と呼ぶことにします)
get_hasher関数は、先ほどコードを読んだdjango.contrib.auth.models.User.set_passwordメソッド経由で呼ばれる場合は PASSWORD_HASHERSの先頭のhasherクラスを使います。

PASSWORD_HASHERSがデフォルト設定の場合、先頭のhasherクラスはdjango.contrib.auth.hashers.PBKDF2PasswordHasher です。
今回はこのクラスの定義を見てみましょう。

django/contrib/auth/hashers.py(日本語のコメントは筆者が追記したものです)

django.utils.crypto.pbkdf2 という関数が「入力されたパスワードとソルト値を結合してストレッチング」をやっているコードの本丸のようです。ここでは何をしているのでしょうか?

django/utils/crypto.py(日本語のコメントは筆者が追記したものです)

Djangoでパスワードをセキュアに保存してくれる仕組みの正体は、Pythonの標準関数 hashlib.pbkdf2_hmacであることが分かりました。
公式ドキュメントではhashlib.pbkdf2_hmacについて以下のように書かれています。

この関数は PKCS#5 のパスワードに基づいた鍵導出関数 2 を提供しています。疑似乱数関数として HMAC を使用しています。https://docs.python.org/ja/3/library/hashlib.html#hashlib.pbkdf2_hmac

「鍵導出関数 2」は英語名で「Password-Based Key Derivation Function 2(PBKDF2)」、RFC 2898(Password-Based Cryptography)の中で提案されているパスワード保存に関する仕様です。
RFC2898は前述の「ウェブアプリケーションにおけるパスワード保存の原則」をRFCとして規定したものです。

ただし、これはdjango.contrib.auth.hashers.PBKDF2PasswordHasherの場合の話です。他のhasherクラスでは脆弱なアルゴリズムを使っているものもあります。基本的にはPASSWORD_HASHERSはデフォルト設定を使うようにしましょう。

なお、Django1.11 LTSでは hashlib.pbkdf2_hmacがないバージョンのPythonもサポートしているため、 hashlib.pbkdf2_hmacを代替するコードを使ったdjango.utils.crypto.pbkdf2関数も定義されています。

Djangoがパスワード保存時にハッシュ値以外も含める理由

これで、Djangoがパスワードをセキュアに保存する仕組みが分かりました。ただ、今までに読んできたコードに1点不可解な部分がありました。もう少し付き合ってください。

デフォルトのhasherクラスdjango.contrib.auth.hashers.PBKDF2PasswordHasherの定義をもう一度見てみましょう。

django/contrib/auth/hashers.py(日本語のコメントは筆者が追記したものです)

最後の returnを見てください。文字列に「アルゴリズム名」・「ストレッチング回数」・「ソルト値」が含まれています。なぜこんなことをしているのでしょうか? 理由は2点あります。以下で詳しく解説します。

【理由1】

django.contrib.auth.hashers.PBKDF2PasswordHasher.verifyメソッドの定義を見てください。

django/contrib/auth/hashers.py(日本語のコメントは筆者が追記したものです)

保存した値 encodeから「アルゴリズム名」・「ストレッチング回数」・「ソルト値」を取り出して、ログイン時に入力されたパスワード passwordを保存時と同じロジックでハッシュ化するようになっています。
入力されたパスワードが正しければ、最後に比較している encodedencoded_2 は全く同じ値になっているはずです。

ちなみに、最後の行の constant_time_compare 関数は「タイミング攻撃」対策のために使われています。今回のテーマからは外れるので詳しい解説は割愛しますが、パスワードのような秘密の値と入力値を検証するコードでは、 ==!= は使ってはいけないことだけ覚えておいてください。

【理由2】

ユーザーが入力したパスワードの正しさを検証する関数 django.contrib.auth.hashers.check_passwordの定義を見てください。

django/contrib/auth/hashers.py(日本語のコメントは筆者が追記したものです)

「保存時に使っていたアルゴリズム名(hasher.algorithm)」と「現在使っているアルゴリズム名(preferred.algorithm)」が異なる場合、「保存時に使っていたhasher」で検証した上で「現在使っているhasher」でハッシュ値を作り直してくれています。

実際にDjangoアプリケーションを作って検証してみましょう。まず、settings.py に以下設定を加えたDjangoアプリケーションを用意します。

PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher',
)

この状態で作ったユーザーのパスワードはdjango.contrib.auth.hashers.MD5PasswordHasherを使ってハッシュ値が作られます。

>>> from django.contrib.auth.models import User
>>> user = User.objects.get(username='example')
>>> user.set_password('my_password')
>>> user.save()
>>> user.password
'md5$SMuy5hYbT0UO$275f29884e10c547874c5c170ddf53cb'

次に、PASSWORD_HASHERS の先頭にdjango.contrib.auth.hashers.PBKDF2PasswordHasherを追加してみましょう。

PASSWORD_HASHERS = (
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher',
)

この状態でパスワードの検証を行うと、 django.contrib.auth.hashers.MD5PasswordHasherで検証した上で、 django.contrib.auth.hashers.PBKDF2PasswordHasherを使った値に更新してくれます。

>>> from django.contrib.auth.models import User
>>> user = User.objects.get(username='example')
>>> # check_passwordメソッドは中でdjango.contrib.auth.hashers.check_passwordを呼んでいる
>>> user.check_password('my_password')
True
>>> user.password
'pbkdf2_sha256$36000$ASrxdtCsw3E6$u1k+CFO1y2TpbgClMQFiVITT6pUIP+H9Ss8sDrfK+iU='

このように、Djangoには後からよりセキュアなアルゴリズムが登場してもスムーズに移行できる仕組みが用意されています。

まとめ

  • ウェブアプリケーションでのパスワードは、ソルト値と結合してストレッチングしたものを保存するのが原則
  • Djangoでは、デフォルト設定ではRFC2898を実装したPython標準関数 hashlib.pbkdf2_hmacを使って前述の原則に従った実装にしている(1.11 LTSではhashlib.pbkdf2_hmacがないバージョンのPythonでも動くように、代替するコードも用意されている)
  • また、ログイン時に入力されたパスワードは、ハッシュ値と一緒に保存された「アルゴリズム名」・「ストレッチング回数」・「ソルト値」を使って保存時と同じロジックでハッシュ化してから検証する
  • さらに、後でハッシュアルゴリズムをよりセキュアなものに変更した場合にスムーズに移行できる仕組みも用意されている

参考資料

--

--

Ryuji Tsutsui
CreditEngine Tech

私がmediumに記事を書くことはおそらく当分ないので、 https://ryu22e.org/ の方を読んでください。