【実務・中級編】 パスワードリセット機能におけるトークン予測とホストヘッダー注入 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

現場のエンジニア諸君、お疲れ様。今日は「パスワードリセット」という、一見地味だがWebアプリの心臓部とも言える機能の闇に切り込む。

多くの開発者は「メールを送ってリンクを踏ませれば終わり」と考えているが、そこには攻撃者が好んで狙う「エントロピーの欠如」と「環境依存の脆弱性」が隠されている。今日は、君たちが今日書いているコードを、数年後にインシデント対応で泣かないための強固なものに変えていこう。

—

1. 狙われる「トークン予測」という脆弱性

パスワードリセットのトークンが、単なる md5(user_id + time()) だったり、mt_rand() のような予測可能な乱数で生成されていたら、それは「どうぞ私のアカウントを乗っ取ってください」と言っているのと同じだ。

攻撃シナリオ:ブルートフォースの効率化

攻撃者は、短期間に大量のリセットリクエストを投げ、生成されるトークンの「パターン」を特定する。特に time() をシード(種)にしている場合、リクエスト送信時刻をサーバーのログから推測すれば、トークンの候補は劇的に絞り込まれる。

対策:暗号学的に安全な乱数生成

PHPであれば random_bytes()、Pythonであれば secrets モジュールを使うのが鉄則だ。

【安全なトークン生成(PHP実装例)】

<?php
// 安全な暗号論的擬似乱数生成器を使用する
function generateSecureToken(int $length = 32): string {
    // bytesを生成し、URLセーフなBase64エンコードを行う
    return rtrim(strtr(base64_encode(random_bytes($length)), '+/', '-_'), '=');
}

// データベースへの保存時は、トークンをハッシュ化して保持する(重要!)
$rawToken = generateSecureToken();
$hashedToken = hash('sha256', $rawToken); 

// トークンをユーザーにメール送信し、hashedTokenをDBに保存する
?>

*ポイント:DBにそのまま保存してはいけない。万が一DBが流出した際、トークンが生だと即座にアカウントが乗っ取られる。ハッシュ化して保存し、検証時に照合せよ。*

—

2. Hostヘッダー注入:メールを「偽サイト」へ誘導する罠

次に狙われるのが「Hostヘッダー注入」だ。多くのWebフレームワークは、パスワードリセットメール内のリンクを生成する際、現在リクエストされている Host ヘッダーを信頼してURLを組み立てる。

攻撃手法

攻撃者が Host: evil.com というヘッダーを付けてリクエストを送ると、システムは「パスワードリセット用URL: https://evil.com/reset?token=...」というメールを被害者に送信する。被害者が何も疑わずにリンクをクリックすれば、トークンが攻撃者のサーバーに送信されることになる。

対策:ドメインのホワイトリスト化とサーバー設定

コードレベルでの信頼と、インフラレベルでの制限の両輪で守る必要がある。

【NginxによるHostヘッダーの制限】

# デフォルトのサーバーブロックで、許可されていないHostヘッダーを遮断する
server {
    listen 80 default_server;
    server_name _;
    return 444; # 接続を即座に切断する(レスポンスを返さない)
}

# 許可するドメインのみを定義
server {
    listen 80;
    server_name example.com;
    # ...アプリケーション設定
}

【PHPでのドメイン検証】

// 環境変数から正規のドメインを取得し、比較する
$allowedHost = getenv('APP_DOMAIN'); // 例: example.com
$requestHost = $_SERVER['HTTP_HOST'];

if ($requestHost !== $allowedHost) {
    // ログに記録し、不正なリクエストとして弾く
    error_log("不審なHostヘッダー: " . $requestHost);
    die("Invalid Request");
}

—

3. 実務で守るための「3つの鉄則」

最後に、現場で設計する際に必ずチェックリストに入れてほしい項目を伝えておく。

1. トークンの有効期限は短くせよ:
発行から最大でも15〜30分以内。DBに expires_at カラムを作り、検証時に必ず現在時刻と比較すること。
2. 使い捨て(One-time use)を強制せよ:
一度リセットに成功したら、そのトークンは即座に削除または無効化すること。再利用を許してはならない。
3. ユーザーに「通知」を送れ:
パスワードが変更されたことを、メールやSMSで元の登録アドレスに通知せよ。攻撃者がリセットに成功した場合でも、被害者が即座に気づいて対応できる時間を稼げる。

—

結びに

「たかがパスワードリセット」と甘く見ているエンジニアは、攻撃者にとって格好の餌食だ。セキュリティは「一度設定して終わり」ではない。コードをデプロイするたびに、今回紹介したロジックが損なわれていないか、ミドルウェアの設定が意図通りかを確認する。

それが、プロフェッショナルとしての最低限の責務だ。分からないことがあれば、いつでもコードを見せてくれ。共に堅牢なシステムを築いていこう。

コメント

タイトルとURLをコピーしました