FlawOpen

flawopen.com/Type Juggling/PHP ==

Безопасно ли сравнивать хэши через PHP ==?

CWE-697: Incorrect ComparisonСправочная страница
Краткий ответ

Нет. При == PHP численно сравнивает две числовые строки, поэтому разные строки в научной нотации могут совпасть и в 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. Шестнадцатеричный хэш, начинающийся с 0e и содержащий далее только цифры, является корректной строкой в научной нотации со значением ноль. Например, 0e462097431906509019562988736854 и 0e987654321 различаются, но обе строки означают ноль и равны при ==. PHP 8 изменил сравнение строки с числом, но не сравнение двух числовых строк.

PHP 8 изменил сравнение числа с нечисловой строкой: теперь сравниваются их строковые формы, а не строка приводится к нулю. Классическое сравнение двух числовых строк 0e… осталось. Не используйте == для аутентификации независимо от версии PHP.

Типы запроса — часть границы доверия

Параметры запроса не всегда являются строками. Запрос вроде ?token[]=x создаёт массив. Передача его строковой API может вызвать ошибку типа или неожиданный путь выполнения — в зависимости от версии PHP и функции. Проверяйте, что значение является строкой, до сравнения или вызова hash_equals().

Надёжная защита состоит из двух шагов: проверяйте тип на границе и выбирайте оператор по требованию. === проверяет тип и значение; hash_equals() сравнивает секретные строки без утечки времени, зависящей от содержимого.

Частые вопросы

PHP 8 полностью устраняет проблему?

Нет. PHP 8 изменил сравнение строки с числом, но две числовые строки всё ещё сравниваются численно. Разные строки 0e… могут быть равны. Используйте === для точных типизированных значений или hash_equals() для секретов и проверяйте типы во всех поддерживаемых версиях.

Почему hash_equals(), а не ===?

PHP указывает, что hash_equals() защищает содержимое строки от временных атак; обычное сравнение может занимать разное время в зависимости от позиции различия. Передавайте известный секрет первым аргументом, пользовательскую строку — вторым. Для паролей используйте password_verify().

Такое есть в других языках?

В JavaScript тоже есть неожиданные результаты оператора ==, поэтому обычно рекомендуют ===. Специфика magic hash в PHP связана с числовым сравнением шестнадцатеричных строк в научной нотации.

Источники

См. также Безопасен ли Rust с блоком unsafe? Безопаснее ли strncpy(), чем strcpy()? CWE и CVE: в чём разница? Mass assignment: риски и безопасные списки полей Утечка состояния serverless: изоляция данных запросов