1. 識別と認証と認可 #identify-vs-auth
- 識別(Identification) — 「私は store_admin です」と名乗る。会員ID やメールアドレスがこれに当たる
- 認証(Authentication) — 「本当に store_admin か」を検証する。パスワード、トークン、生体情報などで証明
- 認可(Authorization) — 「store_admin に何が許されているか」を判定。読み取り、書き込み、削除、管理画面アクセス等
本カテゴリの Lab は主に識別と認証の段階の脆弱性を扱います。認可の脆弱性は Access Control Labs で別途扱います。
2. 3要素認証 #three-factors
認証で使われる要素は大きく3カテゴリに分類されます:
- 知識要素(Something you know) — パスワード、PIN、秘密の質問
- 所持要素(Something you have) — スマートフォン、ハードウェアトークン、SMSの認証コード受信端末
- 生体要素(Something you are) — 指紋、顔、虹彩、声紋
複数の異なるカテゴリの要素を組み合わせるのが多要素認証(MFA)。同じカテゴリ内で2つ(パスワード + 秘密の質問)を組み合わせるのは多段階認証であって多要素認証ではないことに注意。
SMS は所持要素として広く使われていますが、SIM スワップ攻撃(攻撃者が通信会社に被害者を装って SIM 再発行を申請)で奪取される事例があります。重要なアカウントには TOTP アプリ(Google Authenticator 等)やハードウェアトークン(YubiKey 等)の方が推奨されます。
3. パスワードの保管 #password-storage
パスワードを DB に保管する際は、遅いハッシュ関数を使った一方向ハッシュにすべきです。代表的な選択肢:
- Argon2 — 2015年の Password Hashing Competition 優勝。現在の第一推奨
- bcrypt — 1999年から使われ続ける定番。実装が枯れている
- scrypt — メモリ消費を強制してハードウェア攻撃を困難にする
- PBKDF2 — 計算回数を増やせるが、メモリ強化はない。FIPS 認証が必要な場面で
使ってはいけないもの:
- MD5、SHA-1、SHA-256 単発(高速すぎてブルートフォースに弱い)
- 独自実装(レインボーテーブル耐性、タイミング攻撃耐性等の検証が困難)
- 暗号化(復号可能だと、鍵が漏れた瞬間に全パスワード流出)
// PHP — bcrypt
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// 検証時
if (password_verify($input, $hash)) {
// 認証成功
}
ソルト(salt)は最近のライブラリ(password_hash 等)が自動的に付与・管理するので、明示的に扱う必要はほぼありません。
4. セッション管理 #session-management
HTTP は元々ステートレスなので、ログイン後の認証状態を保つ仕組みとしてセッションが使われます。
- ログイン成功時、サーバが暗号論的にランダムなセッション ID を生成
- サーバ側でセッション ID とユーザ情報を紐付けて保存(DB、Redis 等)
- レスポンスで
Set-Cookie: session=xyz; HttpOnly; Secure; SameSite=Laxを返す - 以降、ブラウザは自動的にこの Cookie を添付。サーバはセッション ID で「誰のリクエストか」を判定
セッション Cookie の重要属性:
- HttpOnly — JavaScript からアクセス不可。XSS で Cookie を盗まれるのを防ぐ
- Secure — HTTPS でのみ送信。中間者攻撃を防ぐ
- SameSite — cross-site でのリクエストでの送信を制限。CSRF 対策(詳しくは CSRF KB 01 §5)
セッション ID は十分なエントロピーを持つ必要があります(128bit 以上推奨)。連番や短い文字列だと総当たりで他ユーザのセッションを奪える。
5. ログインエラーメッセージの設計 #error-message
ログイン失敗時のエラーメッセージで、よくある2つの設計を比較しましょう:
// 親切だが脆弱
if (user_not_found) {
return "そのメールアドレスは登録されていません";
}
if (wrong_password) {
return "パスワードが間違っています";
}
// 正しい設計
if (user_not_found || wrong_password) {
return "メールアドレスまたはパスワードが正しくありません";
}
なぜ統一すべきか? 攻撃者が次のような試行をすれば:
alice@example.com+ 適当なパスワード → 「パスワードが間違っています」bob@example.com+ 適当なパスワード → 「そのメールアドレスは登録されていません」
これだけで「alice@example.com は登録ユーザだが bob@example.com は登録されていない」という情報が漏れます。これをユーザ列挙(Username Enumeration)と呼びます。
列挙されたメールアドレス一覧は、フィッシング、ブルートフォース、クレデンシャル・スタッフィング攻撃の入口になります。「このメールアドレスは うつろい EC の会員」という情報自体が、社会的に晒したくないプライバシーである場合もあります(成人向けサイト、医療系サービス等で特に深刻)。