FlawOpen

flawopen.com/型のジャグリング/PHP ==

PHP の == はハッシュ比較に安全ですか?

CWE-697: Incorrect Comparisonリファレンスページ
要点

いいえ。== では2つの数値文字列が数値として比較されるため、科学表記の異なる文字列が PHP 8 でも一致することがあります。まず入力型を検証します。厳密な値比較には ===、秘密トークンや MAC には内容依存の時間漏えいを避ける hash_equals() を使います。

脆弱
<?php
// Both strings are numeric and represent zero
$storedHash = "0e462097431906509019562988736854";
$submittedHash = "0e987654321";

// == returns true on PHP 7 and PHP 8
if ($storedHash == $submittedHash) {
    login();
}

// Request data may also have the wrong type:
$submitted = $_POST['token'] ?? null;
if ($storedHash == $submitted) {
    login();
}
// ?token[]=x supplies an array, not a string.
修正済み
<?php
$submitted = $_POST['token'] ?? null;
if (!is_string($submitted)) {
    http_response_code(400);
    exit;
}

// For secret tokens or MACs:
$tokenIsValid = hash_equals($storedToken, $submitted);
if ($tokenIsValid) {
    login();
}

// For non-secret identifiers, strict
// comparison checks both type and value:
$idsMatch = ($expectedId === $submitted);

// Passwords require password_verify(),
// not a hand-written hash comparison:
$passwordIsValid = password_verify($plain, $storedPasswordHash);

「Magic Hash」の問題

PHP 8 の前後を問わず、2つの数値文字列は数値として比較されます。0e で始まり、その後が数字だけの16進ハッシュは、数値がゼロとなる有効な科学表記文字列です。たとえば 0e462097431906509019562988736854 と 0e987654321 は異なる文字列ですが、どちらもゼロを表すため == で等しくなります。PHP 8 が変更したのは文字列と数値の比較であり、数値文字列同士のこのケースではありません。

PHP 8 では数値と非数値文字列の比較が変わり、非数値文字列をゼロ扱いする代わりに文字列表現を比較します。しかし、2つの数値 0e… 文字列の比較は残っています。バージョンを問わず認証に == を使わないでください。

リクエストの型も信頼境界の一部

リクエストパラメーターが文字列とは限りません。?token[]=x のようなクエリーは配列になります。文字列 API に渡すと、PHP のバージョンや関数によって型エラーや意図しない分岐が起きます。比較や hash_equals() の呼び出し前に、境界で文字列か確認してください。

堅牢な防御は2段階です。境界で入力型を検証し、安全要件に合う比較方法を選びます。=== は型と値を比較し、hash_equals() は内容依存の時間漏えいを避けて秘密文字列を比較します。

よくある質問

PHP 8 で完全に修正されますか?

いいえ。PHP 8 は文字列と数値の比較を変更しましたが、数値文字列同士は引き続き数値として比較されます。異なる 0e… 文字列が一致する場合があります。厳密な型の値には ===、秘密値には hash_equals() を使い、対応する全バージョンで入力型を検証します。

なぜ === ではなく hash_equals() ですか?

PHP は hash_equals() を文字列内容のタイミング攻撃に安全な関数として説明しています。通常の比較は、差が現れる位置によって時間が変わることがあります。既知の秘密を先に、ユーザー入力を後に渡してください。パスワードには password_verify() を使います。

他の言語にもありますか?

JavaScript の == にも比較上の落とし穴があるため、通常は === が推奨されます。Magic Hash 特有の現象は、PHP が16進文字列を科学表記の数値として比較することに由来します。

参考資料

関連項目 Rust の unsafe ブロックはメモリ安全なのか? strncpy() は strcpy() より安全ですか? CWE と CVE:何が違うのか Mass Assignment:リスクと安全なフィールド許可リスト Serverless の状態漏えい:ユーザーデータをリクエストごとに分離