Fichiers .php.jpg dans les uploads : pourquoi c'est grave
Un `facture.php.jpg` dans les médias n'est pas une image mal nommée. Selon la config, le serveur exécute le PHP malgré l'extension image. Interdire l'exécution dans les dossiers d'upload est le correctif le moins coûteux une fois l'incident constaté — et souvent le plus oublié.
Le serveur exécute parfois le PHP malgré l'extension image. Interdire l'exécution dans les dossiers médias est le correctif le moins coûteux après incident.
Pourquoi `.php.jpg` passe encore
Apache, selon les `AddHandler` et l'ordre des extensions, peut traiter `fichier.php.jpg` comme du PHP. Certains hôtes historiques le font encore. Nginx est plus strict par défaut, pas toujours : un `location ~ \.php` trop large rattrape. Le détail est chez l'hébergeur ; le risque, chez vous, dès qu'un upload écrit un PHP.
WordPress interdit en théorie les uploads PHP. Un plugin file manager, un champ « non filtré », un bug de validation, un admin fantôme : le fichier est là. La médiathèque peut même afficher une icône image si le `guid` finit en `.jpg`.
Ce n'est pas un cas d'école. C'est le vecteur d'entrée le plus banal après le mot de passe faible : on upload une « image », on l'appelle en URL, on a un shell.
Lister autrement que par la médiathèque
La médiathèque filtre les images et pagin. Passez par SFTP sur `wp-content/uploads/` (ou `img/`, `media/` Presta). Recherche `*.php*` dans l'arbre. Les `.phtml`, `.php5`, `.phar` aussi.
Exportez la liste nom / chemin / date. Les fichiers du jour de l'incident d'abord. Un `2024/01/` plein de PHP alors que vous n'uploadez plus depuis 2023 : quelqu'un écrit encore.
Les pièces jointes WordPress (`attachment` dans `wp_posts`) : un `guid` vers un `.php.jpg`. La SQL complète la recherche disque. Voir aussi articles inconnus pour les attachments déguisés.
- Recherche `*.php*` sous uploads.
- `.phtml`, `.php5`, `.phar`, `.inc`.
- `guid` en base.
- Même passe sur les dossiers « import » et « backup ».
Noms fréquents et doubles extensions
`.php.jpg`, `.php.png`, `.php.gif`, `.php.webp`, `.jpg.php`, `.png.php`. L'ordre change selon le handler. Cherchez les deux. Un `.phtml.jpg` apparaît sur les hôtes qui exécutent phtml.
Des noms `image4.php.jpg`, `favicon.php.ico`, `doc.php.pdf`. Le PDF n'est pas exécuté comme PHP en général ; le `.php.pdf` peut l'être si le handler est naïf. En doute, sortez le fichier du webroot.
Un vrai JPG qui contient un payload dans les métadonnées n'est pas une double extension. C'est un autre dossier (inclusion de fichier). Ici, on parle du nom.
Tester sans exécuter
Ne cliquez pas l'URL du suspect depuis un navigateur de prod « pour voir le shell ». Téléchargez en SFTP, ouvrez en texte. `<?php` en tête ou après une signature JPEG : classé.
Si vous devez tester l'exécution, faites-le sur une copie isolée, pas en laissant le fichier en 200 public. Le temps du test, l'URL est une porte ouverte au monde.
Les logs d'accès : des `GET` vers ce fichier avec des query strings longs (`?cmd=`, `?e=`) confirment l'usage. Gardez l'extrait. Heure d'entrée vs premier hit.
Interdire PHP dans uploads (Apache, Nginx)
Apache : un `.htaccess` dans `uploads` qui refuse l'engine PHP (`php_flag engine off` selon le mode, ou `Require all denied` sur `\.php`). Le bon snippet dépend de PHP-FPM vs mod_php. L'hébergeur a souvent un modèle « sécuriser uploads ». Demandez-le ; un snippet de forum casse les images.
Nginx : `location` sur le path médias qui n'inclut pas FastCGI vers PHP. C'est au vhost, pas à un `.htaccess` ignoré.
Après la règle : un `<?php echo 1;` de test que vous posez et retirez, appelé en HTTP, doit afficher le source ou 403, pas `1`. Si vous voyez `1`, la règle est morte. Voir interdire PHP pour le détail durable.
Le handler dans `.htaccess` qui annule tout
L'attaquant pose souvent, dans le même dossier, un `.htaccess` `AddHandler php .jpg`. Votre règle « engine off » à la racine perd. Lisez tous les `.htaccess` sous uploads. Voir htaccess modifié.
Un `.user.ini` `auto_prepend` dans uploads : même étage. Lisez-le.
LiteSpeed : des règles spécifiques. Testez réellement l'URL de test PHP, ne croyez pas le fichier.
Avatars, builders, et marketplaces
BBPress, un LMS, un marketplace : chaque vendeur uploade. Le dossier n'est pas `uploads/2024` classique, c'est `uploads/ultimatemember/` ou équivalent. La même recherche `*.php*` là.
Elementor et les kits importent des JSON + des médias. Un kit « gratuit » est un vecteur. Scannez après chaque import.
Les comptes clients en masse qui posent un avatar : croisez les dates d'inscription et les fichiers.
Nettoyer les fichiers, pas seulement la règle
Copie hors serveur, retrait du webroot, 404 sur l'URL. Les attachments en base : mettez à jour ou supprimez la pièce pour ne pas servir un lien cassé dans un article. Un `guid` mort vaut mieux qu'un shell.
Un cron peut reposer le fichier. Panel + cron. Un admin avec File Manager aussi.
Si l'URL du shell était dans Safe Browsing, réexamen après 404 réelle. 24 à 72 h. Inspection : 404, pas 200 avec une image vide.
Prévention qui tient après J+7
Règle d'exécution off, File Manager désinstallé, moins d'admins, types MIME stricts. Une revue `*.php*` sous uploads à J+7. C'est trois minutes.
Les plugins « permettre tous les uploads » n'ont plus leur place. Un besoin PDF/SVG se traite à part (SVG = XSS, autre sujet).
Vous pouvez déclarer le site. Une liste des `*.php*` sous uploads est déjà un plan de travail.
- Liste `*.php*` + copies.
- Règle anti-exécution testée.
- `.htaccess` locaux relus.
- File Manager hors jeu.