PHP et le type juggling : quand la conversion automatique de types devient une faille
PHP est un langage à typage faible. Quand on compare deux valeurs avec l'opérateur ==, PHP essaie de convertir automatiquement les types pour pouvoir les comparer, plutôt que de refuser la comparaison. C'est pratique au quotidien, mais ça devient dangereux dès que cette conversion produit un résultat contre-intuitif sur des données sensibles : mots de passe, hash, tokens, identifiants.
Le cas classique : les magic hashes
Certaines fonctions de hash comme MD5 ou SHA1 produisent parfois des chaînes hexadécimales qui, par pure coïncidence, ressemblent à une notation scientifique. Par exemple :
0e462097431906509019562988736854
En notation scientifique, cette chaîne signifie 0 fois 10 puissance 462. PHP l'interprète donc comme le nombre 0 dès qu'elle est comparée avec == face à une autre valeur numérique ou une autre chaîne du même format.
$hash1 = "0e462097431906509019562988736854";
$hash2 = "0e830400451993494058024219903391";
var_dump($hash1 == $hash2); // true
Les deux chaînes se convertissent en 0, donc 0 == 0 renvoie vrai, alors que ce sont deux hash totalement différents. Une application qui compare un hash stocké en base avec un hash fourni par l'utilisateur via == plutôt que === devient exploitable : il existe des listes de valeurs précalculées qui hashent toutes en 0e..., permettant de contourner une vérification de mot de passe sans connaître le vrai mot de passe.
Le réflexe défensif en PHP
- Toujours utiliser
===pour comparer des hash, tokens ou identifiants sensibles, jamais== - Pour les mots de passe, utiliser
password_verify()plutôt qu'une comparaison manuelle, ce qui évite le problème par construction
Le juggling ne s'arrête pas à PHP
Le principe est transversal : une conversion de type implicite et non désirée sur une donnée sensible peut se produire à n'importe quelle couche d'une application, pas seulement dans le code applicatif.
Une faille récente sur Flarum, le logiciel de forum open source, l'illustre bien. Le bug ne venait pas d'un == mal utilisé en PHP, mais du comportement de l'ORM Eloquent combiné à celui de MySQL.
Les modèles de tokens de réinitialisation de mot de passe utilisaient une clé primaire de type chaîne, mais sans le déclarer explicitement. Par défaut, l'ORM traite donc cette clé comme un entier et caste toute valeur reçue en conséquence. Quand une requête envoie le token sous forme d'entier 0, la requête générée devient WHERE token = 0. Or MySQL, en comparant une colonne texte à un entier, convertit le texte en entier pour la comparaison. Une chaîne qui commence par une lettre se convertit en 0 dans ce contexte, ce qui représente environ 84% des tokens générés aléatoirement.
Résultat : un attaquant peut envoyer {"passwordToken": 0} et matcher un vrai token existant sans jamais le connaître, prenant le contrôle de n'importe quel compte, y compris administrateur.
La leçon à retenir
Le mécanisme racine est identique dans les deux cas : une conversion de type qu'on n'a pas explicitement contrôlée transforme une comparaison censée être stricte en une comparaison qui laisse passer des valeurs qu'elle ne devrait pas. Seule la couche où ça se produit change, le langage applicatif d'un côté, l'ORM et le moteur de base de données de l'autre.
Le réflexe à généraliser : ne jamais laisser un langage, un framework ou une base de données deviner le type d'une donnée sensible à ta place. Déclarer explicitement les types de colonnes et de clés primaires, valider strictement le format attendu côté application avant toute requête, et bannir les comparaisons souples partout où une identité doit être vérifiée.
#PHP #Sécurité #Cybersécurité #DevSecOps