Текуща ситуация: PHP2Shell. Две критични уязвимости в PHP (CVSS 9,2) позволяват злонамерен код без вход. Корекции от 21.10.2026 г.Какво да направите сега →

Type Juggling в PHP: хлабавите сравнения като риск за сигурността

Защо хлабавите сравнения с == в PHP могат да доведат до уязвимости, какво се промени с PHP 8.0 и как да пишете устойчиви сравнения във входове, токени и права.

Актуалност 5 мин. четенеСигурна разработка

Накратко: 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 в типични проверки за сигурност

Хлабавите сравнения се крият на повече места, отколкото само в оператора ==:

  1. in_array() и array_search() сравняват хлабаво без трети параметър. Списък с разрешени роли или ID-та така става по-мек, отколкото се предполага.
  2. switch сравнява хлабаво. Изразът match, наличен от PHP 8.0, според наръчника сравнява безопасно по тип с === и хвърля грешка, ако никой клон не съвпада.
  3. JSON входовете връщат истински типове. Поле, което очаквате като низ, може да пристигне като true или като число и да съвпадне изненадващо с ==.
  4. strcmp() за тайни е неподходящо, защото времето за изпълнение зависи от съдържанието и случаите на грешка лесно се оценяват погрешно.
  5. Проверка на върнатите стойности: функции като 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 не е подходящо за това.

Източници

Незабавен контакт

Всеки час има значение. Пишете ни сега.

Опишете накратко системата, версията на PHP и какво сте забелязали. Ние се свързваме с Вас, уточняваме ситуацията и работим отдалечено, докато инцидентът бъде приключен и документиран.

Незабавна помощ