Skip to Content

Chapitre 9.1 — Pièges PHP classiques

⏱️ TL;DR — PHP moderne est un langage correct, mais il traîne des pièges qui, mal maîtrisés, créent des failles. Les comparaisons laxistes (==) font des rapprochements surprenants ("0e123" == "0e456" peut être vrai — type juggling) : pour tout ce qui touche à la sécurité (tokens, hashes), on compare avec === ou hash_equals(). Les superglobales ($_GET, $_POST, $_REQUEST, $_COOKIE) sont des entrées non fiables — on valide (filter_var), on ne fait pas confiance ; $_REQUEST mélange les sources (piège). Des fonctions comme extract() sur des entrées peuvent écraser des variables. Et en prod, display_errors = Off : une erreur affichée est une fuite. Le PHP historique (WordPress, vieux scripts) concentre ces risques.

🎯 Objectifs

  • Comprendre le type juggling et comparer les secrets de façon sûre (===, hash_equals).
  • Traiter les superglobales comme des entrées hostiles (validation, pas $_REQUEST).
  • Éviter les fonctions dangereuses sur des entrées (extract, eval, variables variables).
  • Masquer les erreurs en production et configurer php.ini sainement.

Le type juggling : == te trahit

PHP a un typage dynamique et des comparaisons laxistes (==) qui convertissent les types avant de comparer. Cela produit des résultats contre-intuitifs, dangereux quand on compare des secrets :

// ❌ Comparaisons laxistes surprenantes (type juggling) var_dump(0 == "abc"); // selon la version : peut etre false en PHP 8, true avant var_dump("1e3" == "1000"); // true (compares comme des nombres) var_dump("0e111" == "0e222"); // true (deux "zero en notation exponentielle")

Le dernier cas est un vrai vecteur : certains hashes commencent par 0e suivis de chiffres (« magic hashes »). Comparer deux tels hashes avec == les traite comme 0 == 0vrai, ce qui a permis des contournements d’authentification dans du code qui comparait des empreintes avec ==.

// ❌ Comparaison de hash/token avec == : vulnerable au type juggling if ($_POST['token'] == $tokenAttendu) { /* ... */ } // ✅ Comparaison STRICTE de type, ou temps constant pour un secret if ($_POST['token'] === $tokenAttendu) { /* ... */ } // strict : type + valeur if (hash_equals($tokenAttendu, $_POST['token'])) { /* ... */ } // + temps constant (anti-timing)

⚠️ Piège — Ne compare jamais un token, un hash, une signature ou un secret avec ==. Utilise === (comparaison stricte, sans conversion de type) et, pour un secret comparé à une valeur fournie par l’utilisateur, hash_equals() (qui ajoute la comparaison en temps constant contre les attaques temporelles — rappel Partie 3). Le == sur des secrets est une faille classique de contournement.

Les superglobales sont des entrées hostiles

$_GET, $_POST, $_COOKIE, $_SERVER (dont $_SERVER['HTTP_...']), $_FILES : toutes ces superglobales contiennent des données contrôlées par le client, donc hostiles (rappel Partie 1). On ne les utilise jamais brutes dans une requête SQL, une commande, du HTML ou une inclusion — on valide et on paramètre.

// ✅ Valider/filtrer les entrees $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL); // false si invalide $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); // int ou false if ($email === false || $id === false) { /* rejeter */ }

Piège spécifique : $_REQUEST fusionne $_GET, $_POST et $_COOKIE. Un attaquant peut donc injecter une valeur via un canal inattendu (ex. un cookie qui prend le pas sur un POST) et l’ordre de priorité dépend de la config. Évite $_REQUEST : utilise la source précise attendue ($_POST pour un formulaire, $_GET pour un paramètre d’URL). En Symfony, on ne touche d’ailleurs pas aux superglobales : on passe par l’objet Request ($request->request->get(...), $request->query->get(...)), plus explicite.

Fonctions dangereuses sur des entrées

Certaines constructions PHP deviennent des failles dès qu’une entrée les alimente :

  • extract($_GET) / extract($_POST) : crée des variables locales à partir des clés du tableau → un attaquant peut écraser des variables (?isAdmin=1 crée $isAdmin). À bannir sur des entrées.
  • Variables variables ($$nom) pilotées par l’entrée : même risque d’écrasement/confusion.
  • eval(), assert() (avec une chaîne), create_function : exécutent du code → RCE si une entrée y entre. À proscrire.
  • include/require avec une entrée : inclusion de fichiers (LFI/RFI — chapitre 9.2).
  • unserialize() sur une entrée : désérialisation dangereuse (chapitre 9.2).
  • Fonctions système (system, exec, backticks) avec une entrée : command injection (Partie 6).
// ❌ extract sur une entree : ecrase des variables locales extract($_GET); // ?role=admin cree $role = "admin" if ($role === 'admin') {} // pilote par l'attaquant // ✅ Lire explicitement la valeur attendue, validee $role = filter_input(INPUT_GET, 'role', FILTER_SANITIZE_SPECIAL_CHARS);

Masquer les erreurs en production

Rappel de la Partie 6 appliqué à PHP : en production, les erreurs détaillées ne doivent jamais s’afficher à l’utilisateur (elles révèlent chemins, versions, requêtes, parfois secrets). Réglage php.ini :

; production display_errors = Off ; ne PAS afficher les erreurs au client log_errors = On ; les envoyer dans un log serveur error_reporting = E_ALL ; tout logger (cote serveur) expose_php = Off ; ne pas annoncer "PHP" dans l'en-tete Server

