Sécurité PHP : failles, attaques et bonnes pratiques

Introduction

PHP reste le langage de programmation côté serveur le plus populaire. Utilisé par plus de 75% des applications dans le monde, PHP est aujourd’hui dans sa version 8. Cette dernière comme les précédentes apportent toujours de nouvelles fonctionnalités et des couches de sécurité supplémentaires.

Cependant, la sécurité de PHP se construit dès ses fonctionnalités historiques. Dans cet article, nous passerons en revue les bonnes pratiques pour sécuriser vos applications PHP. Nous reviendrons notamment sur les failles et attaques courantes ainsi que sur les défauts de configuration pouvant compromettre la sécurité de vos applications PHP. 

Guide complet sur la sécurité de PHP

Réduire la surface d’attaque de vos applications PHP

Réduire la surface d’attaque d’une application web consiste à minimiser les points d’entrée vulnérables, en limitant les éléments susceptibles d’être exploités par des attaquants.

Cela inclut une gestion rigoureuse des configurations, des mises à jour régulières, la gestion des erreurs, la protection du code source et la sécurité par l’obscurité.

Un des aspects les plus cruciaux de la sécurité est de bien connaître les composants et la configuration de votre serveur web.

Voici quelques questions essentielles à vous poser :

  • Quelle version de PHP est installée ?
  • Quelles librairies sont utilisées ?
  • Quel code externe est inclus, et est-il fiable ?
  • Quel framework est utilisé, et quelle est sa version ?
  • Le système d’exploitation est-il à jour ?

Si vous ne pouvez pas répondre à ces questions de manière claire et précise, il vous manque des informations essentielles pour la sécurité de votre application.

En effet, il est crucial d’avoir une liste centralisée et à jour des composants de votre infrastructure. Cela vous permet de répondre à deux questions clés :

  • Ai-je vraiment besoin de ce composant ? Si ce n’est pas le cas, il doit être supprimé. Tout composant non utilisé représente une surface d’attaque inutile.
  • Comment savoir si une mise à jour est disponible pour ce composant ? Il est indispensable d’avoir des process en place pour suivre les mises à jour, via des mailing lists, des notifications, etc.

La configuration de la sécurité doit être un processus continu. Cela semble évident au début d’un projet, mais après plusieurs mois en production, cette tâche est souvent négligée.

Ainsi, il est essentiel de vérifier régulièrement vos configurations et vos mises à jour pour limiter les risques.

Les erreurs affichées sont précieuses pour le développement. Cependant, elles peuvent également exposer des informations sensibles lorsqu’elles sont visibles en production. Il est donc primordial de configurer les rapports d’erreurs de manière adéquate.

Voici trois paramètres importants du fichier php.ini :

  • error_reporting : contrôle quels types d’erreurs sont rapportés.
  • display_errors : définit si les erreurs doivent être affichées à l’écran.
  • log_errors : active la journalisation des erreurs dans un fichier de log.

Sur un environnement de développement, il est acceptable d’afficher les erreurs à l’écran, mais en production, cela doit être strictement interdit.

À la place, activez la journalisation des erreurs et choisissez soigneusement les types d’erreurs à enregistrer, particulièrement sur les sites à fort trafic, où les logs peuvent devenir volumineux.

Le paramètre error_reporting peut être configuré ainsi :

error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT

En production, vous pouvez configurer display_errors à « Off » et log_errors à « On » pour un meilleur équilibre entre sécurité et débogage.

En savoir plus sur les configurations des rapports d’erreurs.

Par ailleurs, d’autres paramètres de sécurité dans php.ini doivent être revus régulièrement, comme :

  • disable_functions : désactive certaines fonctions potentiellement dangereuses.
  • memory_limit : limite la quantité de mémoire qu’un script peut utiliser.
  • max_execution_time : empêche les scripts de tourner indéfiniment.
  • file_uploads et upload_max_filesize : contrôlent les autorisations et limites des téléchargements de fichiers.

Bien que la sécurité par l’obscurité ne soit pas une stratégie complète en soi, elle peut ralentir les attaquants et leur compliquer la tâche.

