なぜPHPアプリにセキュリティホールが多いのか?

第40回MOPS:安全性の高いパスワードハッシュ作成ツール - phpass

第32回 PHPセキュリティ月間(Month of PHP Sercurity)「PHPセキュリティ月間」MOPS - Month of PHP Securityについて簡単に紹介しました。

今回はパスワードを安全に保存するツールの紹介です。今まで数回に渡ってMOPSの成果の一部を紹介してきましたが、今回で最後です。

MOPS Submission 10: How to manage a PHP application’s users and passwords
http://www.php-security.org/2010/05/26/mops-submission-10-how-to-manage-a-php-applications-users-and-passwords/index.html

この論文は1位を取得した論文です。Drupal 7でこのライブラリが利用されていたので筆者はphassの存在を知っていました。phpassは本格的なパスワードハッシュツールで、すべての開発者お薦めできます。ライセンスはパブリックドメインです。

Alexander Peslyak氏が投稿した論文はライセンス条件が異なるので、今までのMOPSの紹介記事と異なり、論文を参考文献として筆者が書いた記事になります。

パスワード保存の問題

パスワードは言うまでもなく非常に高いセキュリティが要求される機密情報です。これはユーザ数が少ないサイトであっても同じです。多くのユーザは、ユーザが利用する複数またはすべてのサイトで共通のパスワードを利用しています。ユーザIDが同じであることも非常に多いです。

このため、ユーザ数が少ないサイトであってもパスワードが漏洩してしまうと、パスワードが漏洩したユーザが利用するほかのサイトで盗まれたパスワードが利用され被害にあう可能性があります。パスワード漏洩は情報を盗まれたサイトだけの問題ではありません。

保存されたパスワードの強度は次のようになります。saltとstretchingについては後述します。

平文<平文のハッシュ<平文+saltのハッシュ<平文+stretching+saltのハッシュ

Peslyak氏によると「SSLを使っていないのに何故パスワードを暗号化する必要があるのか?」⁠SSLを使っているのにパスワードを暗号化する必要があるのか?」と聞かれることがあるとしています。暗号化したパスワードを保存することと、ネットワークトランスポート層で暗号化することは別のリスクに対応する対策であるため無関係であり、パスワードのハッシュ化はパスワードが盗まれたり、漏れたりした場合の対策である、としています。すべてのセキュリティ専門家は同じ考えだと思います。

パスワードは元のパスワードが解析できないような形で保存されるべきです。phpassはこれを実現するライブラリです。

平文

パスワードを平文で保存する問題点は指摘するまでもありません。SQLインジェクションなどでユーザデータが盗まれた場合、攻撃者はパスワードを解析する必要もなく利用できます。平文の場合、あまりにも簡単にパスワードが分かってしまうためユーザ情報にアクセスできる組織内部のスタッフにより悪用される可能性も高くなります。

平文のハッシュ

平文ままパスワードをデータベースに保存するのは危険なことは明らかなので、パスワードのハッシュ値を保存するアプリケーションが現在の主流ではないでしょうか? パスワードのハッシュ値を保存する方法は平文に比べて何倍もよいと言えるのですが、十分なセキュリティは提供しません。

ハッシュ化したパスワードは総当り攻撃で解析したり、辞書を構築して攻撃できます。レインボーテーブルと呼ばれる特殊な辞書を構築すれば辞書サイズを大幅に小さくできます。8文字程度のパスワードをmd5ハッシュ化した状態は平文とあまり変わりがありません。

sha1/sha256などより強度が強いハッシュ関数を利用することも可能ですが、十分に安全だとは言えません。

平文+saltのハッシュ

saltとはパスワードハッシュを辞書攻撃から守るためのランダム文字列です。固定saltは簡単に利用でき、saltを別に保存しておけば、一緒に盗めなかった場合はパスワードとsalt、両方を推測する必要があります。また、あらかじめ辞書を用意しておくことができないので、ハッシュするだけよりも高い安全性を期待できます。ユーザ別saltには色々な方法が考えられますが、ランダムな文字列をsaltとして生成してパスワードハッシュ文字列と一緒に保存する方法が利用されることが多いと思います。saltを利用することにより安全性はかなり向上しますが、ハッシュ関数は元々高速に動作するように実装されているのでsaltとパスワード文字列を使うだけでは総当たり攻撃でパスワードが解析される危険性は十分にあります。

平文+stretching+saltのハッシュ ─ パスワードの強化

ハッシュ関数は高速に動作するよう設計されています。この特徴はパスワードハッシュが盗まれた場合に解析されてしまうリスクを増加させてしまいます。パスワードを総当たり攻撃で解析しづらくする対策はkey/password stretchingまたは key/password strengheningと呼ばれています。よりCPUパワーが必要となるハッシュを利用したり、ハッシュを何度も適用してより多くのCPU時間を消費させる方法があります。saltのみを利用してパスワードを保存するよりかなり高い安全性を期待できます。

ハッシュ関数の選択

Peslyak氏はどのハッシュ関数を使用するかは、saltとstretchingを実装した、より高いレベルのパスワードハッシングの問題であるので、どのハッシュ関数を使うか問題ではないとしています。phpassはBlowfish, DES, MD5の三種類のハッシュ関数が利用できるようになっています。パスワードをハッシュ化するハッシュ関数の適合性はBlowfish, DES, MD5の順ですが、十分にstrechingを行えばMD5を利用してもBlowfishと同等の強度を達成できます。

phpass ─ パスワードハッシングフレームワーク

