# Type Juggling PHP: laisvi palyginimai kaip saugumo rizika

> Suprasti PHP Type Juggling: kaip lygina ==, ką pakeitė PHP 8.0 ir kodėl žetonai, maišos (hash) ir rolės turi būti lyginami griežtai.

Šaltinis: https://phpsos.de/lt/blog/type-juggling-php/  
Būsena: 2026-10-25  
Teikėjas: PHP SOS, PHP avarinė pagalba ir serverio sauga. Skubi pagalba per „WhatsApp“: https://wa.me/491736494914?text=Sveiki%2C%20PHP%20SOS%2C%20mums%20reikia%20pagalbos%20su%20m%C5%ABs%C5%B3%20PHP%20serveriu.%20%C4%AEmon%C4%97%3A%20%E2%80%A6%20Svetain%C4%97%3A%20%E2%80%A6

**Trumpai:** PHP Type Juggling reiškia automatinį tipų pavertimą, kuris laisvo palyginimo su `==` atveju lemia rezultatą. Nuo PHP 8.0 taisyklės griežtesnės, bet skaitinės simbolių eilutės ir toliau lyginamos kaip skaičiai, o `true`, `false` ir `null` seka savomis taisyklėmis. Saugumui svarbioms patikroms todėl galioja: `===`, `in_array(..., true)`, `match` vietoj `switch`, `hash_equals()` žetonams ir `password_verify()` slaptažodžiams.

## Palyginimas, kuris teisingas, nors neturėtų būti

Daug PHP programų saugumo spragų slypi ne sudėtingose funkcijose, o viename simbolyje: `==`, kur turėjo stovėti `===`. Kodas atrodo teisingas, bandymai su įprasta įvestimi praeina, ir vis dėlto patikra tam tikromis sąlygomis praleidžia tai, ką turėjo sustabdyti.

Priežastis yra PHP savybė, kuri kasdienybėje praktiška: reikšmės prireikus paverčiamos kitu tipu. Simbolių eilutė iš formos taip be aplinkkelių gali būti lyginama su skaičiumi iš duomenų bazės. Prisijungimuose, žetonuose, rolėse ar būsenos laukuose būtent šis dosnumas gali tapti pavojingas.

## Kaip PHP lygina su ==

PHP vadovas išsamiai aprašo laisvų palyginimų taisykles. Saugumui svarbiausi šie atvejai:

| Palyginimas | PHP 7 | PHP 8 | Priežastis |
|---|---|---|---|
| `0 == "a"` | teisinga | klaidinga | nuo PHP 8.0 lyginama kaip simbolių eilutės |
| `"1" == "01"` | teisinga | teisinga | abi skaitinės, lyginama kaip skaičiai |
| `"10" == "1e1"` | teisinga | teisinga | eksponentinis užrašas yra skaitinis |
| `100 == "1e2"` | teisinga | teisinga | skaičius prieš skaitinę simbolių eilutę |
| `null == false` | teisinga | teisinga | lyginama kaip loginė reikšmė |
| `"abc" == true` | teisinga | teisinga | netuščia simbolių eilutė laikoma teisinga |

PHP 8.0 pakeitimas apėmė visą klaidų klasę: anksčiau neskaitinė simbolių eilutė lyginant su skaičiumi tapdavo `0`. Tai veikė ir `switch` sakinius. Likusios lentelės eilutės rodo, kad laisvi palyginimai ir toliau slepia staigmenų.

Taip pat operatoriai `<`, `>` ir palyginimo operatorius `<=>` seka šias taisykles. Rikiavimuose tai dažniausiai nekenksminga, o tikrinant „suma mažesnė už limitą“ su reikšmėmis iš užklausos tipas turi būti žinomas iš anksto.

Ypač klastinga eilutė su eksponentiniu užrašu. Maišų (hash) reikšmės ir žetonai dažnai sudaryti iš skaitmenų ir raidžių. Jei tokia reikšmė atsitiktinai atrodo kaip skaičius eksponentiniu užrašu, `==` dvi skirtingas maišas (hash) gali palyginti kaip vienodas. Tai daugelio pranešimų apie prisijungimo apėjimus senesnėse PHP programose pagrindas.