Par exemple :

  • Cacher la version de PHP : empêche les attaquants de connaître la version exacte de PHP utilisée. Cela peut être fait en désactivant expose_php dans le fichier php.ini : expose_php = Off.
  • Messages d’erreur de login génériques : lors de l’échec d’une tentative de connexion, ne précisez pas si c’est le mot de passe ou le nom d’utilisateur qui est incorrect. Utilisez un message générique comme « Identifiants incorrects » pour ne pas révéler qu’un identifiant valide a été trouvé.

Ces techniques ne remplacent pas les mesures de sécurité fondamentales, mais elles compliquent la tâche des attaquants en rendant plus difficile la collecte d’informations exploitables.

Prévenir les injections SQL

Les injections SQL sont des attaques redoutables qui peuvent compromettre gravement la sécurité de votre application PHP. Heureusement, il est possible de s’en protéger en suivant quelques bonnes pratiques simples.

Le moyen le plus efficace de prévenir les injections SQL est d’utiliser les requêtes préparées, disponibles via l’extension PDO de PHP.

Les requêtes préparées permettent de séparer la structure de la requête SQL des données qui lui sont transmises, empêchant ainsi l’exécution de code SQL malveillant.

Voici un exemple d’implémentation avec PDO :

$pdo = new PDO('mysql:host=localhost;dbname=test', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username');
$stmt->bindParam(':username', $username);
$stmt->execute();

Les paramètres envoyés dans la requête sont traités comme des données, et non comme du code, bloquant ainsi toute tentative d’injection malveillante.

Même avec des requêtes préparées, il est important de valider et assainir les entrées utilisateur. Utilisez des fonctions adaptées à vos besoins pour vérifier le type et le format des données avant de les utiliser dans une requête SQL.

Cela inclut l’utilisation de filtres PHP tels que filter_var() pour valider des adresses email ou des entiers.

Se prémunir des injections de commandes

L’exécution de commandes via PHP peut être extrêmement puissante, mais elle présente aussi un risque élevé si des précautions ne sont pas prises.

Une mauvaise gestion peut permettre à des attaquants d’injecter des commandes malveillantes sur votre serveur, compromettant ainsi la sécurité de votre application.

Voici les bonnes pratiques à suivre.

La première mesure pour limiter les risques d’injection de commandes est de désactiver les fonctions PHP potentiellement dangereuses.

Cela peut être fait via la directive disable_functions dans le fichier php.ini. Une fois ces fonctions désactivées, elles ne pourront pas être exploitées, même par un attaquant qui réussirait à injecter du code PHP via une autre faille de sécurité.

Voici un exemple de configuration :

disable_functions = show_source, exec, shell_exec, system, passthru, proc_open, popen, curl_exec, curl_multi_exec, parse_ini_file, show_source

Ces fonctions sont couramment utilisées pour exécuter des commandes système, ouvrir des processus ou accéder à des informations sensibles. Leur désactivation empêche les abus en cas de faille.

Si vous devez absolument utiliser des fonctions permettant l’exécution de commandes (comme exec() ou system()), vous devez être extrêmement vigilant quant aux paramètres qui leur sont transmis.

Validez toutes les données d’entrée en utilisant des listes blanches (whitelists), c’est-à-dire, en acceptant uniquement des valeurs pré-approuvées. Cela réduit la probabilité qu’une entrée malveillante soit exécutée comme une commande.

Lorsque vous exécutez des commandes, il est essentiel d’échapper correctement les arguments pour éviter les tentatives d’injection.

PHP fournit des fonctions natives pour sécuriser l’exécution des commandes.

  • escapeshellcmd() : Échappe les caractères spéciaux dans une commande pour empêcher leur interprétation comme des opérateurs shell. Utilisez cette fonction pour sécuriser la commande dans son ensemble.
  • escapeshellarg() : Échappe les arguments passés à une commande pour les traiter comme des chaînes littérales. Cela empêche l’injection de caractères spéciaux ou de commandes supplémentaires.

Ces fonctions permettent d’éviter que des utilisateurs malveillants n’injectent des commandes supplémentaires ou ne modifient la commande exécutée via des entrées mal formées.

Prévenir les détournements de sessions

Les détournements de sessions reposent souvent sur l’exploitation des identifiants de session pour accéder à la session d’un utilisateur légitime.

Une attaque courante est la fixation de session, où l’attaquant force la victime à utiliser un identifiant de session connu de l’attaquant, pour ensuite exploiter cette session une fois que la victime s’est connectée.

Pour prévenir ce type d’attaques, il est essentiel de suivre plusieurs bonnes pratiques.

L’usage des identifiants de session dans les URL facilite la capture ou la réutilisation de ces informations par des attaquants, notamment via des attaques de type Man in the Middle.

Pour éviter cela, configurez PHP pour n’accepter les identifiants de session que via des cookies. Cela peut être activé en définissant le paramètre session.use_only_cookies dans le fichier php.ini :

session.use_only_cookies = 1

Les cookies de session doivent être protégés contre les attaques telles que le cross-site scripting (XSS).

Deux mesures importantes à prendre :

  • Flag HttpOnly : Empêche l’accès aux cookies via JavaScript, limitant ainsi les risques liés aux failles XSS.
  • Flag Secure : Assure que les cookies ne sont envoyés que via des connexions sécurisées (HTTPS).

Ces paramètres peuvent être activés dans le fichier php.ini :

session.cookie_httponly = 1
session.cookie_secure = 1

Les identifiants de session générés doivent être suffisamment complexes pour rendre leur prédiction difficile. En augmentant l’entropie des identifiants, vous réduisez les risques d’attaques brute force.

Sur les systèmes Linux, il est recommandé d’utiliser le fichier spécial /dev/urandom pour générer des identifiants de session avec un niveau d’entropie élevé :

session.entropy_file = /dev/urandom

Une mesure cruciale pour limiter les attaques de fixation de session est de régénérer l’identifiant de session lorsque l’utilisateur effectue une action sensible, telle que la connexion.

Cela empêche un attaquant d’exploiter une session fixée avant que l’utilisateur ne se soit authentifié.

PHP fournit une fonction simple pour régénérer l’identifiant de session :

session_regenerate_id(true); // true pour supprimer l'ancien identifiant

Il est recommandé de régénérer l’identifiant à des moments critiques, comme :

  • Après la connexion de l’utilisateur.
  • Lors de changements de privilèges ou d’accès à des informations sensibles.

Pour renforcer davantage la sécurité de vos sessions, plusieurs stratégies complémentaires peuvent être mises en place :

  • Surveiller l’activité des utilisateurs : Enregistrez les actions importantes effectuées dans la session de chaque utilisateur. Vous pouvez par exemple conserver une trace des dernières interactions pour détecter des comportements suspects, comme des tentatives d’accès à des ressources sensibles sans actions préalables cohérentes.
  • Vérifier le « User-Agent » : Stockez le User-Agent (l’empreinte du navigateur) de l’utilisateur dans la session, puis comparez-le lors des actions sensibles. Si l’empreinte du navigateur change subitement, cela pourrait indiquer une prise de contrôle de la session. Ci-dessous un exemple de mise en oeuvre :
if (!isset($_SESSION['user_agent'])) {
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
} elseif ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
    die("User-Agent mismatch. Possible session hijacking attempt.");
}