En développement, on peut activer display_errors — mais jamais en prod. En Symfony, c’est géré par APP_ENV (voir chapitre 9.5) : prod désactive le profiler et les pages d’erreur détaillées.

Le PHP historique : là où ça fait mal

Les pièges ci-dessus sont surtout dangereux dans le PHP ancien ou artisanal : vieux scripts, thèmes/plugins WordPress mal écrits, code sans framework. On y trouve encore des mysql_query("... $_GET ...") concaténés, des == sur des mots de passe, des include $_GET['page']. Si tu maintiens ce type de code (cf. le cours WordPress), applique les correctifs des Parties 6-8 : paramétrer, valider, échapper en sortie, et migrer vers PDO/un framework quand c’est possible.

💡 Réflexe — Trois réflexes PHP : (1) ===/hash_equals pour comparer quoi que ce soit de sensible (jamais ==) ; (2) toute superglobale = entrée hostile à valider (filter_input), et jamais $_REQUEST ; (3) aucune entrée dans eval/extract/include/unserialize/system. En Symfony, tu passes par Request et les composants — ce qui t’éloigne naturellement de ces pièges.

🧭 Sur FormaCampus — Le back de FormaCampus est en Symfony (donc à l’abri de la plupart de ces pièges par construction : objet Request, Doctrine, composant Security). Mais l’équipe maintient aussi quelques scripts PHP hérités (imports batch) et un Moodle (PHP). Sur ces surfaces, les règles sont explicites : ===/hash_equals pour tout secret, filter_input sur les superglobales, $_REQUEST proscrit, aucune entrée dans include/unserialize/eval, et display_errors = Off en prod. Les vieux scripts sont progressivement migrés vers PDO préparé et intégrés au framework.

✏️ Exercices

Exercice 1 — Le token contourné. Ce code vérifie un token de réinitialisation : if ($_GET['t'] == $user->getResetToken()). Quel est le risque et comment corriger ?

✅ Solution

Le == est vulnérable au type juggling : si les deux valeurs ressemblent à des « nombres » PHP (ex. des hashes 0e... — magic hashes), la comparaison peut renvoyer vrai à tort, permettant un contournement du token (l’attaquant valide sans connaître le vrai token). De plus, == non-strict et non temps-constant fuit potentiellement de l’information de timing. Correctif : utiliser hash_equals($user->getResetToken(), $_GET['t']) — comparaison stricte de type et en temps constant. (Idéalement, le token de reset est aléatoire CSPRNG, stocké haché côté serveur, à usage unique et à expiration courte — Parties 3-4.)

Exercice 2 — extract dangereux. Explique le risque de extract($_POST) en tête d’un contrôleur, avec un exemple d’abus.

✅ Solution

extract($_POST) crée des variables locales à partir des clés du tableau $_POST, toutes contrôlées par l’attaquant. Il peut donc écraser des variables du script en ajoutant des champs : par exemple, si le code s’attend à calculer $isAdmin plus loin, l’attaquant envoie isAdmin=1 et $isAdmin vaut déjà "1" avant tout contrôle ; ou il écrase $user, $prix, $config… Cela crée des failles d’autorisation et de logique imprévisibles. Parade : ne jamais extract() une entrée. Lire explicitement chaque valeur attendue et la valider (filter_input), ou passer par l’objet Request de Symfony. On contrôle quelles variables existent, pas l’attaquant.

🧠 Quiz de révision

1. Qu’est-ce que le type juggling et pourquoi est-il dangereux ?

La conversion implicite de types par les comparaisons laxistes (==) de PHP, qui produit des égalités surprenantes ("0e11" == "0e22" → vrai). Dangereux car comparer des secrets/hashes avec == peut renvoyer vrai à tort (magic hashes) → contournement d’authentification. On utilise ===/hash_equals.

2. Comment comparer un secret/token de façon sûre en PHP ?

Avec === (comparaison stricte : type et valeur, pas de conversion) et, pour un secret comparé à une entrée utilisateur, hash_equals() qui ajoute la comparaison en temps constant (anti-attaque temporelle). Jamais ==.

3. Pourquoi éviter $_REQUEST ?

Parce qu’il fusionne $_GET, $_POST et $_COOKIE : un attaquant peut injecter une valeur par un canal inattendu (ex. cookie) et l’ordre de priorité dépend de la config. On lit la source précise attendue ($_POST/$_GET), ou l’objet Request en Symfony.

4. Cite trois fonctions PHP à ne jamais alimenter avec une entrée.

Par ex. : extract() (écrase des variables), eval()/assert() avec une chaîne (RCE), include/require avec une entrée (LFI/RFI), unserialize() (désérialisation dangereuse), system/exec (command injection). Aucune entrée utilisateur ne doit y entrer.

5. Quel réglage php.ini est crucial en production, et pourquoi ?

display_errors = Off (avec log_errors = On) : les erreurs détaillées ne doivent jamais s’afficher au client (elles révèlent chemins, versions, requêtes, secrets — aide à l’attaque, surtout aux injections aveugles). On loggue côté serveur. Bonus : expose_php = Off.


Chapitre suivant : Uploads, LFI & désérialisation — trois vecteurs PHP redoutables : le fichier uploadé qui s’exécute, l’inclusion de fichiers, et unserialize sur une entrée.

Last updated on