認証は「壊れるもの」と心得よ:A07:2021の現実と防壁の構築
現場でインシデント対応をしていると、つくづく痛感させられることがある。それは「完璧なパスワードポリシーなど存在しない」という冷徹な事実だ。OWASP Top 10の常連である「識別および認証の不備(Identification and Authentication Failures)」は、高度なハッキング技術というより、むしろ「攻撃者の自動化ツールがいかに優秀か」を証明する指標に過ぎない。
今日は、教科書的な「パスワードを複雑にしろ」というアドバイスを卒業し、攻撃者がどのような視点で君たちのシステムを突こうとしているのか、そしてそれをどう技術で封殺するか、腹を割って話そう。
—
1. 攻撃者が狙う「認証の盲点」
攻撃者が狙うのは、脆弱なコードそのものよりも「設計の隙間」だ。特に以下の2点は、多くのシステムで見過ごされている。
- Credential Stuffing(クレデンシャルスタッフィング): どこかのサイトから流出したID/PWのリストを、君たちのシステムで機械的に試す攻撃。パスワードポリシーが厳しくても、使い回されているID/PWなら一発で突破される。
- レート制限の不備: 「1分間に何回までログインを試行できるか」という制限がない、あるいはIPアドレスだけで制限している場合。攻撃者は分散ネットワーク(ボットネット)を使い、IPを数秒おきに変えてブルートフォースを行う。これでは単純なIP制限は無力だ。
—
2. 多要素認証(MFA)は「義務」である
パスワードだけでは、もはや防御として成立しない。現代の認証基盤において、MFAはオプションではなく必須だ。特に、SMS認証はSIMスワッピングのリスクがあるため、できればTOTP(Time-based One-time Password)やWebAuthn(FIDO2)への移行を強く推奨する。
実装サンプル:PythonでのTOTP検証(PyOTP使用)
バックエンドでMFAを実装する際のロジックは極めてシンプルだ。
import pyotp
# ユーザーごとに保存された秘密鍵を使用してTOTPを検証
def verify_mfa_code(user_secret, user_input_code):
totp = pyotp.TOTP(user_secret)
# 現在の時刻と照合して検証。
# ネットワーク遅延を考慮して、前後1ステップ(約30秒)の猶予を持たせるのが定石
return totp.verify(user_input_code, valid_window=1)
# 使用例:
# user_secret = "JBSWY3DPEHPK3PXP" # データベースから取得したユーザーの秘密鍵
# if verify_mfa_code(user_secret, "123456"):
# print("認証成功")
—
3. ブルートフォースを防ぐ「段階的ロックアウト」
アカウントロックアウトは諸刃の剣だ。短すぎればブルートフォースを許し、長すぎればDoS攻撃(特定のユーザーを意図的にロックアウトしてサービス利用を妨害する)の標的になる。
ここで重要なのは、「ログイン失敗回数」だけでなく「時間経過」を考慮することだ。
Nginxによるレート制限設定(設定ファイル例)
アプリケーション層に負荷をかける前に、Webサーバー(Nginx)の入り口で攻撃を減衰させる。
# nginx.conf の http ブロックに記述
# IPアドレス単位でログインエンドポイントのレート制限を行う
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
server {
location /api/login {
# 1分間に5回以上のアクセスを制限。超過分は 503 Service Unavailable を返す
limit_req zone=login_limit burst=5 nodelay;
proxy_pass http://app_server;
}
}
—
4. セキュアなパスワード管理:ハッシュ化の「常識」
未だに md5 や sha1 でハッシュ化しているエンジニアはいないと信じたいが、もしそうなら今すぐ改修してほしい。暗号理論において、ハッシュ関数は「計算コストの高さ」がセキュリティを担保する。
PHPでの安全なパスワードハッシュ実装
PHPの password_hash 関数は、将来的にハッシュアルゴリズムが強化されてもコードを変えずに対応できる優れたインターフェースだ。
// パスワードをハッシュ化(PASSWORD_DEFAULT は現在 BCrypt が使用される)
$password = "ユーザーの入力したパスワード";
$hash = password_hash($password, PASSWORD_DEFAULT);
// 認証時の照合
if (password_verify($user_input, $stored_hash)) {
// ログイン成功
// さらにここで MFA の検証フェーズへ移行させる
} else {
// ログイン失敗(IDとPWのどちらが間違っているか教えないこと!)
// 「IDまたはパスワードが違います」とだけ返すのがセオリー
}
—
最後に:エンジニアとしてのマインドセット
セキュリティ対策のゴールは、「攻撃を完全にゼロにすること」ではない。「攻撃にかかるコストを、攻撃者が諦めるレベルまで引き上げること」だ。
君たちのシステムが堅牢であればあるほど、攻撃者はもっと楽なターゲット(セキュリティ対策を怠っている他のサイト)へ移る。それがサイバー空間のリアリティだ。
1. パスワードの複雑化を強制するよりも、MFAの導入を優先せよ。
2. 失敗のログを監視し、異常なログイン試行を検知できる体制を作れ。
3. 「ログイン失敗しました」のメッセージを画一化し、攻撃者にヒントを与えるな。
この3つを守るだけで、君たちのサービスは「格好の餌食」から「攻略困難な砦」へと進化する。技術は嘘をつかない。実装の細部にこだわり、泥臭い戦いを制していこう。次回のインシデント発生時に、「ああ、あの時実装しておいてよかった」と思える日が必ず来るはずだ。
コメント