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
- Prévenir les injections SQL
- Se prémunir des injections de commandes
- Prévenir les détournements de sessions
- Ne pas accepter les identifiants de session dans les URL
- Sécuriser les cookies de session avec les flags HttpOnly et Secure
- Renforcer l'entropie des identifiants de session
- Régénérer les identifiants de session lors d'actions critiques
- Ajouter des mesures de sécurité supplémentaires (défense en profondeur)
- Contrer les attaques XSS
- Sécuriser l’upload de fichiers
- Limiter l’accès aux utilisateurs authentifiés
- Restreindre les extensions et formats acceptés
- Configurer un fichier .htaccess pour restreindre l'accès aux fichiers uploadés
- Valider le type MIME et l'extension des fichiers
- Gérer les fichiers de manière sécurisée lors de leur enregistrement
- Limiter la taille des fichiers avec upload_max_filesize
- Se protéger des attaques CSRF (Cross Site Request Forgery)
- Conclusion
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é.
Surveiller les configurations et mises à jour PHP
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.
Configurer les rapports d’erreurs de manière adéquate
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.
Appliquer les principes de sécurité par l’obscurité
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.
Utiliser les requêtes préparées (Prepared Statements)
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.
Valider et assainir les entrées utilisateur
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.
Désactiver les fonctions dangereuses avec disable_functions
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.
Valider strictement les paramètres d’entrée
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.
Échapper les commandes et arguments avec les fonctions appropriées
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.
Ne pas accepter les identifiants de session dans les URL
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
Sécuriser les cookies de session avec les flags HttpOnly et Secure
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
Renforcer l’entropie des identifiants de session
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
Régénérer les identifiants de session lors d’actions critiques
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.
Ajouter des mesures de sécurité supplémentaires (défense en profondeur)
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.
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.
Renforcer la protection avec les HTTP headers
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.
Protection des cookies
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.
Limiter l’accès aux utilisateurs authentifiés
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.
Restreindre les extensions et formats acceptés
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.
Configurer un fichier .htaccess pour restreindre l’accès aux fichiers uploadés
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.
Valider le type MIME et l’extension des fichiers
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érer les fichiers de manière sécurisée lors de leur enregistrement
- 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é.
Limiter la taille des fichiers avec upload_max_filesize
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.
Implémenter des jetons CSRF dans les formulaires
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");
}
Utiliser des bibliothèques ou frameworks anti-CSRF
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