## PHP Type Juggling tipinėse saugumo patikrose

Laisvi palyginimai slepiasi daugiau vietų nei tik operatoriuje `==`:

1. **`in_array()` ir `array_search()`** be trečio parametro lygina laisvai. Rolių ar ID leidžiamųjų sąrašas dėl to tampa minkštesnis, nei manoma.
2. **`switch`** lygina laisvai. Nuo PHP 8.0 prieinama išraiška `match`, pasak vadovo, priešingai lygina tipui saugiai su `===` ir meta klaidą, jei nė viena šaka netinka.
3. **JSON įvestys** pateikia tikrus tipus. Laukas, kurio tikitės kaip simbolių eilutės, gali ateiti kaip `true` ar skaičius ir su `==` netikėtai sutapti.
4. **`strcmp()` paslaptims** netinka, nes vykdymo laikas priklauso nuo turinio, o klaidų atvejai lengvai neteisingai įvertinami.
5. **Grąžinamų reikšmių patikra**: funkcijos kaip `strpos()` grąžina `0` radiniui pradžioje ir `false`, jei radinio nėra. `if (!strpos(...))` abu sumaišo.

## Saugūs palyginimai: palyginimas greta

Toliau pateikti pavyzdžiai rodo nesaugų ir saugų variantą:

```php
<?php
declare(strict_types=1);

// Patikrinti rolę
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen);          // nesaugu: laisvai
in_array($rolle, $erlaubteRollen, true);    // saugu: griežtai

// Įvertinti būseną
$aktion = match ($status) {                 // griežtai, klaida esant nežinomai reikšmei
    'offen'      => 'bearbeiten',
    'erledigt'   => 'archivieren',
};

// Patikrinti žetoną iš nuorodos
if ($tokenAusLink == $gespeichertesToken) { /* nesaugu */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* saugu */ }

// Patikrinti slaptažodį
if (md5($eingabe) == $hash) { /* nesaugu: silpnas metodas ir laisvas palyginimas */ }
if (password_verify($eingabe, $hash)) { /* saugu */ }

// Ieškoti poeilutės
if (strpos($text, 'abc') !== false) { /* teisinga: ir radinys 0 pozicijoje */ }
```

