Sécurité WordPress : désactiver l’exécution de scripts dans uploads

Quand on parle de sécurité WordPress, on pense souvent aux failles de plugins, aux mots de passe, à la mise à jour du core. Tout à fait logique. Mais il existe un angle plus discret, pourtant très rentable: empêcher tout contenu téléchargé dans wp-content/uploads de pouvoir s’exécuter comme un script.

Dans la pratique, la majorité des compromissions qui passent par une montée de privilèges ou une injection finissent par tenter quelque chose du genre: déposer un fichier “d’attaque” dans le répertoire des médias, puis déclencher son exécution via une URL ou une règle de réécriture. Si l’exécution dans les uploads est bloquée, cette dernière étape devient inutile. Ce n’est pas une garantie absolue contre tout, mais c’est un barrage solide, simple à maintenir, et facile à vérifier.

Pourquoi cibler uploads plutôt que “tout” faire au hasard

Le répertoire uploads sert à stocker des images (JPG, PNG, WebP), des PDF, des documents, parfois des fichiers vidéo, et surtout tout ce que le site veut afficher ou transmettre. Le but n’est pas de “désactiver les téléchargements”, mais de casser une chaîne d’attaque très spécifique: “je peux envoyer un fichier vers un endroit accessible publiquement, donc je tente de l’exécuter”.

Sur un serveur mal configuré, un attaquant peut parfois réussir à amener le serveur à interpréter du contenu comme du PHP (ou autre), même si le fichier a été déposé dans un dossier censé contenir uniquement des médias. Dans ce scénario, l’impact n’est plus limité à un affichage ou à un contenu statique. On passe à l’exécution de code, donc à la porte ouverte vers le contrôle du site.

Le choix de uploads est aussi un bon compromis côté performance et risque fonctionnel. On applique une règle claire et ciblée, sans toucher aux scripts du cœur WordPress, ni aux thèmes, ni aux plugins qui doivent continuer à fonctionner.

Ce qui doit rester possible, et ce qui doit être bloqué

Un blocage propre ne se fait pas en “interdisant tout”. Il s’agit de distinguer deux comportements:

Un fichier image ou PDF doit être servi tel quel, via HTTP. Un fichier qui ressemble à un script (généralement .php, parfois d’autres extensions selon la stack) ne doit jamais être interprété par le moteur.

Sur certains environnements, “désactiver l’exécution” signifie “ne jamais passer par PHP”. Sur d’autres, cela revient à renvoyer une erreur 403, ou à forcer le serveur à traiter le fichier comme un contenu statique.

Le point délicat, c’est que tous les hébergeurs ne gèrent pas uploads de la même manière. Certains ont déjà des règles internes. D’autres laissent l’interprétation PHP assez large. Du coup, la bonne approche consiste à vérifier votre configuration actuelle avant de multiplier les règles.

Vérifier l’existant avant d’agir

Avant d’ajouter des règles, prenez deux minutes pour comprendre ce que votre serveur fait déjà.

    Test rapide côté URL: essayez d’accéder à un fichier “inexistant” du type wp-content/uploads/quelquechose.php et observez le résultat. Vous ne cherchez pas un “oui”, vous cherchez un comportement cohérent. Un 404, un 403, ou une réponse PHP sont des indices. Si vous obtenez une réponse qui ressemble à une exécution, c’est un signal d’alarme. Vérifier le mode PHP sur les fichiers: selon votre hébergeur, la configuration PHP peut être injectée via .htaccess, via directives globales, ou via des réglages de pool PHP-FPM.

Si vous êtes en Apache, .htaccess est votre outil naturel. Si vous êtes en Nginx, c’est la configuration du serveur, pas wp-content/uploads/.htaccess, qui fera foi. Si vous êtes sur un environnement “mixte”, comme certains setups avec directives partielles, mieux vaut viser ce que vous contrôlez réellement.

image

Option 1: Apache, via .htaccess dans uploads

Si votre serveur utilise Apache et que la configuration via .htaccess est autorisée, c’est la méthode la plus directe. L’idée: dans wp-content/uploads (et éventuellement ses sous-dossiers), on empêche l’interprétation de certaines extensions de scripts.

