問題:まちがった自動ログイン処理の解答です。このブログエントリは最近作られたアプリケーションでは「問題」にしたような実装は行われていないはず、と期待していたのですがあっさり期待を破られたのでブログに書きました。このブログの方が詳しく書いていますけが「Webアプリセキュリティ対策入門」にも正しい自動ログイン処理を書いています。
参考:自動ログイン以外に2要素認証も重要です。「今すぐできる、Webサイトへの2要素認証導入」こちらもどうぞ。HMACを利用した安全なAPIキーの送受信も参考にどうぞ。
間違った自動ログイン処理の問題点
まず間違った自動ログイン処理を実装しているコードの基本的な問題点を一つ一つ順番にリストアップします。
- クッキーにランダム文字列以外の値を設定している
- クッキーにユーザ名が保存されている
- クッキーにパスワードが保存されている
- ユーザ名・パスワードが保存されているクッキーは暗号化されていないBASE64エンコードされたキストである
- 自動ログイン用にが保存されたクッキー(ユーザ名・パスワード)がアプリケーション全体で有効になっている
クッキーにランダム文字列以外の値を設定している
セキュリティのベストプラクティスとしてはクッキーにユーザに関連するいかなる情報(暗号化された情報を含む)も保存すべきではありません。クッキーにユーザに関連した情報(ユーザID、パスワード、メール、氏名、etc)を保存しなければならないシステムである場合、設計を見直し、ユーザ情報をクッキーに保存しなくても良い設計にしなければなりません。クッキーに保存しても良い値は、表示設定などのユーザ情報と関係の無い情報、セッションIDなどの予測不可能なランダムな値のみです。
ベストプラクティスには「最小権限の原則」「攻撃可能領域の最小化の原則」があります。クッキーに保存する必要が全く無いユーザ名とパスワードをクッキーに保存することはこれらの原則にも適合していません。
クッキーにユーザ名、パスワードが保存されている
問題のコードではユーザ名、パスワードを含んだクッキーがBASE64でエンコードされていますがBASE64は暗号化とは言えません。テキストで送信しているのと変わりありません。仮に暗号化していてもクッキーにユーザ名とパスワードを保存するのは非常に危険です。そもそも既に記述したようにクッキーに重要な情報を保存する設計となっている場合、設計自体に問題があると言えます。
問題のコードではユーザ名、パスワードを含んだクッキーがアプリケーション全体で有効になっています。サイト全体でない分ましですが非常に重要なクッキーを保存しているので不十分極まりない、と言われても仕方ないです。
自動ログインの実装にユーザ名とパスワード(クレデンシャル)をクッキーに保存する方法はセキュリティ上大きな問題があります。コードの作者は「HTTPのBASIC認証でも同じような仕組みになっている」と考えられているかも知れません。しかし、そもそもBASIC認証自体が安全ではなく通常利用は避けるべきです。さらに、クレデンシャルをクッキーに保存するのはBASIC認証にはないリスクを増加させます。
- クッキーはPCにファイルとして保存される
- ファイルとして保存されるため有効期限が(通常は)非常に長い(問題のアプリは30日)
- クロスサイトスクリプティング(XSS、JavaScript挿入)に脆弱な場合簡単に盗まれる
自動ログインの性質上、自動ログインに利用するクッキーは必ずファイルに保存されるクッキーとして送信しなければなりません。BASIC認証の場合、クレデンシャルはファイルに保存されません。セッションクッキーと同様にブラウザを終了すると消えてしまいます。
自動ログイン用にが保存されたクッキーがアプリケーション全体で有効
自動ログイン通常は非常に長い期間(1週間から1ヶ月)保存されます。長い期間保存されるクッキーは悪用される可能性が高くなります。しかも、問題のアプリのクッキーには変化が無くBASE64エンコードされた内容を知らなくても再生攻撃で簡単に攻撃されます。アプリケーション全体で常に送信されるクッキーであることもリスクを増加させています。
クッキーとして保存された情報はXSSに対してアプリケーションが脆弱であると簡単に盗むことができます。ブログアプリケーションはXSSに脆弱になりがちなので非常に危険といえます。BASIC認証のクレデンシャルを盗むには盗聴など、XSSより高いハードルを乗り越える必要があります。(ただし、最近は盗聴のリスクが増加しているので盗聴は行われている前提でアプリケーションを作るべきです。キーワード:Evil Twins, AP Phishing など)また、共有PCを使っている場合はクッキーファイルを参照して簡単にドメイン名、ユーザ名、パスワード、アクセスに必要な情報全てが簡単に盗み出せます。
自動ログインの実装方法に次の3つの方法がよく見かけられます。
- ユーザ名とパスワードを保存する (セキュリティ上最悪の実装方法)
- セッションIDに長い有効期限を設定する (セキュリティ上問題かつ多くのサーバリソースが必要となる)
- 自動ログイン専用の鍵となるクッキーを設定する (最も好ましい方法)
安全な自動ログイン処理
最後の最も好ましい実装は以下の様な手順で自動認証を行います。(Pesudoコード インデントが無くなって表示されていますが切れて見えないよりましなのでこのままにします。)
手動ログイン時 – ユーザ名とパスワードを入力してログイン
if (authenticate($username, $password) && !empty($_POST['auto_login'])) {
setup_auto_login();
}
if (empty($_POST['auto_login'])) {
// 自動ログイン用のクッキーを削除
delete_cookie('auto_login');
// 古い自動ログインkeyを削除
$sql = "delete from auto_login where key = '". sql_escape($_COOKIE['auto_login']). "';";
sql_qeury($sql);
}
function setup_auto_login() {
// 認証が完了し、自動ログインを設定
$auto_login_key = hash('sha256', random_bytes(32)); // keyを生成
$sql = "insert into auto_login (user_id, key, expire) values (" .
"'". sql_escape($_SESSION['user_id']). "', '". esq_escape($auto_login_key)."', '". date('Y-m-d H:i:s', time()+3600*24*7) ."';";
sql_query($sql);
send_cookie('auto_login', $key, 3600*24*7); //有効期限7日の自動ログインクッキーを送信
}
自動ログイン時
if (!is_authenticated() && !empty($_COOKIE['auto_login'])) {
$sql = "select * from auto_login where key = '". sql_escape($_COOKIE['auto_login']) ."' and expire > "'. date('Y:m:d H:i:s'). "';";
$records = sql_select_and_fetch_all($sql);
if ($records[0]['key'] === $_COOKIE['auto_login']) {
// 認証OK。認証処理を行う
......
// 古い自動ログインkeyを削除
$sql = "delete from auto_login where key = '". sql_escape($_COOKIE['auto_login']). "';";
sql_qeury($sql);
// 新しい自動ログインkeyを設定
setup_auto_login();
}
}
if (!is_authenticated()) {
// 通常のログイン処理
}
ログアウト時
if (!empty($_COOKIE['auto_login'])) {
// 自動ログイン用のクッキーを削除
delete_cookie('auto_login');
// 自動ログインkeyを削除
$sql = "delete from auto_login where key = '". sql_escape($_COOKIE['auto_login']). "';";
sql_qeury($sql);
}
//通常のログアウト処理
まとめ
- 自動ログイン機能は基本的にセキュリティ上のリスクを増加させるので安全性が重要なサービスでは実装しない。
- 自動ログインの実装にはセッション管理と同様にランダムなクッキーの値を使用する。
- 自動ログイン用のクッキーはログインの度に新しい値に更新する。(古い鍵の削除も忘れない)
- ログイン時に自動ログインオプションが無効かつ自動ログイン用のクッキーが設定されている場合は自動ログインテーブルの該当レコードと自動ログインクッキーも削除する。
- 自動ログイン用のクッキーは自動ログイン処理を行う場合にのみ必要なので自動ログイン専用のディレクトリを設定し、そのディレクトリのURLがリクエストされた場合にのみ送信する。(XSSリスクの低減)
- ログアウトした場合、自動ログインクッキーが設定されている場合、自動ログインテーブルの該当レコードと自動ログインクッキーも削除する。
実際の運用では古い自動ログイン用のレコードが溜まるので定期的に不要なレコードを削除する。
補足
自動ログイン用のクッキーを送信しているにも関わらず、自動ログインが成功しない場合は第三者に自動ログイン用のクッキーを利用された可能性がある。(JavaScriptでPCの時間を送ってもらう必要あり。時間設定の誤差と時差に注意。)ユーザに注意を喚起するのも良い。
/dev/urandomなどエントロピーファイルが利用できる場合は自動ログインキーの作成に利用する方がよい。(PHPの乱数関数でrandより良いとされているmt_randの実装も今ひとつと評価している人も多い)
ユーザ名、パスワード等、ユーザ情報をクッキーに保存するのは非常に大きな問題です。多くのユーザは複数のWebサイトのアカウントに同じユーザ名とパスワードを使用しています。自分のサイトのみでなく他のサイトにも迷惑をかける可能性が非常に大きいです。ハッシュ化、暗号化の有無を問わずクッキーにいかなるパスワード情報でも保存するのは間違いです。
ところで、データベースにパスワードを保存する場合は必ず”salt”付きでハッシュ関数を使った値をパスワード情報として保存するべきです。
例:$password_will_be_stored_db = sha1($_POST[‘password’], ‘some-random-string’);
追記:現在のPHPではpassword_hash()を利用すべきです。
こうする事によって万が一、SQLインジェクション、DBバックアップの流出等によってパスワード情報が漏洩した場合でも”salt”が分からない場合はパスワードを推測できません。(slatが無いと弱いパスワードは辞書攻撃で簡単に判別できます)自動ログインコードに問題があったブログでもパスワードはMD5でハッシュ化されています。しかし、saltは利用されていないのでこの点は改善の余地があります。
saltとパスワードを一緒にハッシュ化すれば辞書攻撃が難しくなるので問題のブログアプリも「パスワードをsaltと一緒にハッシュ化したパスワードをクッキーに保存すれば良いのでは?」と考えられるかも知れません。確かにセキュリティ上のリスクは比較にならないほど改善しますが、この方法だと「攻撃可能領域の最小化の原則」に適合しません。(正しい設計では元々ユーザ情報をクッキーとして保存する必要はない。salt付きでハッシュ化したパスードでも解読される可能性はある。これらの理由から「攻撃可能な領域」を増やす結果となる)さらに「パスワードをsaltと一緒にハッシュ化した値」はパスワードかslatを変更しない限り変化しません。したがってこの方法は再生攻撃に非常に脆弱になります。
コメントは受け付けていません。