はじめに
PHP8 上級試験の勉強をしている中で、文字列比較に関する挙動を確認するコードに出会いました。
一見すると同じ「比較」ですが、===
と hash_equals()
では処理時間の差が大きく、セキュリティ面でも意味が異なります。この記事では実際のコードと計測結果を通して、その違いを整理してみます。
目次
実験コード
まずは 65,535 文字の文字列を用意し、先頭1文字だけ異なるケースで比較を行いました。
<?php
declare(strict_types=1);
error_reporting(-1);
$known_string = $user_string = str_repeat('a', 65535);
$user_string[0] = 'b';
$t = hrtime(true);
$res1 = ($known_string === $user_string);
$t_end = hrtime(true);
echo "=== 1st: ", $t_end - $t, " ns\n";
$t = hrtime(true);
$res2 = hash_equals($known_string, $user_string);
$t_end = hrtime(true);
echo "hash_equals: ", $t_end - $t, " ns\n";
実行結果
私の環境では以下のような結果が得られました。
=== 1st: 1333 ns
=== 2nd: 125 ns
hash_equals 1st: 59334 ns
hash_equals 2nd: 55417 ns
===
:100ns〜1µs 程度で終了(非常に高速)hash_equals()
:毎回 50µs 以上(桁違いに遅いが安定)
なぜこうなるのか?
===
の特徴
- 内部的には
memcmp
のような実装で、最初に不一致を見つけた時点で終了。 - → 先頭が違えば即終了、末尾や完全一致なら全バイトを走査するため遅くなる。
hash_equals()
の特徴
- 「全バイトを必ず走査し、最後にまとめて結果を返す」仕組み。
- どこで違っても処理時間は同じ(定時間比較)。
- タイミング攻撃への耐性を持つ。
補足:タイミング攻撃とは?
ここで登場するキーワードが 「タイミング攻撃(Timing Attack)」 です。
概 要
処理時間の違いを手掛かりにして、秘密情報(パスワードやトークン)を推測する攻撃手法。
サンプルコード
function checkPassword($input, $real) {
for ($i = 0; $i < strlen($real); $i++) {
if ($input[$i] !== $real[$i]) {
return false; // 不一致が出たら即終了
}
}
return true;
}
この場合
- 最初の文字が違えばすぐ終わる → 時間が短い
- 最初の文字が合っていれば2文字目まで比較 → ちょっと時間が長い
- 正解に近づくほど処理が遅くなる
攻撃者はこの「時間差」を精密に測定することで、正しい文字列を1文字ずつ推測できてしまう。
対 策hash_equals()
は「全長を必ず比較する」ため、処理時間が入力内容に依存しない。つまり時間差から推測されることを防ぐ仕組み。
セキュリティ観点
===
は便利で速いが、比較位置によって処理時間が変動するため、攻撃者が「一致している接頭辞の長さ」を推測できる可能性がある。
hash_equals()
は遅いが、どの位置で違っても処理時間がほぼ一定なので、安全に使える。
まとめ
===
:通常の比較用途で使う。速いがタイミング攻撃に弱い。hash_equals()
:HMAC やトークンなど「秘密情報の比較」では必須。
👉 今回の計測で、両者の挙動の違いとタイミング攻撃対策の重要性を体感できました。