Contrer les attaques XSS

Protéger votre application PHP contre les attaques XSS (Cross-Site Scripting) est essentiel, mais relativement simple si vous suivez des bonnes pratiques rigoureuses.

Le principe fondamental à appliquer est : filtrer les entrées et échapper les sorties.

Chaque donnée provenant de l’utilisateur, qu’elle provienne de formulaires, d’URL (GET), de requêtes POST, de cookies ou même des headers HTTP, doit être validée et filtrée avant d’être traitée par votre application. Cela inclut les champs cachés et toutes les entrées de l’utilisateur, qu’elles soient visibles ou non.

  • Filtrage à l’entrée : Validez et assainissez toutes les données utilisateurs dès qu’elles sont reçues. Utilisez des filtres PHP comme filter_var() pour assurer que les données correspondent à vos attentes (par exemple, un email, un entier ou une URL).
  • Échappement à la sortie : Avant d’afficher des données dynamiques sur votre site, échappez-les correctement pour empêcher l’injection de scripts malveillants. Utilisez les fonctions PHP telles que htmlspecialchars() pour neutraliser les caractères spéciaux comme « <« , « > », et « & ».

Ces mesures simples peuvent déjà empêcher la plupart des attaques XSS classiques.

Une protection supplémentaire contre les attaques XSS peut être implémentée via les headers HTTP. Des headers comme Content-Security-Policy (CSP) peuvent limiter l’exécution de scripts non approuvés ou détecter des attaques potentielles.