La première étape consiste à se décider sur le niveau d’application.

    Soit vous mettez une règle à la racine de wp-content/uploads et vous comptez sur l’héritage pour couvrir uploads/2024/, uploads/2025/, etc. Soit vous créez une règle plus large au niveau de wp-content (souvent plus agressif). Soit vous appliquez la règle au niveau de uploads uniquement, ce qui est généralement suffisant.

Voici un exemple classique à placer dans un fichier .htaccess situé dans wp-content/uploads/:

Php_flag engine off Require all denied

Ce bloc fait deux choses. D’abord, il tente de désactiver le moteur PHP sur ce répertoire si mod_php est utilisé. Ensuite, il bloque explicitement l’accès aux fichiers dont l’extension ressemble à du PHP.

Quelques nuances importantes issues de situations réelles:

    Si votre serveur utilise PHP-FPM (donc pas mod_php.c), la directive php_flag engine off peut ne rien changer. Le bloc FilesMatch peut rester utile, car il bloque l’accès avant interprétation. La valeur exacte des extensions bloquées dépend du contexte. La liste ci-dessus couvre les variantes courantes. Vous éviterez de multiplier sans raison, sinon vous risquez de bloquer un fichier légitime qui utilise une extension atypique. WordPress n’a aucune raison “fonctionnelle” d’exécuter du PHP depuis uploads. C’est donc une cible raisonnable, mais gardez en tête qu’un site pourrait, par accident, uploader un fichier ayant une extension script. Le blocage est précisément ce qu’on veut.

Le piège: permettre l’exécution de scripts ailleurs

Un site bien configuré exécute du PHP pour wp-admin, le thème, les plugins, et le core. Votre règle ne doit pas interférer avec ces zones.

image

Si vous mettez la règle trop haut, par exemple au niveau de wp-content, vous risquez de casser des choses. wp-content contient aussi des scripts qui doivent rester exécutés via le bootstrap normal de WordPress. Le “périmètre minimal” reste la meilleure stratégie.

Contrôler l’impact sur les médias

Cette règle ne devrait pas empêcher l’affichage des médias standards. En revanche, il faut être attentif à deux cas:

    Certains systèmes proposent des formats comme SVG. Ce n’est pas du PHP, mais selon votre configuration de sécurité côté navigateur, un SVG peut être une surface d’attaque XSS. Bloquer l’exécution PHP ne règle pas ce sujet. Certains hébergeurs utilisent des variantes d’extensions ou des méthodes de téléchargement particulières. Si vous constatez des erreurs d’accès à certains fichiers, c’est un signal qu’il faut ajuster la liste d’extensions bloquées, ou déplacer la règle.

Option 2: Nginx, blocage côté serveur (souvent le plus fiable)

Si vous êtes sur Nginx, vous n’aurez généralement pas le bon résultat en misant uniquement sur un .htaccess, car Nginx ne lit pas ce type de fichier de manière standard. Dans ce cas, on règle dans le bloc server ou dans la configuration du location.

L’approche consiste à refuser les requêtes dont le chemin pointe vers une extension de script dans uploads. L’idée ressemble, mais l’outil change.

Un exemple typique dans un fichier de conf Nginx:

Location ~* ^/wp-content/uploads/.*\.(php|phtml|php5|php7|phps|phar)$ Deny all;

Selon votre configuration, vous pouvez aussi combiner avec un location plus large ou une règle d’ordre supérieur. Le point critique en Nginx, c’est l’ordre des location, et le fait que les expressions regex ont des priorités. Si la règle n’est pas “plus prioritaire” que votre configuration existante, elle peut être contournée.

Autre option Nginx, selon votre architecture, consiste à rediriger ces requêtes vers une page statique d’erreur ou à renvoyer un code 403. Mais l’objectif final reste identique: empêcher l’interprétation.

Option 3: durcir “à la source” via paramètres serveur et politiques globales

Selon les hébergeurs, la configuration PHP ou la politique d’exécution peuvent déjà être restreintes dans uploads. J’ai déjà vu des environnements où l’exécution y était bloquée, mais où l’accès aux fichiers .php renvoyait seulement un 404, ce qui donne un peu moins de visibilité pour valider le durcissement.

