パスワードリセットの「0.1ミリ秒」を盗む:タイミング攻撃の真実と防御の極意
現場でコードを叩いていると、「ログイン処理なんてライブラリを使えばいい」「パスワードリセットのトークン比較なんて==で十分だ」という声が聞こえてくる。だが、セキュリティの最前線でインシデント対応をしていると、その「些細な油断」が数千万規模の個人情報流出に直結する瞬間を何度も見てきた。
今日は、開発者が軽視しがちな「タイミング攻撃(Timing Attack)」について、その実態と、今日から実装を変えるための「正解」を伝授する。
—
なぜ「比較処理」が攻撃の対象になるのか?
結論から言えば、現代のCPUは「賢すぎる」からだ。
多くのプログラミング言語で使われる文字列比較演算子(== や === など)は、「最初の文字が違えば、即座に処理を中断して false を返す」という最適化を行っている。
これを攻撃者の視点で見るとどうなるか。
1. 攻撃者は、ランダムなトークンを大量に送りつける。
2. 「1文字目が正解」のトークンを送った時だけ、システム内部の比較処理が「2文字目の判定」に進むため、レスポンスがわずかに遅れる。
3. この「わずかな遅延」を統計的に積み重ねれば、1文字ずつ、確実に正しいトークンを特定できてしまう。
ネットワークの揺らぎがあるから無理だ?……いや、現代の統計的処理能力を甘く見てはいけない。数千〜数万リクエストを投げれば、ノイズを打ち消して「正解の文字」を浮き彫りにするのは、もはや攻撃者にとってルーチンワークだ。
—
「定数時間比較」という唯一の防御策
この攻撃を防ぐ唯一の手段は、「文字列の内容に関わらず、必ず同じ時間だけ処理を継続する」アルゴリズムを採用することだ。これを「定数時間比較(Constant-time comparison)」と呼ぶ。
PHPでの実装例
PHPには hash_equals() という関数が標準で用意されている。これを使うのが「正解」だ。
<?php
// 安全なトークン比較の実装例
// データベースから取得した本来のトークン(ハッシュ化済であること)
$correct_token = '...';
// ユーザーから送信されたトークン
$user_provided_token = $_POST['token'];
// hash_equals は定数時間で比較を行うため、
// 攻撃者はレスポンス時間の差からトークンを推測できない。
if (hash_equals($correct_token, $user_provided_token)) {
// 成功時の処理
} else {
// 失敗時の処理
}
?>
Pythonでの実装例
Pythonでは secrets.compare_digest() を使用する。hmac.compare_digest も同様だ。
import secrets
# 比較対象のトークン
correct_token = "..."
user_provided_token = request.form.get("token")
# 定数時間比較を使用して攻撃を無効化する
if secrets.compare_digest(correct_token, user_provided_token):
# 成功時の処理
pass
else:
# 失敗時の処理
pass
—
実務で「もう一歩」先に行くための防御戦術
コードを直すだけでは不十分な場合がある。システム全体で層を厚くするのがプロの仕事だ。
1. レートリミット(Rate Limiting)の徹底
タイミング攻撃には「大量のリクエスト」が必要だ。NginxなどのWebサーバー層で、同一IPや同一アカウントへのリクエスト頻度を制限しよう。
# Nginx設定例:同一IPからのリクエストを秒間5回に制限
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
server {
location /password-reset {
limit_req zone=one burst=10;
# 以下プロキシ設定など
}
}
2. トークンの性質を変える
もし可能なら、トークン自体を「一度使い切りの短いハッシュ」にするだけでなく、「ユーザーのIPアドレスやUser-Agentを複合キーとして含める」ことで、攻撃の難易度を跳ね上げることができる。
3. WAFの活用
クラウド型WAF(AWS WAFやCloudflareなど)を使用している場合、異常なリクエストパターンを検知するルールを適用しよう。「短時間に特定のURLへ、異なるパラメーターで大量アクセスがある」といった挙動を検知して遮断するだけで、タイミング攻撃の成功率は劇的に下がる。
—
チーフからのアドバイス:セキュリティは「負けない戦い」である
エンジニアとしてコードを書いていると、ついつい「機能の実装」が目的になりがちだ。しかし、セキュリティにおいては「機能していること」以上に「悪用できる隙がないこと」が評価される。
今回紹介した hash_equals() のような「定数時間比較」は、一見すると地味で、性能面でのメリットも皆無だ。だが、こうした「誰にも気づかれないような細かい配慮」の積み重ねこそが、万が一のインシデント発生時に、あなたのシステムとユーザーを守る最後の砦になる。
「== でいいや」と手を抜くか、それとも「数ミリ秒の差を埋める」プロの仕事をするか。その選択が、数年後のあなた自身のキャリアと、サービスの信頼性を形作ることを忘れないでほしい。
さあ、今すぐコードベースを grep して、不適切な文字列比較がないか確認してくれ。それが、今日の君の最初の任務だ。
コメント