hash_equalsでタイミング攻撃を防ぐ

hash_equalsとは、二つの文字列を常に一定の時間で比較して一致を返すPHPの関数です。処理時間の差から情報が漏れるタイミング攻撃を防ぐため、トークンや署名の照合に使います。

==比較には時間差が漏れる

トークンや署名を照合するとき、つい === で比べてしまいますが、秘密の値の比較にこれを使うのは危ういですね。文字列比較は先頭から一文字ずつ照合し、違いが見つかった時点で処理を打ち切ります。つまり「何文字目まで一致していたか」で処理時間がわずかに変わる。この差を大量の試行で測ることで、正しい値を一文字ずつ推測していけてしまう、というのがタイミング攻撃です。

// 危険。一致した文字数で処理時間が変わる
if ($knownToken === $userToken) {
    // ...
}

// 先頭が合っているほど比較に時間がかかる、という差が漏れる

hash_equalsは定数時間で比較する

hash_equals は、文字列全体を常に同じ時間で比較するように作られています。途中で違いが見つかっても最後まで走るので、処理時間から一致の度合いが漏れません。秘密の値を比べるときは、迷わずこれを使うのが正解だと思います。第一引数に既知の正しい値、第二引数に利用者から来た値、という順で渡すのが慣例ですね。

// 安全。定数時間で比較する
if (hash_equals($knownToken, $userToken)) {
    // 認証成功
}

// CSRFトークンの照合でも同じ
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
    exit('検証失敗');
}

どこで使うべきか

使いどころは、APIキーやトークン、HMAC署名、CSRFトークンなど「秘密の固定文字列を照合する」場面ですね。逆にパスワードの照合は password_verify が内部で安全な比較をしてくれるので、こちらを hash_equals で自作する必要はありません。またウェブフックの署名検証は典型例で、送られてきた署名を自分で計算し直し、それを hash_equals で比べる、という流れになります。

// Webフック署名の検証
$expected = hash_hmac('sha256', $payload, $secret);
$received = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit('署名が不正です');
}

まとめ

秘密の値の照合で === を使うと、比較にかかる時間の差から情報が漏れる余地があります。hash_equals は定数時間で比べてくれるので、トークンや署名の検証ではこれを使うのが基本だと思いますね。パスワードは password_verify に任せる、その他の秘密は hash_equals、と役割を分けておけば迷わない気がします。地味ですが、外せない一手だと思います。

よくある質問

Q. パスワードの照合にも hash_equals を使うべきですか?
A. パスワードは password_verify が内部で安全な比較をしてくれるので、そちらを使えば十分です。hash_equals はトークンや署名など固定の秘密文字列に使います。

Q. 引数の順番に決まりはありますか?
A. 第一引数に既知の正しい値、第二引数に利用者から来た値を渡すのが慣例です。逆でも比較は成立しますが、慣例に合わせておくと読み手に優しいですね。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です