En effet Content-Security-Policy permet de définir quelles sources de scripts sont approuvées et autorisées à s’exécuter. Par défaut, il empêche l’exécution de scripts injectés via XSS.

Les cookies ne doivent jamais être utilisés pour stocker des informations sensibles, car ils sont vulnérables à des manipulations par l’utilisateur ou des attaques comme le XSS.

Voici quelques bonnes pratiques pour sécuriser l’utilisation des cookies :

  • Ne stockez pas d’informations sensibles : Les cookies ne sont pas sûrs par nature. Évitez de les utiliser pour des données confidentielles ou critiques.
  • Signer les cookies : Pour protéger les cookies contre les manipulations, une méthode efficace est de les signer en calculant un hash que vous stockez dans le cookie lui-même. Lorsque le cookie est renvoyé au serveur, celui-ci peut vérifier son intégrité en recalculant le hash.
  • Utiliser les attributs de sécurité : Comme indiqué plus haut sur le détournement de session; activez les attributs HttpOnly et Secure pour vos cookies.

Exemple d’attributs pour un cookie :

setcookie('user_session', $value, [
    'expires' => time() + 3600,
    'path' => '/',
    'domain' => 'example.com',
    'secure' => true,    // Cookie envoyé uniquement via HTTPS
    'httponly' => true,  // Inaccessible via JavaScript
    'samesite' => 'Strict' // Empêche les envois de cookies par des requêtes cross-site
]);

Sécuriser l’upload de fichiers

L’upload de fichiers est une fonctionnalité particulièrement sensible. Si elle est mal sécurisée, elle permet à des attaquants d’envoyer des fichiers malveillants sur votre serveur, y compris des scripts PHP, pouvant entraîner des prises de contrôle complètes du serveur.

Il est donc essentiel de mettre en place une stratégie de sécurité robuste.

Ne permettez l’upload de fichiers qu’aux utilisateurs ayant préalablement été authentifiés. Cela limite les tentatives d’attaques provenant d’utilisateurs non fiables ou anonymes.

Autorisez uniquement les types de fichiers strictement nécessaires à vos fonctionnalités (ex. : images, documents).

Mettez en place une liste blanche des extensions de fichiers et des formats acceptés, tels que .jpg, .png, ou .pdf. Refusez tout autre format.

Dans le répertoire où sont stockés les fichiers uploadés, placez un fichier .htaccess pour empêcher l’exécution de scripts malveillants.

Vous pouvez limiter l’accès uniquement aux types de fichiers autorisés :

deny from all
<Files ~ "\.(gif|jpe?g|png|pdf)$">
    order deny,allow
    allow from all
</Files>

Cette configuration empêche l’exécution de fichiers PHP ou tout autre script qui pourrait être téléchargé de manière malveillante.

Ne vous fiez pas uniquement à l’extension de fichier pour valider son type. Utilisez une validation stricte du type MIME, en vérifiant que le type du fichier correspond à un des formats autorisés.

Par exemple, pour valider une image, utilisez des fonctions comme mime_content_type() ou getimagesize().

  • Générer des noms de fichiers uniques : Évitez d’utiliser les noms de fichiers fournis par les utilisateurs.
  • Stocker les fichiers hors du dossier racine : Pour éviter l’exécution accidentelle de fichiers malveillants, stockez les fichiers uploadés en dehors du répertoire racine de votre site (par exemple, dans un répertoire dédié hors du répertoire public « www » ou « htdocs »).
  • Révoquer les permissions d’exécution : Après avoir enregistré le fichier, utilisez chmod() pour désactiver les permissions d’exécution, empêchant ainsi tout code d’être exécuté.

Vous pouvez configurer la taille maximale des fichiers à l’échelle globale via le fichier php.ini, en ajustant le paramètre upload_max_filesize.

