Webアプリの開発でパスワードを使ったユーザ認証を設計・実装する機会がよくある。
後輩への説明や勉強会ネタとして、この辺の話を纏めてみようと思う。
概要
オンラインバンキングやネットショッピングのサイトでは、資金移動や物品購入が行えるため、不正アクセスが成功した場合のメリットが多く標的になりやすくなります。
攻撃者の関心事は、いかにログインするか?より短い時間でいかに多くのユーザのパスワードを特定できるか?となります。
運用や保守にかかわる内部犯によるパスワード情報の漏洩、SQLインジェクションやバックドアを使って外部からパスワード情報を不正取得する場合が考えられます。
このような場合でも、ユーザのパスワードが攻撃者に簡単に分からないようにするための工夫が必要となります。
ここでは、単純な例から、どのようにパスワード情報を管理すべきかを説明します。
パスワードの管理方法
ユーザの認証を行うためのユーザIDとパスワード情報がデータベースやファイル等のデータストアに格納されており、このデータストアや付随する情報が外部に漏れた場合を想定して説明します。
パスワードを平文で管理
パスワードそのものをデータストアに保管する方式です。
データストアから取得したパスワードと、ユーザが入力したパスワードが一致している場合、認証成功と見なします。
ユーザID | パスワード |
---|---|
u0000001 | Yds(0sfa!3s |
u0000002 | 123456 |
u0000003 | password |
u0000004 | sunshine |
u0000005 | 123456 |
問題点:
- 各ユーザのパスワードを、運用や保守の担当者が容易に把握することができる。悪意を持った担当者が、桁数が少なかったり特徴のあるユーザIDやパスワードであれば覚え、別の場所から不正アクセスすることも考えられる。
- SQL/コマンドインジェクション等で、データストアの内容が外部に漏洩した場合、攻撃者は全ユーザのパスワードを何の苦労もなく入手することができる。
- 一部または全てのユーザのパスワードを比較的簡単に入手できリスクが非常に高いことや、後述のハッシュ化を容易に実装できるにも関わらずこの方式を採用することは、SEとしての常識を疑われるので決して採用してはならない。
パスワードを暗号化して管理
AES等の一般的に安全な暗号化アルゴリズムを使ってパスワードを暗号化し、データストアに保管する方式です。
復号時に使用するキーは、アプリの設定ファイル等で保管する想定です。
ユーザ認証時、暗号化パスワードをアプリで保管しているキーを使って、パスワードを復号します。このパスワードと、ユーザが入力したパスワードが一致していれば認証成功と見なします。
ユーザID | 暗号化パスワード |
---|---|
u0000001 | 9d79a…f8c6a |
u0000002 | e4b11…8d1b5 |
u0000003 | 53ddf…3fae4 |
u0000004 | 5aef1…8d6ab |
u0000005 | c72ab…ff9a2 |
問題点:
- 暗号化のキーが漏洩した場合、すべてのユーザのパスワードが分かってしまう、という大きな問題がある。(c.f. 後述のハッシュ関数を使う場合、このようなハッシュ値からパスワードを逆算できないため、攻撃者は基本的には総当たりでハッシュ値を推測するしかない。)
- 運用や保守の担当者であれば、比較的容易に復号できる。
- 例えば、顧客からの問い合わせに対してパスワードを回答するような運用要件があるのであれば、この方式でもやむを得ない場合があるが、セキュリティリスクや運用コストを踏まえ、この方式は可能な限り回避すべきである。一般的には、後述のハッシュ関数を使う方が、運用や保守の担当者がパスワードを知ることができないので、リスク低減や運用コストの低減が期待できる。
- この方式を採用するなら、パスワード毎に鍵を変える、キーをハードウェアで管理(HSM等を使ってキーの持ち出しを困難にする。)等、攻撃に対する耐性を高める工夫が必要である。
パスワードをハッシュ化して管理
SHA-2等の一般的に安全なハッシュアルゴリズムを使って、パスワードからハッシュ値(パスワードハッシュ)を求め、その値をデータストアに保管する方式です。
データストアから取得したパスワードハッシュと、ユーザが入力したパスワードから生成したハッシュ値が一致していれば認証成功と見なします。
運用保守担当者が見ても、データストアが外部に漏洩してもパスワードは直接は分かりません。攻撃者は、任意のパスワードに対するハッシュ値を計算し、そのうちのどれがデータストアのパスワードハッシュと一致するかを総当たりで調べる必要があります。
※ハッシュ値はバイナリデータですが扱いやすいよう16進数の文字列として表現しています。また、文字数が多いので”…”で省略しています。以降の説明も同様です。
ユーザID | パスワードハッシュ |
---|---|
u0000001 | 81675…4fc9a |
u0000002 | 8d969…c6c92 |
u0000003 | 5e884…542d8 |
u0000004 | a941a…6c223 |
u0000005 | 8d969…c6c92 |
問題点:
- よくあるパスワードに対するハッシュ値は簡単に計算できる。例えば、「最悪のパスワード2018年版」の1位のパスワード”123456″のハッシュ値は”8d969…c6c92″、2位のパスワード”password”のハッシュ値は”5e884…542d8″となる。ユーザIDがu0000002, u0000005のハッシュ値は”8d969…c6c92″になっているので、これらのユーザIDのパスワードは”123456″と推測できる。
- 単純にパスワードからハッシュ値を生成すると、同一パスワードの場合、同一のハッシュ値になってしまう。そのため、上記の例だと、ハッシュ値を見ただけで、ユーザIDがu0000002, u0000005が同じパスワードだと分かってしまう。(結果として、一人分のパスワードが分かると、他のユーザのパスワードも分かってしまう場合があり、攻撃者のパスワード解析を容易にしてしまう。)
パスワード+ソルトをハッシュ化して管理
SHA-2等のハッシュアルゴリズムで、パスワード+ランダムな文字列(ソルト)からハッシュ値を求め、その値をデータストアに保管する方式です。
ユーザが入力したパスワード+データストアから取得したソルトからハッシュ値を求め、その値がデータストアのパスワードハッシュと一致していれば認証成功と見なします。
単純なパスワードのハッシュ値からの推測が困難になります。また、同一のパスワードをもつu0000002, u0000005のハッシュ値が別々の値となるため、同一のパスワードを持っているか否かの推測が困難になります。
ユーザID | パスワードハッシュ | ソルト |
---|---|---|
u0000001 | 296cb…818b3 | 8a%a1dsf |
u0000002 | a0cab…f5aa6 | )kaus7Aa |
u0000003 | 80dc9…78cc1 | 6us$8sfQ |
u0000004 | 63c42…73e6c | d1df&s)s |
u0000005 | 94b1b…3df79 | 98sdS=f2 |
問題点:
- よく見かける実装です…一見すると推測が難しくなったように見えるが、実際の攻撃者は潤沢なコンピュータリソースやハードウェアサポート等で使って、より高速に大量のハッシュ値を生成し、パスワードを特定することができるので安全ではない。
- パスワード毎に異なるソルトではなく全体で一つのソルトを使う設計だと、任意のユーザのパスワードが分かった時点で後続のユーザの
- あらかじめ膨大な数のパスワードとハッシュ値の組み合わせを持つデータベース・テーブルを用意し、ハッシュ値からパスワードを検索するためのクエリで一発特定する「レインボーテーブル攻撃」に弱い。(膨大な数の組み合わせのデータをデータベース・テーブルに保持する必要があるため大量のメモリやディスクスペースが必要とされ、誰でも簡単にできるわけではないが。)
- SHA-2等のポピュラーなハッシュアルゴリズムは、高速にハッシュ値を作成するよう設計されている。また、ポピュラーであるがゆえに、高速計算が可能な特定用途向けのハードウェア(ASIC/GPU/FPGA)でサポートされているため、短期間で大量のハッシュ値を作成できるので、総当たりの時間短縮が可能となってしまう。
最適な方式
次を考慮した、パスワード+ソルトをハッシュ化して管理する方法がポピュラーな方式です。
この辺の詳細は別途説明予定です。
- ハードウェアを使った攻撃に耐性を持つハッシュアルゴリズムが必要となります。
- 攻撃者の負荷を高めるために、ハッシュ関数の実行を複数回繰り返してハッシュ値を算出するストレッチングがあります。ストレッチングの回数(ラウンド数)を十分大きくすることも考慮する必要があります。
- ソルトの生成では、十分な桁数かつ予測が困難な乱数生成アルゴリズムが必要となります。
番外
上記では、パスワード情報が外部に漏洩した前提の話でしたが、システムのログイン画面に対して総当たりでログインを試行する方法もあります。
この場合も同様に、いかに攻撃者に手間や時間をかけさせるか?というのが対策となります。
認証失敗時にスリープ等で遅延させる対策が考えられますが、攻撃者はより試行回数を増やすために同時並行的にログインを試行する可能があるので、十分な対策にはなりません。
一般的なアカウントロックのように「ある期間内で何回のログイン試行を許可するか」を制御する仕組みが必要となります。
より安全にするのであれば、多要素認証の導入を検討してください。