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

パスワードリセットは「信頼の最後の砦」:その実装、本当に大丈夫か?

システム開発の現場で、ログイン機能やパスワードリセット機能に全精力を注ぐエンジニアは案外少ない。大抵はフレームワークの標準機能に頼り切り、裏側で何が起きているかまで追うことはないだろう。だが、攻撃者はそこを狙っている。パスワードリセットは、言わば「認証という門」を強引にこじ開けるためのマスターキーだ。

今日は、多くのWebアプリで放置されている「リセットトークンの脆弱性」と「Hostヘッダー注入」という、古典的だが極めて強力な攻撃手法について、実戦的な防衛論を語る。

—

1. 狙われる「予測可能なトークン」

多くのエンジニアは「ランダムな文字列を生成しているから大丈夫」と信じている。しかし、その「ランダム」の正体は何だ?

もし君がPHPで rand() や mt_rand() を使っていたら、それは「乱数」ではなく「計算可能な数列」だ。攻撃者は、短時間で発行された複数のリセットURLからシード値を逆算し、次に発行されるトークンを予測する。

攻撃のロジック(PoCの概念)

1. 攻撃者はターゲットのアカウントでパスワードリセットを連打し、発行されたトークンをいくつか収集する。
2. トークン生成アルゴリズム(mt_rand() 等)の内部状態を、収集したデータから php_mt_seed のようなツールを使って特定する。
3. 次のトークンを予測し、被害者のメールが届く前にそのトークンでリセット画面にアクセスする。

対策:
絶対に「暗号学的に安全な疑似乱数生成器(CSPRNG)」を使え。PHPなら random_bytes()、Pythonなら secrets モジュールだ。

—

2. Hostヘッダー注入:メールをハイジャックする

パスワードリセット機能で、メール本文内のURLを生成する際に $_SERVER['HTTP_HOST'](または同等のヘッダー)を信頼していないだろうか?

攻撃者は、リクエストヘッダーの Host を細工して送信する。

POST /forgot-password HTTP/1.1
Host: attacker-site.com
...

アプリがこれを受け取り、「パスワードリセット用URL: https://attacker-site.com/reset?token=...」というメールを送信した場合、被害者は攻撃者のサイトに誘導され、トークンを盗まれることになる。

—

3. 実践:セキュアな実装コード

ここからは、コピペして使える堅牢な実装サンプルだ。設計の根幹は「固定されたホスト名」と「暗号学的に安全なトークン」にある。

Python (Flask) での安全なトークン生成

import secrets
import hashlib
import time

def generate_reset_token(user_id):
    # 1. 予測不能な256ビットの乱数を生成
    raw_token = secrets.token_urlsafe(32)
    
    # 2. トークンをハッシュ化して保存(DBにはハッシュを保存する)
    token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
    
    # 3. 有効期限(例: 30分)とセットでDBへ保存
    save_to_db(user_id, token_hash, expires_at=time.time() + 1800)
    
    return raw_token  # ユーザーにはハッシュ前のトークンを送る

Nginx 設定:Hostヘッダーのクリーンアップ

アプリ側での対策に加え、インフラ側でも Host ヘッダーを固定化し、不正なリクエストを弾くのが鉄則だ。

# /etc/nginx/conf.d/app.conf
server {
    listen 80;
    server_name my-secure-app.com;

    # 許可されていないHostヘッダーでのアクセスを拒否
    if ($host != "my-secure-app.com") {
        return 403;
    }

    location / {
        # アプリケーションサーバーへ転送する際もHostを固定する
        proxy_set_header Host my-secure-app.com;
        proxy_pass http://backend_upstream;
    }
}

—

4. 最後に:エンジニアが持つべき「疑いの目」

これらを実装したからといって、100%安全とは言い切れない。セキュリティとは積み重ねだ。

  • トークンは使い捨てか?(リセット成功後に必ず無効化しているか)
  • レートリミットを設けているか?(短時間に大量のリセット要求があればブロックしているか)
  • メール本文にトークンをそのまま含めていないか?(URLのパスパラメータとしてではなく、署名の一部として扱うべきケースもある)

「動けばいい」という考え方は、攻撃者にとっては「穴だらけです」という招待状に等しい。システムを設計する際は、常に「自分が攻撃者だったら、どこを突くか?」という視点を忘れないでほしい。

コードを書き終えたら、次は必ず「リセットリンクが漏洩したとき、被害を最小限にするにはどうするか?」を考えてみてくれ。それが、トップクラスのエンジニアへの第一歩だ。

コメント

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