Si vous souhaitez définir une limite plus stricte pour un formulaire particulier, vous pouvez utiliser le paramètre MAX_FILE_SIZE dans votre formulaire HTML. Cependant, cette limite doit toujours être vérifiée côté serveur, car elle peut être modifiée par l’utilisateur avant la soumission du formulaire.

Exemple de vérification côté serveur :

if ($_FILES['file']['size'] > 1048576) { // 1MB
    die("Fichier trop volumineux.");
}

Se protéger des attaques CSRF (Cross Site Request Forgery)

Les attaques CSRF exploitent la confiance qu’un site accorde à un utilisateur authentifié, en forçant celui-ci à exécuter des actions non désirées. Pour se protéger.

Utiliser les requêtes POST pour les actions sensibles

La première étape pour réduire les risques liés aux attaques CSRF est de s’assurer que toutes les actions impliquant des modifications d’état (comme la création, la modification ou la suppression de données) sont effectuées via des requêtes POST, et non via des requêtes GET.

Bien que cela ne supprime pas complètement le risque d’attaques CSRF, cela réduit les attaques de base où un attaquant pourrait exploiter une requête GET (comme un simple clic sur un lien malveillant) pour déclencher une action.

La méthode la plus efficace pour se protéger des attaques CSRF est l’utilisation de jetons CSRF.

Ces jetons sont des valeurs uniques générées par le serveur et associées à la session de l’utilisateur. Lorsqu’un formulaire est soumis, le serveur compare le jeton envoyé avec celui stocké dans la session de l’utilisateur pour valider l’authenticité de la requête. Si les jetons ne correspondent pas, l’action est rejetée.

Voici comment implémenter un jeton CSRF simple.

Générer un jeton à la création d’un formulaire :

$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

Ajouter le jeton au formulaire en tant que champ caché :

<form method="POST" action="/submit">
    <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
    <!-- autres champs -->
    <button type="submit">Soumettre</button>
</form>

Vérifier le jeton côté serveur lors de la soumission :

if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die("Invalid CSRF token");
}

Si vous travaillez avec un framework, de nombreuses solutions modernes incluent déjà des protections CSRF intégrées.

Par exemple, Laravel, Symfony ou Django génèrent et vérifient automatiquement les tokens CSRF dans les formulaires.

Si vous ne travaillez pas avec un framework offrant cette protection nativement, vous pouvez soit implémenter les jetons vous-même (ce qui est déconseillé), soit utiliser des bibliothèques tierces.

Conclusion

Adopter des pratiques de sécurité robustes est essentiel pour protéger les applications PHP contre les menaces et les vulnérabilités.

Les diverses attaques courantes peuvent être anticipées et réduites par une mise en œuvre systématique de bonnes pratiques de sécurité. Toutefois, ces mesures de sécurité peuvent être complexes et chronophages à mettre en œuvre manuellement dans chaque projet.

C’est ici que les frameworks modernes apportent une solution avantageuse pour les développeurs. En effet, intégrer un framework offre de nombreux avantages :

  • Fonctionnalités de sécurité intégrées : Les frameworks modernes offrent souvent des protections intégrées contre les failles courantes. Par exemple, l’utilisation de requêtes préparées pour contrer les injections SQL, le filtrage automatique des entrées pour éviter les attaques XSS, ou encore la gestion de tokens anti-CSRF.
  • Maintenance simplifiée : Les frameworks bénéficient d’une communauté active de développeurs qui fournissent régulièrement des mises à jour, corrigeant les vulnérabilités identifiées et assurant une amélioration continue des standards de sécurité. Cela permet de réduire considérablement le risque d’exploits issus de composants obsolètes.
  • Gain de temps et réduction des coûts : En automatisant de nombreux aspects de la sécurité et en offrant des bibliothèques prêtes à l’emploi, les frameworks permettent aux équipes de se concentrer sur les spécificités métiers, sans devoir développer from scratch des solutions de sécurité. Ce gain de temps se traduit également par une optimisation des ressources et des coûts de développement.
  • Meilleures pratiques consolidées : Les frameworks sont conçus autour de principes éprouvés et testés. Ils intègrent des architectures solides qui favorisent un développement modulable et sécurisé, limitant les erreurs humaines et facilitant la maintenance du code sur le long terme.

Auteur : Amin TRAORÉ – CMO @Vaadata