「たかが文字列比較」でシステムが落ちる:タイミング攻撃の真実と防御の実装
「パスワード照合なんて、if ($input === $stored_password) でいいだろ?」
もし君が現場のコードでそう断言しているなら、今すぐ考えを改めたほうがいい。レッドチームの視点から言わせれば、そのコードは「鍵の付いたドアの前に、わざわざ鍵の形を指でなぞって教えている」のと同じだ。
今日は、開発者が軽視しがちなサイドチャネル攻撃の一種「タイミング攻撃(Timing Attack)」について、なぜそれが致命的なのか、そしてどう防ぐべきかを、現場のリアリティを交えて解説する。
—
1. なぜ「一致した瞬間」に処理が終わるとバレるのか
一般的な文字列比較演算子(== や === など)は、「不一致が見つかった瞬間に比較を打ち切る(ショートサーキット)」という仕様になっている。
例えば、パスワードが password123 だとして、攻撃者が paxxxxxxxxxx を送ったとする。
1. p vs p(一致:次の文字へ)
2. a vs a(一致:次の文字へ)
3. x vs s(不一致:ここで即座に処理終了)
この「不一致の場所が後ろになるほど処理時間がわずかに長くなる」という差異を、攻撃者は何千、何万回と計測して統計を取る。ネットワークのジッター(ゆらぎ)を統計的に除去すれば、一文字ずつパスワードを特定することは驚くほど容易だ。
「そんな細かい差なんてネットワーク越しじゃ測定できないだろ?」と思うかもしれないが、今の機械学習や高精度な統計ツールを甘く見てはいけない。数分もあれば、長いパスワードも「一文字ずつ」丸裸にされる。
—
2. 鉄則:定数時間比較(Constant-time Comparison)
この攻撃を無効化する唯一の方法は、比較結果がどうあれ「必ず同じ時間だけ処理を継続する」ことだ。これを定数時間比較と呼ぶ。
PHPでの実装例
PHPには hash_equals() という神関数が標準で用意されている。これを使えば、内部的に文字列の長さに関わらず常に一定の時間で比較を行ってくれる。
<?php
// 安全なパスワード照合の例
$stored_hash = '$2y$10$e/EXAMPLEHASH...'; // データベースから取得したハッシュ
$user_input = $_POST['password'];
// hash_equals は常に一定時間で実行されるため、タイミング攻撃を防げる
if (hash_equals($stored_hash, crypt($user_input, $stored_hash))) {
echo "認証成功!";
} else {
// 成功時と失敗時で処理時間を極端に変えないことにも注意
echo "認証失敗";
}
?>
Node.js (JavaScript) での実装例
Node.jsでは crypto.timingSafeEqual() を使う。これは非常に厳密で、引数のバッファサイズが一致しないとエラーを吐く設計だ。
const crypto = require('crypto');
// 認証トークンやHMACの比較時に使用
function safeCompare(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
// サイズが違う場合、あえてダミーデータを作成して時間を合わせる(重要)
if (bufA.length !== bufB.length) {
return crypto.timingSafeEqual(Buffer.alloc(bufA.length, 'a'), Buffer.alloc(bufA.length, 'b'));
}
return crypto.timingSafeEqual(bufA, bufB);
}
—
3. インフラ・アーキテクチャ層での防御
コードレベルでの修正が基本だが、防御は多層的であるべきだ。
WAFによるレート制限
攻撃者は数万回の試行を繰り返す必要がある。したがって、同一IPからの異常なリクエストをブロックするレート制限(Rate Limiting)は有効な防御壁になる。
Nginxでのレート制限設定例:
# 認証エンドポイントへのアクセスをIPごとに制限
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/s;
location /login {
limit_req zone=auth_limit burst=10 nodelay;
proxy_pass http://app_server;
}
—
4. 現場のエンジニアへ:明日からの「規律」
最後に、一つだけ覚えておいてほしい。「自作の暗号比較関数」は絶対に作るな。
今回紹介した関数は、OSのメモリレベルで最適化され、分岐予測による処理時間の変化が起きないように極めて慎重に書かれている。我々エンジニアが「if文で分岐させて効率化しよう」と考えることは、セキュリティの世界では「脆弱性を作っている」と同義だ。
1. 比較は常に標準の定数時間比較関数を使う。
2. 比較対象が空であっても、同じ時間を消費するフローにする。
3. 認証エラーのレスポンス時間を、成功時と極端に乖離させない(ユーザー体験を損なわない程度に、意図的にウェイトを入れる手法もある)。
セキュリティとは、派手なハッキングテクニックよりも、こうした「退屈で泥臭い実装の積み重ね」で守られている。君が書くその一行が、次のインシデントを防ぐ防波堤になることを忘れないでくれ。
何か不明点があればいつでも聞いてくれ。現場からは以上だ。
コメント