Накратко: PHP Type Juggling е автоматичното преобразуване на типове, което при хлабаво сравнение с == решава резултата. От PHP 8.0 правилата са по-строги, но числовите низове продължават да се сравняват като числа, а true, false и null следват собствени правила. За проверки, свързани със сигурността, затова важи: ===, in_array(..., true), match вместо switch, hash_equals() за токени и password_verify() за пароли.
Сравнение, което е вярно, въпреки че не би трябвало да е
Много уязвимости в PHP приложения не се крият в сложни функции, а в един-единствен знак: ==, където е трябвало да стои ===. Кодът изглежда правилен, тестовете с нормален вход минават и въпреки това проверката при определени условия допуска нещо, което е трябвало да спре.
Причината е свойство на PHP, което в ежедневието е практично: при нужда стойностите се преобразуват в друг тип. Низ от формуляр така може да се сравни без заобикаляне с число от базата данни. При входове, токени, роли или полета за състояние точно тази щедрост може да стане опасна.
Как PHP сравнява с ==
Наръчникът на PHP описва подробно правилата за хлабави сравнения. За сигурността са важни най-вече тези случаи:
| Сравнение | PHP 7 | PHP 8 | Причина |
|---|---|---|---|
0 == "a" |
вярно | невярно | от PHP 8.0 се сравняват като низове |
"1" == "01" |
вярно | вярно | и двата са числови, сравнение като число |
"10" == "1e1" |
вярно | вярно | експоненциалният запис е числов |
100 == "1e2" |
вярно | вярно | число срещу числов низ |
null == false |
вярно | вярно | сравнение като булева стойност |
"abc" == true |
вярно | вярно | непразен низ се счита за вярно |
Промяната в PHP 8.0 обезвреди цял клас грешки: по-рано нечислов низ при сравнение с число ставаше 0. Това засягаше и switch конструкциите. Останалите редове от таблицата обаче показват, че хлабавите сравнения продължават да крият изненади.
И операторите <, > и операторът за сравнение <=> следват тези правила. При сортиране това обикновено е безобидно, но при проверки като „сума по-малка от лимит“ със стойности от заявката типът трябва да е известен предварително.
Особено коварен е редът с експоненциалния запис. Хеш стойностите и токените често се състоят от цифри и букви. Ако такава стойност случайно изглежда като число в експоненциален запис, == може да сравни два различни хеша като равни. Това е фонът на много съобщения за заобикаляне на входа в по-стари PHP приложения.
PHP Type Juggling в типични проверки за сигурност
Хлабавите сравнения се крият на повече места, отколкото само в оператора ==:
in_array()иarray_search()сравняват хлабаво без трети параметър. Списък с разрешени роли или ID-та така става по-мек, отколкото се предполага.switchсравнява хлабаво. Изразътmatch, наличен от PHP 8.0, според наръчника сравнява безопасно по тип с===и хвърля грешка, ако никой клон не съвпада.- JSON входовете връщат истински типове. Поле, което очаквате като низ, може да пристигне като
trueили като число и да съвпадне изненадващо с==. strcmp()за тайни е неподходящо, защото времето за изпълнение зависи от съдържанието и случаите на грешка лесно се оценяват погрешно.- Проверка на върнатите стойности: функции като
strpos()връщат0при попадение в началото иfalseпри липса на попадение.if (!strpos(...))бърка двете.
Сигурни сравнения: сравнителен преглед
Следващите примери показват всеки път несигурния и сигурния вариант:
<?php
declare(strict_types=1);
// Проверка на роля
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // несигурно: хлабаво
in_array($rolle, $erlaubteRollen, true); // сигурно: стриктно
// Оценка на състояние
$aktion = match ($status) { // стриктно, грешка при непозната стойност
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Проверка на токен от връзка
if ($tokenAusLink == $gespeichertesToken) { /* несигурно */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* сигурно */ }
// Проверка на парола
if (md5($eingabe) == $hash) { /* несигурно: слаб метод и хлабаво сравнение */ }
if (password_verify($eingabe, $hash)) { /* сигурно */ }
// Търсене на подниз
if (strpos($text, 'abc') !== false) { /* правилно: включително попадение на позиция 0 */ }
hash_equals() според наръчника очаква известната тайна като първи и стойността на потребителя като втори параметър и сравнява за константно време. Как изобщо да създавате токени сигурно, описва Сигурни случайни числа и токени в PHP.
declare(strict_types=1) не прави сравненията стриктни. Директивата засяга само преобразуването на типове при извикване на функции и върнатите стойности. Хлабавите сравнения трябва сами да замените в кода.
Стриктните сравнения имат обратна страна: те се провалят, ако типовете не си подхождат. '42' === 42 е невярно. Стойностите от формуляри и URL адреси пристигат в PHP като низове, стойностите от базата данни, в зависимост от драйвера и настройката, като низове или числа. Който въведе навсякъде ===, без да изравни типовете, създава грешки, които в експлоатацията се забелязват като отхвърляне на легитимни потребители.
Решението е ясна граница: входовете се валидират при получаването и се довеждат до правилния тип, например с filter_var($wert, FILTER_VALIDATE_INT), а след това в кода се третират само с декларации на типове и стриктни сравнения. Декларациите на типове при параметри, върнати стойности и свойства помагат, защото грешните типове тогава се забелязват рано със съобщение за грешка, вместо тихо да изчезнат в едно сравнение.
Систематична проверка на съществуващия код
В разраснали се проекти се намират стотици хлабави сравнения. Не всяко е проблем. Съсредоточете се върху местата, където се взема решение за достъп:
# Хлабави сравнения в редове със свързани със сигурността понятия
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array без стриктно сравнение
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Инструменти за статичен анализ като PHPStan или Psalm могат при желание постоянно да докладват хлабави сравнения. Така новите случаи изобщо не попадат в главния клон. Основите за типовете описва Типовата система на PHP; как входовете се довеждат предварително до правилния тип, показва Валидиране и филтриране на вход в PHP.
Какво означава това за стари системи
Приложенията, които още работят на PHP 7, са засегнати от старите, по-небрежни правила за сравнение. Към това се добавя втори проблем: за PHP 8.1 и по-стари проектът PHP няма повече официални актуализации за сигурност, включително срещу PHP2Shell. Положението описва PHP 8.1 и по-стари: какво важи сега.
При миграция към PHP 8 някои резултати от сравнения се променят. Това обикновено е подобрение, но може да счупи код, който се е осланял на старото поведение. Затова планирайте тестове, които целенасочено покриват сравнения с числа, празни стойности и null.
Как помага PHP SOS конкретно?
- Търсим във Вашия код хлабави сравнения на места, които решават за вход, права или плащания.
- Заменяме ги със стриктни сравнения, изрази
matchи подходящи функции за проверка. - Настройваме статичния анализ така, че новите хлабави сравнения да се забелязват в build-а.
- Съпровождаме миграции към актуални версии на PHP с тестове за променените правила за сравнение.
Проверка на Вашия софтуер описва Одит на сигурността и тест за проникване.
Често задавани въпроси
Какво е Type Juggling в PHP?
PHP преобразува автоматично стойности от различни типове при някои операции, например низ в число. При хлабавото сравнение с == това преобразуване решава резултата. Това е удобно, но може да доведе до сравнения, които са верни, въпреки че стойностите не изглеждат равни.
Реши ли PHP 8 проблема?
Отчасти. От PHP 8.0 число се сравнява с нечислов низ като низ, така че 0 == 'a' вече не е вярно. Два числови низа обаче продължават да се сравняват като числа, а сравненията с true, false и null следват собствени правила.
Помага ли declare(strict_types=1) срещу хлабави сравнения?
Не. strict_types управлява дали скаларните типове се преобразуват автоматично при извикване на функции и връщане на стойности. Върху операторите за сравнение == и != няма влияние.
Как да сравнявам правилно пароли и токени?
Паролите проверявате с password_verify, а токените и подписите с hash_equals. И двете сравняват точно и, при hash_equals, за константно време. Сравнение с == или strcmp не е подходящо за това.