Dans un monde idéal, on devrait avoir une politique globale qui refuse l’exécution de scripts dans les répertoires de stockage de fichiers, pas uniquement pour uploads. Mais “idéal” et “réel” ne collent pas toujours: certains plugins stockent des fichiers “techniques” dans des sous-dossiers, et l’équipe d’exploitation préfère éviter les surprises.

Quand vous avez le choix, un durcissement global est excellent. Quand vous n’avez pas le choix, la stratégie ciblée sur wp-content/uploads reste un excellent niveau de sécurité WordPress, car elle vise un point d’attaque très concret et fréquemment ciblé.

Une configuration WordPress peut masquer le problème, pas le résoudre

WordPress en lui-même ne devrait pas exécuter les scripts depuis uploads. Le core sert le contenu, il ne devrait pas appeler le moteur PHP pour un fichier téléchargé “comme un média”.

Mais je l’ai déjà vu: un site fonctionne “normalement” pendant des semaines, puis un attaquant trouve une incohérence, ou un plugin mal configuré crée une URL de traitement, ou l’hébergement a une règle de type “tous les fichiers sont passés à PHP”. Le résultat peut être très variable. Parfois l’exécution semble impossible, puis elle devient possible après un changement d’environnement.

C’est pour ça que je recommande d’ajouter la protection côté serveur et non seulement de “compter” sur WordPress.

Étapes pratiques, sans casser vos médias

Voici une façon prudente de procéder, que vous soyez sur Apache (avec .htaccess) ou ailleurs. L’idée est de limiter le risque fonctionnel.

Identifiez si vous pouvez modifier une règle dans wp-content/uploads/.htaccess (Apache) ou si vous devez passer par la configuration Nginx. Déployez la règle sur un environnement de test ou, à défaut, pendant une fenêtre courte, et surveillez les erreurs 403 ou 404 sur les fichiers concernées. Lancez un test contrôlé: créez un fichier inoffensif avec une extension script dans un sous-dossier de test de uploads, et vérifiez que le serveur refuse l’accès ou ne l’exécute pas. Évitez de mettre du code dangereux, l’objectif est un test de contrôle. Vérifiez que les médias “normaux” (images, PDF) s’affichent et se téléchargent. Le bon signe, c’est quand les URL existantes continuent de répondre comme avant. Retirez les traces du test une fois validé.

Cette méthode vous évite le piège classique: appliquer une règle trop large, découvrir le problème trop tard, puis chercher pendant une heure pourquoi un plugin n’affiche plus un document.

Exemple de test simple (et propre)

Le test que vous faites doit être suffisamment inoffensif pour ne pas ajouter de risque au site, mais assez parlant pour prouver le comportement.

Par exemple, vous pouvez créer un fichier texte nommé upload-test.php qui ne contient rien d’exécutable (ou seulement un contenu neutre). Ensuite, vous tentez d’y accéder via l’URL publique. Si le serveur renvoie une erreur 403 ou 404, c’est bon signe. Si le serveur renvoie https://gardewp.fr/securite-wordpress/ un output typé “exécution PHP”, ou un comportement inattendu, c’est que la protection ne s’applique pas.

Ensuite seulement, vous supprimez le fichier. Cette discipline évite de laisser traîner une “preuve” qui pourrait devenir une arme si jamais la configuration change.

Les compromis qu’on oublie souvent

Bloquer trop d’extensions

Une règle qui interdit .php et variantes est généralement cohérente. Interdire “toutes les extensions” serait tentant, mais ce serait souvent destructeur. Un site peut avoir des fichiers *.phtml ou *.phar très rarement, mais le bénéfice dépend du risque. Mon approche: commencer par les extensions qui correspondent vraiment aux scripts interprétés dans PHP.

Sous-dossiers et héritage

uploads contient typiquement des années et mois, et parfois des sous-dossiers créés par des plugins. Si votre serveur n’hérite pas les règles comme prévu, vous pouvez voir des différences de comportement selon le dossier.

C’est un point où j’ai déjà perdu du temps: on ajoute une règle, mais sur un sous-dossier, le fichier est traité différemment. Un check visuel des chemins et des logs serveur simplifie tout.

