In breve: il type juggling in PHP indica la conversione automatica dei tipi che, nel confronto lasco con ==, decide il risultato. Da PHP 8.0 le regole sono più severe, ma le stringhe numeriche continuano a essere confrontate come numeri, e true, false e null seguono regole proprie. Per le verifiche rilevanti per la sicurezza vale quindi: ===, in_array(..., true), match al posto di switch, hash_equals() per i token e password_verify() per le password.
Un confronto vero anche se non dovrebbe esserlo
Molte vulnerabilità nelle applicazioni PHP non si nascondono in funzioni complicate, ma in un solo carattere: un == dove avrebbe dovuto esserci un ===. Il codice sembra corretto, i test con input normali passano, e tuttavia in certe condizioni la verifica lascia passare qualcosa che avrebbe dovuto fermare.
La causa è una caratteristica di PHP, pratica nell'uso quotidiano: i valori vengono convertiti in un altro tipo quando serve. Una stringa proveniente da un modulo può così essere confrontata senza passaggi intermedi con un numero del database. In login, token, ruoli o campi di stato, proprio questa generosità può diventare pericolosa.
Come PHP confronta con ==
Il manuale PHP descrive in dettaglio le regole per i confronti laschi. Per la sicurezza contano soprattutto questi casi:
| Confronto | PHP 7 | PHP 8 | Motivo |
|---|---|---|---|
0 == "a" |
vero | falso | Da PHP 8.0 confrontati come stringhe |
"1" == "01" |
vero | vero | Entrambi numerici, confronto come numero |
"10" == "1e1" |
vero | vero | La notazione esponenziale è numerica |
100 == "1e2" |
vero | vero | Numero contro stringa numerica |
null == false |
vero | vero | Confronto come valore di verità |
"abc" == true |
vero | vero | Una stringa non vuota vale come vera |
La modifica in PHP 8.0 ha disinnescato un'intera classe di errori: in precedenza una stringa non numerica nel confronto con un numero diventava 0. Ciò riguardava anche le istruzioni switch. Le altre righe della tabella mostrano però che i confronti laschi continuano a riservare sorprese.
Anche gli operatori <, > e l'operatore di confronto <=> seguono queste regole. Negli ordinamenti di solito è innocuo, ma in verifiche come «importo inferiore al limite» con valori provenienti dalla richiesta il tipo dovrebbe essere già stabilito.
Particolarmente insidiosa è la riga con la notazione esponenziale. Valori di hash e token sono spesso composti da cifre e lettere. Se un valore del genere appare per caso come un numero in notazione esponenziale, == può confrontare come uguali due hash diversi. È lo sfondo di molte segnalazioni di aggiramento del login in applicazioni PHP più vecchie.
Type juggling in PHP nelle tipiche verifiche di sicurezza
I confronti laschi si nascondono in più punti del solo operatore ==:
in_array()earray_search()confrontano in modo lasco senza il terzo parametro. Una lista consentita con ruoli o ID diventa così più morbida del previsto.switchconfronta in modo lasco. L'espressionematch, disponibile da PHP 8.0, confronta invece secondo il manuale con sicurezza di tipo, con===, e genera un errore se nessun ramo corrisponde.- Gli input JSON forniscono tipi reali. Un campo che si aspetta come stringa può arrivare come
trueo come numero e coincidere in modo sorprendente con==. strcmp()per i segreti non è adatta, perché la durata dipende dal contenuto e i casi di errore vengono facilmente valutati in modo sbagliato.- Verifica dei valori di ritorno: funzioni come
strpos()restituiscono0per un riscontro all'inizio efalseper nessun riscontro.if (!strpos(...))confonde le due cose.
Confronti sicuri: la contrapposizione
Gli esempi seguenti mostrano di volta in volta la variante insicura e quella sicura:
<?php
declare(strict_types=1);
// Verificare il ruolo
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // insicuro: lasco
in_array($rolle, $erlaubteRollen, true); // sicuro: rigoroso
// Valutare lo stato
$aktion = match ($status) { // rigoroso, errore con valore sconosciuto
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Verificare il token dal link
if ($tokenAusLink == $gespeichertesToken) { /* non sicuro */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* sicuro */ }
// Verificare la password
if (md5($eingabe) == $hash) { /* non sicuro: algoritmo debole e confronto non rigoroso */ }
if (password_verify($eingabe, $hash)) { /* sicuro */ }
// Cercare una sottostringa
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
Secondo il manuale hash_equals() si aspetta come primo parametro il segreto noto e come secondo il valore dell'utente, e confronta in tempo costante. Come generare i token in sicurezza lo descrive Numeri casuali e token sicuri in PHP.
declare(strict_types=1) non rende rigorosi i confronti. L'istruzione riguarda solo la conversione dei tipi nelle chiamate di funzione e nei valori di ritorno. I confronti laschi deve sostituirli lei stessa nel codice.
I confronti rigorosi hanno un rovescio: falliscono quando i tipi non coincidono. '42' === 42 è falso. I valori di moduli e URL arrivano in PHP come stringhe, quelli del database a seconda di driver e impostazione come stringhe o numeri. Chi introduce ovunque === senza allineare i tipi crea errori che in esercizio si notano come rifiuto di utenti legittimi.
La soluzione è un confine chiaro: gli input vengono validati alla ricezione e portati al tipo giusto, ad esempio con filter_var($wert, FILTER_VALIDATE_INT), e poi nel codice trattati solo con dichiarazioni di tipo e confronti rigorosi. Le dichiarazioni di tipo su parametri, valori di ritorno e proprietà aiutano, perché i tipi sbagliati emergono presto con un messaggio di errore, anziché sparire in silenzio in un confronto.
Verificare sistematicamente il codice esistente
Nei progetti cresciuti nel tempo i confronti laschi si contano a centinaia. Non tutti sono un problema. Si concentri sui punti in cui si decide l'accesso:
# Confronti laschi nelle righe con termini rilevanti per la sicurezza
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array senza confronto rigoroso
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Strumenti di analisi statica come PHPStan o Psalm possono, su richiesta, segnalare stabilmente i confronti laschi. Così i nuovi casi non arrivano nemmeno nel ramo principale. Le basi sui tipi le descrive Il sistema dei tipi di PHP; come portare prima gli input al tipo giusto lo mostra Validare e filtrare gli input in PHP.
Che cosa significa per i sistemi legacy
Le applicazioni che girano ancora su PHP 7 sono interessate dalle vecchie regole di confronto, più lasche. A ciò si aggiunge un secondo problema: per PHP 8.1 e precedenti il progetto PHP non fornisce più aggiornamenti di sicurezza ufficiali, nemmeno contro PHP2Shell. La situazione la descrive PHP 8.1 e precedenti: che cosa vale ora.
Con una migrazione a PHP 8 alcuni risultati dei confronti cambiano. Di solito è un miglioramento, ma può rompere codice che si affidava al vecchio comportamento. Preveda quindi test che coprano in modo mirato i confronti con numeri, valori vuoti e null.
Come aiuta concretamente PHP SOS?
- Cerchiamo nel suo codice i confronti laschi nei punti che decidono su login, permessi o pagamenti.
- Li sostituiamo con confronti rigorosi, espressioni
matche funzioni di verifica adatte. - Configuriamo l'analisi statica in modo che i nuovi confronti laschi emergano nella build.
- Accompagniamo le migrazioni alle versioni attuali di PHP con test per le regole di confronto modificate.
Una verifica del suo software è descritta in audit di sicurezza e penetration test.
Domande frequenti
Che cos'è il type juggling in PHP?
In alcune operazioni PHP converte automaticamente valori di tipo diverso, ad esempio una stringa in un numero. Nel confronto lasco con == questa conversione decide il risultato. È comodo, ma può portare a confronti veri anche se i valori non sembrano uguali.
PHP 8 ha risolto il problema?
In parte. Da PHP 8.0 un numero confrontato con una stringa non numerica viene confrontato come stringa, quindi 0 == 'a' non è più vero. Due stringhe numeriche continuano però a essere confrontate come numeri, e i confronti con true, false e null seguono regole proprie.
declare(strict_types=1) aiuta contro i confronti laschi?
No. strict_types stabilisce se i tipi scalari vengano convertiti automaticamente nelle chiamate di funzione e nei valori di ritorno. Sugli operatori di confronto == e != non ha alcun effetto.
Come confronto correttamente password e token?
Le password si verificano con password_verify, token e firme con hash_equals. Entrambe confrontano in modo esatto e, nel caso di hash_equals, in tempo costante. Un confronto con == o strcmp non è adatto a questo scopo.