パスワードリセットは「信頼の最後の砦」:その実装、本当に大丈夫か?
システム開発の現場で、ログイン機能やパスワードリセット機能に全精力を注ぐエンジニアは案外少ない。大抵はフレームワークの標準機能に頼り切り、裏側で何が起きているかまで追うことはないだろう。だが、攻撃者はそこを狙っている。パスワードリセットは、言わば「認証という門」を強引にこじ開けるためのマスターキーだ。
今日は、多くの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のパスパラメータとしてではなく、署名の一部として扱うべきケースもある)
「動けばいい」という考え方は、攻撃者にとっては「穴だらけです」という招待状に等しい。システムを設計する際は、常に「自分が攻撃者だったら、どこを突くか?」という視点を忘れないでほしい。
コードを書き終えたら、次は必ず「リセットリンクが漏洩したとき、被害を最小限にするにはどうするか?」を考えてみてくれ。それが、トップクラスのエンジニアへの第一歩だ。
コメント