`hash_equals()` pagal vadovą pirmu parametru tikisi žinomos paslapties, o antru naudotojo reikšmės, ir lygina pastoviu laiku. Kaip žetonus apskritai saugiai sukurti, aprašo [Saugūs atsitiktiniai skaičiai ir žetonai PHP](https://phpsos.de/lt/blog/sichere-tokens-php/).

> **Svarbu:**
> `declare(strict_types=1)` palyginimų griežtais nepadaro. Šis sakinys liečia tik tipų pavertimą kviečiant funkcijas ir grąžinant reikšmes. Laisvus palyginimus turite pakeisti patys kode.

Griežti palyginimai turi kitą pusę: jie nepavyksta, kai tipai nesutampa. `'42' === 42` yra klaidinga. Reikšmės iš formų ir URL PHP ateina kaip simbolių eilutės, reikšmės iš duomenų bazės, priklausomai nuo tvarkyklės ir nustatymų, kaip simbolių eilutės ar skaičiai. Kas visur įveda `===` neišlygindamas tipų, sukuria klaidas, kurios veikiant pastebimos kaip teisėtų naudotojų atmetimas.

Sprendimas yra aiški riba: įvestys gaunant patikrinamos ir paverčiamos teisingu tipu, pavyzdžiui, su `filter_var($wert, FILTER_VALIDATE_INT)`, o po to kode tvarkomos tik su tipų deklaracijomis ir griežtais palyginimais. Tipų deklaracijos parametrams, grąžinamoms reikšmėms ir savybėms padeda, nes neteisingi tipai tada anksti išryškėja su klaidos pranešimu, užuot tyliai pranykę palyginime.

## Esamo kodo sisteminė patikra

Išaugusiuose projektuose laisvų palyginimų rasite šimtus. Ne kiekvienas yra problema. Susitelkite į vietas, kuriose sprendžiamas prieigos klausimas:

```bash
# Laisvi palyginimai eilutėse su saugumui svarbiais terminais
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array be griežto palyginimo
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
```

Statinės analizės įrankiai kaip PHPStan ar Psalm gali pageidavimu nuolat pranešti apie laisvus palyginimus. Taip nauji atvejai net nepatenka į pagrindinę šaką. Tipų pagrindus aprašo [PHP tipų sistema](https://phpsos.de/lt/php/typsystem/); kaip įvestys iš anksto paverčiamos teisingu tipu, rodo [Įvesties patikra ir filtravimas PHP](https://phpsos.de/lt/blog/eingaben-validieren-php/).

## Ką tai reiškia senoms sistemoms

Programos, kurios dar veikia su PHP 7, paveiktos senų, laisvesnių palyginimo taisyklių. Be to, yra antra problema: PHP 8.1 ir senesnėms PHP projektas oficialių saugumo atnaujinimų nebeteikia, taip pat ir nuo PHP2Shell. Padėtį aprašo [PHP 8.1 ir senesnės: kas dabar galioja](https://phpsos.de/lt/php2shell/php-8-1-und-aelter/).

Migruojant į PHP 8 kai kurie palyginimų rezultatai keičiasi. Tai dažniausiai pagerinimas, bet gali sulaužyti kodą, kuris rėmėsi senuoju elgesiu. Todėl suplanuokite bandymus, kurie tikslingai apima palyginimus su skaičiais, tuščiomis reikšmėmis ir `null`.

## Kaip PHP SOS padeda konkrečiai?

- Peržiūrime jūsų kodą ieškodami laisvų palyginimų vietose, kurios sprendžia dėl prisijungimo, teisių ar mokėjimų.
- Pakeičiame juos griežtais palyginimais, `match` išraiškomis ir tinkamomis patikros funkcijomis.
- Sukonfigūruojame statinę analizę taip, kad nauji laisvi palyginimai išryškėtų kuriant (build).
- Migracijas į naujausias PHP versijas lydime bandymais pakeistoms palyginimo taisyklėms.

Jūsų programinės įrangos patikrą aprašo [Saugumo auditas ir įsiskverbimo testas](https://phpsos.de/lt/leistungen/sicherheitsaudit-penetrationstest/).


## Dažni klausimai

**Kas yra Type Juggling PHP?**  
PHP kai kurių operacijų metu skirtingų tipų reikšmes automatiškai paverčia, pavyzdžiui, simbolių eilutę skaičiumi. Laisvo palyginimo su == atveju rezultatą lemia šis pavertimas. Tai patogu, bet gali sukelti palyginimus, kurie teisingi, nors reikšmės neatrodo vienodos.

**Ar PHP 8 išsprendė problemą?**  
Iš dalies. Nuo PHP 8.0 skaičius su neskaitine simbolių eilute lyginamas kaip simbolių eilutė, taigi 0 == 'a' nebėra teisinga. Tačiau dvi skaitinės simbolių eilutės ir toliau lyginamos kaip skaičiai, o palyginimai su true, false ir null seka savomis taisyklėmis.

**Ar declare(strict_types=1) padeda nuo laisvų palyginimų?**  
Ne. strict_types valdo, ar skaliariniai tipai kviečiant funkcijas ir grąžinant reikšmes paverčiami automatiškai. Palyginimo operatoriams == ir != tai įtakos neturi.

**Kaip teisingai lyginti slaptažodžius ir žetonus?**  
Slaptažodžius tikrinkite su password_verify, žetonus ir parašus su hash_equals. Abu lygina tiksliai, o hash_equals ir pastoviu laiku. Palyginimas su == ar strcmp tam netinka.

## Šaltiniai

- [PHP vadovas: palyginimo operatoriai](https://www.php.net/manual/de/language.operators.comparison.php)
- [PHP vadovas: match](https://www.php.net/manual/de/control-structures.match.php)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
