Pe scurt: PHP Type Juggling desemnează conversia automată de tip care, la comparația lejeră cu ==, decide rezultatul. Începând cu PHP 8.0 regulile sunt mai stricte, dar șirurile numerice sunt comparate în continuare ca numere, iar true, false și null urmează reguli proprii. Pentru verificările relevante pentru securitate se aplică de aceea: ===, in_array(..., true), match în loc de switch, hash_equals() pentru token-uri și password_verify() pentru parole.
O comparație care este adevărată, deși n-ar trebui să fie
Multe vulnerabilități din aplicațiile PHP nu se află în funcții complicate, ci într-un singur caracter: un == acolo unde ar fi trebuit un ===. Codul pare corect, testele cu introduceri normale trec și, totuși, în anumite condiții verificarea lasă să treacă ceva ce ar fi trebuit să oprească.
Cauza este o proprietate a PHP, practică în viața de zi cu zi: valorile sunt convertite la nevoie într-un alt tip. Un șir de caractere dintr-un formular poate fi astfel comparat fără ocol cu un număr din baza de date. La autentificări, token-uri, roluri sau câmpuri de stare, exact această generozitate poate deveni periculoasă.
Cum compară PHP cu ==
Manualul PHP descrie detaliat regulile pentru comparațiile lejere. Pentru securitate sunt importante mai ales aceste cazuri:
| Comparație | PHP 7 | PHP 8 | Motiv |
|---|---|---|---|
0 == "a" |
adevărat | fals | comparate ca șiruri începând cu PHP 8.0 |
"1" == "01" |
adevărat | adevărat | ambele numerice, comparație ca număr |
"10" == "1e1" |
adevărat | adevărat | notația exponențială este numerică |
100 == "1e2" |
adevărat | adevărat | număr față de șir numeric |
null == false |
adevărat | adevărat | comparație ca valoare booleană |
"abc" == true |
adevărat | adevărat | un șir nevid este considerat adevărat |
Modificarea din PHP 8.0 a dezamorsat o întreagă clasă de erori: înainte, un șir nenumeric devenea 0 la compararea cu un număr. Aceasta privea și instrucțiunile switch. Celelalte rânduri ale tabelului arată însă că comparațiile lejere ascund în continuare surprize.
Și operatorii <, > și operatorul de comparație <=> urmează aceste reguli. La sortări este de obicei inofensiv, dar la verificări precum „suma mai mică decât limita”, cu valori din cerere, tipul ar trebui să fie stabilit dinainte.
Deosebit de perfid este rândul cu notația exponențială. Valorile hash și token-urile constau adesea din cifre și litere. Dacă o astfel de valoare arată întâmplător ca un număr în notație exponențială, == poate compara două hash-uri diferite drept egale. Acesta este fundalul multor rapoarte despre ocoliri ale autentificării în aplicații PHP mai vechi.
PHP Type Juggling în verificări tipice de securitate
Comparațiile lejere se ascund în mai multe locuri decât doar în operatorul ==:
in_array()șiarray_search()compară lejer fără al treilea parametru. O listă de permisiuni cu roluri sau ID-uri devine astfel mai moale decât se crede.switchcompară lejer. Expresiamatch, disponibilă începând cu PHP 8.0, compară în schimb, potrivit manualului, sigur din punct de vedere al tipului, cu===, și aruncă o eroare dacă nu se potrivește nicio ramură.- Intrările JSON furnizează tipuri reale. Un câmp pe care îl așteptați ca șir poate sosi ca
truesau ca număr și poate coincide surprinzător cu==. strcmp()pentru secrete este nepotrivită, deoarece durata de execuție depinde de conținut, iar cazurile de eroare sunt ușor evaluate greșit.- Verificarea valorilor returnate: funcții precum
strpos()returnează0pentru o potrivire la început șifalsepentru nicio potrivire.if (!strpos(...))le confundă pe amândouă.
Comparații sigure: confruntarea
Exemplele următoare arată de fiecare dată varianta nesigură și pe cea sigură:
<?php
declare(strict_types=1);
// Verificarea rolului
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // nesigur: lejer
in_array($rolle, $erlaubteRollen, true); // sigur: strict
// Evaluarea stării
$aktion = match ($status) { // strict, eroare la valoare necunoscută
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Verificarea token-ului din link
if ($tokenAusLink == $gespeichertesToken) { /* nesigur */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* sigur */ }
// Verificarea parolei
if (md5($eingabe) == $hash) { /* nesigur: procedeu slab și comparație lejeră */ }
if (password_verify($eingabe, $hash)) { /* sigur */ }
// Căutarea unui subșir
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
hash_equals() așteaptă, potrivit manualului, secretul cunoscut ca prim parametru și valoarea utilizatorului ca al doilea și compară în timp constant. Cum generați în general token-uri în siguranță descrie Numere aleatorii și token-uri sigure în PHP.
declare(strict_types=1) nu face comparațiile stricte. Instrucțiunea privește doar conversia de tip la apelurile de funcții și la valorile returnate. Comparațiile lejere trebuie să le înlocuiți singuri în cod.
Comparațiile stricte au o față ascunsă: eșuează dacă tipurile nu se potrivesc. '42' === 42 este fals. Valorile din formulare și URL-uri sosesc în PHP ca șiruri, valorile din baza de date, în funcție de driver și setări, ca șiruri sau numere. Cine introduce peste tot ===, fără a aduce tipurile la același nivel, produce erori care se observă în exploatare ca respingerea unor utilizatori îndreptățiți.
Soluția este o graniță clară: intrările sunt validate la primire și aduse la tipul corect, de exemplu cu filter_var($wert, FILTER_VALIDATE_INT), iar apoi în cod sunt tratate doar cu declarații de tip și comparații stricte. Declarațiile de tip la parametri, la valori returnate și la proprietăți ajută în acest sens, deoarece tipurile greșite sunt observate devreme printr-un mesaj de eroare, în loc să dispară pe tăcute într-o comparație.
Verificarea sistematică a codului existent
În proiectele crescute în timp se găsesc sute de comparații lejere. Nu fiecare este o problemă. Concentrați-vă pe locurile în care se ia o decizie privind accesul:
# Comparații lejere în rânduri cu termeni relevanți pentru securitate
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array fără comparație strictă
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Instrumentele de analiză statică precum PHPStan sau Psalm pot raporta, la cerere, permanent comparațiile lejere. Astfel cazurile noi nu ajung deloc în ramura principală. Bazele despre tipuri le descrie Sistemul de tipuri al PHP; cum sunt aduse intrările în prealabil la tipul corect arată Validarea și filtrarea intrărilor în PHP.
Ce înseamnă aceasta pentru sistemele vechi
Aplicațiile care rulează încă pe PHP 7 sunt afectate de vechile reguli de comparație, mai laxe. La aceasta se adaugă o a doua problemă: pentru PHP 8.1 și mai vechi nu mai există din partea proiectului PHP actualizări oficiale de securitate, nici împotriva PHP2Shell. Situația o descrie PHP 8.1 și mai vechi: ce se aplică acum.
La o migrare spre PHP 8, unele rezultate ale comparațiilor se schimbă. Aceasta este de regulă o îmbunătățire, dar poate strica cod care se baza pe vechiul comportament. Planificați de aceea teste care acoperă în mod țintit comparațiile cu numere, valori goale și null.
Cum ajută PHP SOS concret?
- Căutăm în codul dumneavoastră comparații lejere în locurile care decid asupra autentificării, drepturilor sau plăților.
- Le înlocuim cu comparații stricte, expresii
matchși funcții de verificare potrivite. - Configurăm analiza statică astfel încât comparațiile lejere noi să iasă în evidență în build.
- Însoțim migrările către versiuni PHP actuale cu teste pentru regulile de comparație modificate.
O verificare a software-ului dumneavoastră descrie Audit de securitate și test de penetrare.
Întrebări frecvente
Ce este Type Juggling în PHP?
PHP convertește automat valori de tipuri diferite la unele operații, de exemplu un șir de caractere într-un număr. La comparația lejeră cu ==, această conversie decide rezultatul. Este comod, dar poate duce la comparații adevărate, deși valorile nu arată egale.
A rezolvat PHP 8 problema?
Parțial. Începând cu PHP 8.0, un număr este comparat cu un șir nenumeric ca șir, deci 0 == 'a' nu mai este adevărat. Două șiruri numerice sunt totuși comparate în continuare ca numere, iar comparațiile cu true, false și null urmează reguli proprii.
Ajută declare(strict_types=1) împotriva comparațiilor lejere?
Nu. strict_types controlează dacă tipurile scalare sunt convertite automat la apelurile de funcții și la valorile returnate. Nu are nicio influență asupra operatorilor de comparație == și !=.
Cum compar corect parolele și token-urile?
Parolele le verificați cu password_verify, token-urile și semnăturile cu hash_equals. Ambele compară exact și, în cazul hash_equals, în timp constant. O comparație cu == sau strcmp este nepotrivită pentru aceasta.