こんにちは!Webアプリケーションを作ったり、会社のITまわりを担当したりする中で、「パスワードをお忘れですか?」という機能は必ずと言っていいほど実装しますよね。
ユーザーがパスワードを忘れたとき、登録されたメールアドレスに「パスワードを再設定するためのURL」が届くアレです。とても便利で当たり前の機能ですが、実はこのパスワードリセット機能、サイバー攻撃者にとっては「システム全体の合鍵をこっそり作れてしまう大好物のターゲット」になりやすい場所なんです。
今回は、このパスワードリセット機能に潜む「トークンの予測」と「Hostヘッダー注入」という2つの怖い手口について、身近な防犯に例えながら、一緒に優しく紐解いていきましょう!一歩ずつ対策を学んでいけば、絶対に怖くありませんよ。
—
1. パスワードリセットの仕組みと「合鍵」の正体
まずは、普段私たちが何気なく使っているパスワードリセットが、裏側でどう動いているのかを整理してみましょう。
1. ユーザーが「パスワードを忘れた」とメールアドレスを入力する。
2. システムが「この人は本当に本人かな?」と確認するため、使い捨ての特別な文字列(これを「リセットトークン」と呼びます)を生成する。
3. システムはそのトークンを含んだURL(例: https://example.com/reset?token=abc123xyz)をメールで送る。
4. ユーザーがそのURLをクリックし、新しいパスワードを入力する。
ここで登場する「リセットトークン」は、いわば「今だけ使える特別な合鍵」です。この合鍵があまりにも単純だったり、作り方がデタラメだったりするとどうなるでしょうか? そう、泥棒に簡単に合鍵を量産されてしまうのです。
—
2. 脆弱性その1:エントロピー不足(予測可能な合鍵)
家の鍵に例えると…
想像してみてください。あなたが新居に引っ越してきて、玄関の鍵を新しく買いました。その鍵の番号が、もし「1234」しかなかったらどうでしょう? 泥棒は適当に「1234」と試すだけで、簡単にあなたの家に侵入できてしまいますよね。
これが、システムにおける「エントロピー(ランダム性・多様性)不足」という状態です。
攻撃のメカニズム
古いシステムや、セキュリティの知識が少し足りないコードでは、リセットトークンを作る際に以下のような甘い方法をとってしまいがちです。
- 単純な連番(
1,2,3…) - 現在の時刻(タイムスタンプ)をそのまま暗号化したもの
- 「ユーザーID + 登録日の日付」を組み合わせただけのもの
これらは、攻撃者からすると「次にどんな合鍵が作られるか簡単に予測できる」状態です。攻撃者は自動化ツールを使って、あなたになりすまし、何万回ものリセット要求をシステムに送りつけて、予測したトークンであなたのアカウントを乗っ取ってしまいます。
優しい対策:安全な乱数を使おう
対策はシンプルです。プログラム側で「人間には絶対に予測できない、完全にバラバラな文字列」を作ればいいのです。
PHPを例に、安全なトークンの作り方を見てみましょう。
<?php
// 【NGな例】予測しやすい単純なトークン
// ユーザーIDと現在時刻をくっつけただけでは、時刻がバレると推測されます
$bad_token = md5($user_id . time());
// 【OKな例】暗号学的に安全なランダムバイト列を使用する
// random_bytes() を使うことで、推測不可能な強力なエントロピー(ランダム性)を確保します
// bin2hex() で人間が扱いやすい16進数の文字列に変換します
$secure_token = bin2hex(random_bytes(32));
// この $secure_token をデータベースに保存し、URLに含めてメールで送信します!
?>
これだけで、合鍵のセキュリティはグッと強固になります。「予測できないランダムな値を作る関数を使う」と覚えておいてくださいね。
—
3. 脆弱性その2:Hostヘッダーインジェクション(偽の合鍵ショップへの誘導)
続いては、少し応用編の「Hostヘッダーインジェクション」という攻撃です。こちらは仕組みが少しユニークですが、実務で非常に狙われやすいポイントです。
郵便配達の仕組みに例えると…
あなたは手紙を出すとき、封筒の宛名(お届け先の住所)を書きますよね。郵便配達員(サーバー)は、その宛名を見て「あ、この人宛てだな」と手紙を届けます。
もし、悪意ある人が郵便配達員に対して、こっそり「次の手紙の宛先、あそこの怪しいビルに変えておいてよ」と嘘の指示を出せたらどうなるでしょうか? 本来あなた宛てだったはずの大切なパスワードリセットのURLが、攻撃者の指定した詐欺サイト(偽の合鍵ショップ)に届いてしまう……これがHostヘッダーインジェクションの罠です。
攻撃のメカニズムと危険なコード
Webサイトにアクセスするとき、ブラウザはサーバーに対して「今、example.com というサイトにアクセスしていますよ」という情報をひっそりと送っています。これが Host ヘッダーと呼ばれるものです。
未熟なシステムでは、この Host ヘッダーの値を「ユーザーから送られてきた嘘のデータ」をそのまま信用して、メール内のURLに使ってしまうという実装ミスを犯すことがあります。
<?php
// 【危ない実装例】
// $_SERVER['HTTP_HOST'] は、ユーザーのブラウザから自由に書き換えて送信できる値です!
$host = $_SERVER['HTTP_HOST'];
// もし攻撃者がこの Host ヘッダーを 「evil-attacker.com」 に書き換えてアクセスしてきたら…
// システムは悪びれもなく、以下のURLを生成してメールを送ってしまいます
$reset_link = "https://" . $host . "/reset?token=" . $secure_token;
// 生成されるURLの例: https://evil-attacker.com/reset?token=abc123xyz
// これを受け取ったユーザーがうっかりクリックすると、トークンが攻撃者に盗まれてしまいます!
?>
優しい対策:ドメインは自分(サーバー側)で固定しよう
この攻撃を防ぐのはとても簡単です。「ユーザーが言ってきた宛先を信用せず、自分(システム)が知っている正しいドメイン名を使う」ようにコードを書き換えればいいのです。
<?php
// 【安全な実装例】
// ユーザーからの入力(Hostヘッダー)は一切信用せず、環境変数や設定ファイルで定義した「信頼できる自社のドメイン」を直書き(または定数として定義)します
define('TRUSTED_BASE_URL', 'https://example.com');
// どんなにHostヘッダーを偽装されても、正しいURLが生成されます
$reset_link = TRUSTED_BASE_URL . "/reset?token=" . $secure_token;
// 生成されるURLの例: https://example.com/reset?token=abc123xyz
// これで安心してユーザーにメールを届けられますね!
?>
もしどうしても動的にドメインを判定したいインフラ環境(マルチテナント型サービスなど)の場合は、許可するドメインのホワイトリスト(許可リスト)を厳格に作り、合致するもの以外はすべて弾く設定をWebサーバー(NginxやApacheなど)やアプリケーション側で行う必要があります。
—
4. まとめ:今日から実践できる防犯チェックリスト
いかがでしたでしょうか? パスワードリセット機能の脆弱性は、ちょっとした油断から生まれてしまいますが、正しい知識を持っていれば確実に防ぐことができます。
最後に、今日からあなたのプロジェクトで確認できる「防犯チェックリスト」をまとめておきますね。
1. パスワードリセットのトークンは、予測不可能な十分な長さとランダム性(random_bytes 等)を持っていますか?
2. メールに記載するURLのドメインは、ユーザーからの入力(Hostヘッダー)をそのまま使わず、システム側で固定または厳格に検証していますか?
3. リセットトークンには「有効期限(例: 発行後30分以内)」や「1回使ったら無効化する仕組み」がきちんとついていますか?
セキュリティ対策は、一度にすべて完璧にするのは大変です。でも、こうして一つひとつの仕組みを知ることで、あなたの作るWebサイトやサービスは確実に安全になっていきます。
一歩ずつ、安心して使えるサービスを一緒に育てていきましょう!
コメント