CDN, cache, et “ça marche peut-être déjà”

Si vous avez un CDN (ou une couche de cache), vous pouvez croire que la protection ne marche pas, alors que c’est le cache qui sert une réponse précédente. Dans le test, tenez compte du fait qu’une ressource peut être servie depuis une mémoire distribuée.

La vérification la plus fiable reste de regarder les codes HTTP au moment du test, et idéalement les entrées logs sur le serveur d’origine si vous y avez accès.

Deux scénarios concrets que j’ai rencontrés

Cas 1: règle présente, mais pas au bon niveau

Un site avait un .htaccess dans wp-content, pas dans wp-content/uploads. Le serveur avait des particularités, et certains fichiers dans les sous-dossiers finissaient par être traités différemment. Résultat, un test sur uploads/2024/ renvoyait une réponse, alors que uploads/ racine renvoyait une erreur.

La correction a consisté à appliquer la règle au dossier exact qui devait être protégé, wp-content/uploads/, et à confirmer le comportement sur un exemple dans un sous-dossier.

Cas 2: PHP-FPM, donc php_flag engine off inutile

Sur un autre environnement, la directive php_flag engine off ne changeait rien, car PHP était géré via PHP-FPM. La partie FilesMatch a servi de garde-fou, mais on ne l’avait pas activée au début. Le test a permis d’identifier rapidement que la première moitié de la règle n’était pas efficace.

L’enseignement: ne vous contentez pas de “coller un snippet”, validez. La configuration PHP varie beaucoup entre hébergeurs, même si l’interface admin ressemble à l’identique.

Comment intégrer cela dans une démarche plus large de sécurité

Désactiver l’exécution dans uploads est un excellent verrou, mais ce n’est pas une substitution à des actions essentielles. En pratique, je le traite comme une couche supplémentaire dans une stratégie simple:

    Mises à jour régulières du core, des thèmes et plugins. Réduction des droits, gestion stricte des comptes. Contrôle de l’accès aux zones d’administration. Journalisation et surveillance des changements.

Le mérite de la règle sur uploads, c’est qu’elle reste utile même si un plugin a un bug, ou si un compte est compromis. Elle empêche l’atterrissage final d’une charge utile dans un dossier où elle pourrait autrement s’exécuter.

Checklist de validation après changement (rapide et utile)

Avant de considérer la modification “terminée”, faites une validation courte.

    Test d’accès à une URL de fichier script dans uploads avec une extension bloquée, sans comportement d’exécution. Test d’affichage de deux à trois médias existants (images et un document si possible). Contrôle des codes HTTP sur vos tests (403 ou 404 attendu, pas de sortie PHP). Vérification que les uploads via WordPress continuent de fonctionner (au moins sur un fichier récent). Vérification des logs serveur pendant le test, si vous y avez accès.

Une fois que ces points sont au vert, vous avez un verrou net et cohérent sur la surface la plus “uploadable” du site.

Points à surveiller sur la durée

La sécurité n’est pas un “one shot”. Un changement de configuration serveur, une migration d’hébergement, ou l’installation d’un plugin de sécurité peut modifier le comportement des fichiers.

Par exemple, un plugin qui réécrit des règles, ou qui ajoute une protection via .htaccess, peut entrer en conflit avec votre blocage dans uploads. Vous verrez alors des erreurs inattendues ou, au contraire, une protection qui ne se déclenche plus.

Une vérification périodique, ou au moins après toute migration, est une bonne hygiène. Le test d’accès décrit plus haut est rapide, et il vous évite les mauvaises surprises.

Mettre fin à l’exécution des scripts dans wp-content/uploads est une mesure concrète, ciblée, et souvent sous-estimée. C’est typiquement le genre de durcissement qui n’interfère pas avec le fonctionnement normal de WordPress, tout en réduisant fortement la valeur d’une tentative d’intrusion. Si vous prenez le temps de valider la règle dans votre contexte réel (Apache ou Nginx, PHP-FPM ou mod_php, cache ou pas), vous obtenez une protection stable, facile à expliquer, et difficile à contourner.