phpassの解説の前に、PHPがサポートするハッシュ関数について説明が必要です。PHPにはmd5関数とcrypt関数があります。crypt関数はCRYPT_BLOWFISHとCRYPT_EXT_DESをサポートしています。しかし、PHP 5.3までのcrypt関数はシステムのcrypt関数を呼び出していたため、システムによってサポートするハッシュ関数が異なっていました。md5関数はPHPによって実装されています。このため、md5が最もポータブルなハッシュ関数でphpassはPHP 3でも動作するそうです。

phpassは特に指定しない限り、CRYPT_BLOWFISH、CRYPT_EXT_DES、md5の順番に利用可能なハッシュ関数を検索します。プラットフォームが異なるマシンにアプリケーションを移動する可能性があるシステムの場合は注意が必要です。

phpassはランダムにsalt文字列を生成し、利用したハッシュ関数、Stretchingの回数(ハッシュの適用回数)⁠ハッシュでエンコードした文字列⁠にして返します。

phpassの準備

phpassのホームページ

http://www.openwall.com/phpass/

からtar.gzまたはzip形式のアーカイブをダウンロードし解凍します。

$ ls -F
PasswordHash.php        demo2/                  demo3-4.diff            demo5/                  phpass-article.html
demo1/                  demo2-3.diff            demo4/                  demo5-6.diff            test.php
demo1-2.diff            demo3/                  demo4-5.diff            demo6/

phpass-articleというディレクトリに中にあるPasswordHash.phpファイルがphpassライブラリです。PHP 4形式のオブジェクトとして実装されていますが、PHP 5でも問題なく動作します。

phpassの使い方

phpassの使い方は非常に簡単です。test.phpを見れば使い方は十分わかるはずです。

test.php
<?php
require 'PasswordHash.php';

header('Content-type: text/plain');

$ok = 0;

# Try to use stronger but system-specific hashes, with a possible fallback to
# the weaker portable hashes.
$t_hasher = new PasswordHash(12, FALSE);

$correct = 'test12345';
$hash = $t_hasher->HashPassword($correct);

print 'Hash: ' . $hash . "\n";

$check = $t_hasher->CheckPassword($correct, $hash);
if ($check) $ok++;
print "Check correct: '" . $check . "' (should be '1')\n";

$wrong = 'test12346';
$check = $t_hasher->CheckPassword($wrong, $hash);
if (!$check) $ok++;
print "Check wrong: '" . $check . "' (should be '0' or '')\n";

(省略)

PasswordHashクラスのコンストラクタは以下のように定義されています。

function PasswordHash($iteration_count_log2, $portable_hashes)
パスワードハッシュオブジェクトを初期化
$iteration_count_log2:整数 ─ Stretching値
$portable_hashes:論理値

$iteration_count_log2は4以上、31までの値を設定します。この値がStretchingの値になります。$portable_hashesがtrueの場合、ハッシュ関数にmd5が利用されます。falseの場合はCRYPT_BLOWFISH, CRYPE_EXT_DES, md5の順に試されます。

その他のメソッド

function HashPassword($password)
平文の$passwordをハッシュします。
$password:文字列 ─ 平文のパスワード
戻り値:ハッシュ化されたパスワード、エラーの場合 '*' が戻る
function CheckPassword($password, $stored_hash)
平文の$passwordと既に保存済みの$stored_hashとを比較します。
$password:文字列 ─ 平文のパスワード
$stored_hash:文字列 ─ DB等に保存されているハッシュ化されたパスワード
戻り値:論理値 - 1の場合同じ、0の場合異なる

test.phpのテスト用パスワードは埋め込まれているので同じパスワードですが、test.phpを繰り返し実行しても毎回違うパスワードハッシュが生成されます。

test.phpの実行例
$ php test.php
Hash: $2a$08$/c4rG/rL7WRbtqCDqBGrp.B1tGJ97umGhoi2i9bVenw.bndbuZrSi
Check correct: '1' (should be '1')
Check wrong: '' (should be '0' or '')
Hash: $P$BovV6HA5WqX5lICRGnLMd0E04GU8E..
Check correct: '1' (should be '1')
Check wrong: '' (should be '0' or '')
Hash: $P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0
Check correct: '1' (should be '1')
Check wrong: '' (should be '0' or '')
All tests have PASSED

まとめ

Peslyak氏の論文には安全にデータベースへパスワードを保存するためにどのようにSQLインジェクションを防ぐか解説されていますが、SQLインジェクションについては本連載で既に取り扱っているので省略しました。

phpassは小さいライブラリですが強固なパスワードハッシュを簡単に生成してくれます。既存のプロジェクトに採用するためには、移行期間は新旧両方のパスワードチェックを行うなど、多少の工夫で導入可能です。新規プロジェクトでは積極的に採用すべきよいライブラリです。

phpassは優れたパスワードハッシュライブラリですが、ユーザが設定するパスワードが⁠123456⁠のように脆弱なパスワードでは効果が半減します。ユーザが推測しやすいパスワードを利用しないようにしなければなりません。phpassにはこの目的を果たすpwqcheckコマンド用のラッパー、pwqcheck.phpがdemoディレクトリに保存されています。

これまで数回に渡ってMOPS関連の記事を連載しましたが今回で最後です。3年前のMOPB(Month of PHP Bugs)と同様に、今回のMOPS(Month of PHP Security)もPHPコミュニティにとって非常に有用なプロジェクトとなりました。また数年後にMOPSが開催されるのを楽しみ待つことにします。

おすすめ記事

記事・ニュース一覧