Symptôme : un client nous transfère une capture d’écran envoyée par un visiteur inquiet, montrant une page blanche avec un bloc de texte inhabituel en haut : Warning: file_get_contents(/home/clientweb42/public_html/wp-content/plugins/extension-maison/data/config.json): failed to open stream. Le visiteur s’inquiétait à raison, même sans comprendre pourquoi. Le message expose sans le vouloir le chemin absolu du serveur, le nom d’utilisateur système probable (clientweb42), et l’organisation des dossiers de l’installation.
Ce n’est ni une faille exotique ni un exploit complexe. C’est une conséquence directe d’une configuration PHP pensée pour le développement, jamais nettoyée avant la mise en ligne. Et si l’incident en lui-même n’a permis aucune intrusion, il illustre un principe fondamental de sécurité défensive : toute information inutile donnée gratuitement à un visiteur est une information qu’un attaquant n’aura pas eu à chercher.
Diagnostic : d’où vient l’affichage
Le comportement d’affichage des erreurs PHP est piloté par deux réglages distincts, souvent confondus :
display_errors, dans la configuration PHP (php.iniou directives équivalentes), qui contrôle si les erreurs s’affichent directement dans la réponse HTTP envoyée au navigateur.WP_DEBUG_DISPLAY, une constante WordPress qui, lorsqu’elle vauttrueen complément deWP_DEBUG, force également cet affichage indépendamment de la configuration PHP globale.
Sur ce site, l’investigation a révélé les deux causes cumulées : un wp-config.php hérité d’un environnement de développement avec define( 'WP_DEBUG', true ) jamais retiré au passage en production, et un hébergement mutualisé dont le php.ini par défaut laissait display_errors activé pour tous les sites hébergés.
// Trouvé tel quel dans wp-config.php en production
define( 'WP_DEBUG', true );
// WP_DEBUG_DISPLAY non défini : WordPress prend alors la valeur de WP_DEBUG,
// donc true également, et affiche les erreurs à l'écran.
Pourquoi un chemin absolu est une information précieuse pour un attaquant

Un chemin comme /home/clientweb42/public_html/wp-content/plugins/extension-maison/ ne permet évidemment pas, à lui seul, de compromettre un serveur. Mais il alimente plusieurs étapes de reconnaissance qu’un attaquant mène habituellement à l’aveugle :
- Il confirme le type d’hébergement (mutualisé, avec un nom d’utilisateur système visible, souvent identifiable par le préfixe utilisé par l’hébergeur) et parfois l’hébergeur lui-même.
- Il révèle la présence et le nom exact d’extensions personnalisées (« extension-maison ») qui ne figurent dans aucun dépôt public, donc jamais auditées ni corrigées par une communauté, une cible de choix pour chercher des failles spécifiques.
- Il peut être combiné à d’autres vulnérabilités, par exemple une inclusion de fichier local (LFI), où connaître le chemin absolu exact est parfois la seule information manquante pour transformer une faille théorique en exploitation réelle.
Le correctif : séparer la journalisation de l’affichage
La règle à retenir est simple et ne souffre aucune exception en production : les erreurs doivent être enregistrées pour que l’équipe technique puisse les consulter, jamais affichées à l’écran d’un visiteur. WordPress permet exactement cette séparation :
// wp-config.php — configuration correcte pour un environnement de production
define( 'WP_DEBUG', true ); // Active la détection des erreurs
define( 'WP_DEBUG_LOG', true ); // Écrit les erreurs dans wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // N'affiche jamais rien à l'écran
@ini_set( 'display_errors', 0 ); // Ceinture et bretelles côté PHP directement
Le fichier wp-content/debug.log ainsi généré doit lui-même être protégé : son emplacement est prévisible, et un attaquant qui le trouve accessible publiquement récupère instantanément tous les avertissements PHP historiques du site, souvent tout aussi bavards que l’erreur affichée à l’écran. Un blocage par configuration serveur ferme cette seconde porte :
# .htaccess dans wp-content/, ou bloc équivalent Nginx
<Files "debug.log">
Require all denied
</Files>
Vérifier au-delà de WordPress lui-même
La configuration WordPress ne suffit pas si le php.ini du serveur ou de l’hébergement impose ses propres réglages en amont. Une vérification directe, sans dépendre de WordPress, permet de s’assurer que rien ne fuit avant même que le CMS n’entre en jeu :
php -i | grep -E "display_errors|log_errors|error_log"
# display_errors doit être "Off" (ou 0) en production
# log_errors doit être "On"
# error_log doit pointer vers un fichier accessible en écriture, hors webroot idéalement
Sur un hébergement mutualisé où le fichier php.ini global n’est pas modifiable, un fichier .user.ini déposé à la racine du site permet généralement de surcharger localement ces directives sans droits d’administration serveur.
Prévention : un contrôle systématique avant chaque mise en production
Depuis cet incident, notre checklist de mise en production inclut une vérification automatisée : une requête vers une URL volontairement inexistante ou vers un paramètre malformé, suivie d’une recherche de motifs révélateurs (Warning:, Fatal error:, /home/, /var/www/) dans la réponse HTML reçue. Un script de quelques lignes suffit à automatiser ce test avant chaque déploiement :
REPONSE=$(curl -s "https://exemple.fr/?param[]=test-erreur-volontaire")
if echo "$REPONSE" | grep -qE "Warning:|Fatal error:|/home/|/var/www/"; then
echo "ALERTE : messages d'erreur PHP exposés en production"
exit 1
fi
Sur nos mises en production, cette vérification prend deux secondes et a déjà bloqué deux déploiements où
WP_DEBUG_DISPLAYétait resté actif par copier-coller d’un ancienwp-config.php.
En résumé
Ce cas ne traite volontairement pas des fuites liées au numéro de version de WordPress ou à un fichier readme.html laissé en place, deux sources de divulgation différentes traitées ailleurs. Il rappelle un principe plus large : chaque environnement a sa configuration propre, et un réglage de confort en développement devient systématiquement un risque en production dès qu’il touche à l’affichage d’informations techniques.