Sécuriser WordPress : contrôler l’exécution des scripts dans uploads

Dans WordPress, le dossier wp-content/uploads ressemble souvent à une zone “inoffensive”. On y met des images, des PDF, parfois des fichiers exportés. Pourtant, c’est aussi l’endroit que beaucoup d’attaques cherchent à exploiter, surtout quand un serveur autorise l’exécution de scripts depuis des répertoires censés ne contenir que des contenus statiques.

Le principe est simple: les fichiers déposés dans uploads ne doivent pas pouvoir être traités comme du code exécutable. Concrètement, il faut empêcher le serveur (Apache ou Nginx) d’interpréter des extensions dangereuses comme du PHP, JavaScript, certains wrappers, ou d’autres formats selon le contexte. Le piège, c’est que l’on croit souvent être protégé parce que WordPress “n’exécute pas” les scripts. Mais l’exécution peut venir du serveur web, pas de WordPress.

Je l’ai vu en production sous plusieurs configurations. Une installation WordPress bien durcie, mais avec un hôte qui avait laissé un réglage par défaut trop permissif, et un attaquant qui n’avait eu besoin que d’uploader un fichier avec l’extension attendue par le serveur. Résultat: un “simple” téléchargement devenait un chemin vers une exécution de code. Le plus déroutant n’est pas le scénario, c’est le temps perdu à chercher dans WordPress alors que le problème se jouait sur la couche serveur.

image

Pourquoi uploads est une cible logique

WordPress met à disposition une interface pratique: vous uploadez un fichier, WordPress le stocke dans wp-content/uploads/année/mois/, puis il est servi via l’URL correspondante. Tant que le serveur renvoie le contenu en tant que fichier statique, tout va bien.

Mais dès que le serveur a une règle qui associe une extension à un interpréteur, ou que certaines options laissent passer des vecteurs inattendus, la situation change. Il suffit alors d’un détail du type:

    une règle qui active l’interprétation PHP même dans des dossiers “hors racine”, une configuration qui suit des liens symboliques sans garde-fou, un traitement spécial d’extensions “ambiguës”, ou une politique qui autorise l’exécution de scripts via des variantes (par exemple certains formats qui déclenchent une chaîne de traitement).

WordPress peut limiter ce que l’interface accepte, mais ce n’est pas une barrière absolue. Il existe aussi des cas “non prévus” comme l’import depuis un plugin, l’upload via API, une restauration de média, ou des opérations de synchronisation.

Le bon réflexe consiste à traiter uploads comme une zone de stockage non exécutable par le serveur, et à compléter avec une hygiène côté WordPress.

Les deux niveaux à verrouiller: serveur et WordPress

Pour sécuriser correctement, il faut penser en couches. Une défense robuste évite la dépendance à un seul mécanisme. Je raisonne généralement comme suit:

Empêcher l’exécution côté serveur (Apache, Nginx, ou reverse proxy). Réduire la probabilité d’upload de fichiers problématiques côté WordPress, et surtout détecter ce qui a été fait.

Si vous ne faites que WordPress, vous comptez sur une logique applicative, et elle peut être contournée par des chemins d’upload alternatifs. Si vous ne faites que le serveur, vous limitez l’impact d’un fichier malicieux, mais vous laissez potentiellement des contenus nuisibles accessibles ou exploitables d’une autre manière (déclenchement de téléchargements forcés, fichiers volumineux, contenu polyglotte, et autres).

La bonne approche est une combinaison. Dans la pratique, le cœur du sujet ici, c’est bien de contrôler l’exécution des scripts dans wp-content/uploads.

Apache: bloquer l’interprétation du code dans uploads

Sous Apache, l’approche la plus classique consiste à utiliser des règles dans un .htaccess pour neutraliser l’exécution des fichiers PHP dans uploads. Le détail dépend de votre structure, mais le raisonnement est le même.

Option fréquente via .htaccess

Dans le dossier wp-content/uploads, vous pouvez placer un fichier .htaccess qui empêche l’exécution des extensions associées à un interpréteur. Le mécanisme exact varie selon la version d’Apache et les modules activés, mais l’idée consiste à dire: “traitez ces fichiers comme des fichiers statiques, pas comme du code”.

Quand c’est possible, on utilise aussi le paramètre php_flag ou des directives qui désactivent la manipulation PHP à l’intérieur du dossier. Toutefois, sur beaucoup d’hébergeurs, PHP est géré via php-fpm ou via une autre passerelle, donc toutes les directives ne sont pas disponibles.

Ce que je recommande en premier, c’est de vérifier deux choses avant d’écrire des règles au hasard:

    Est-ce que PHP est servi via Apache module (mod_php) ou via php-fpm ? La surcharge .htaccess est-elle autorisée sur le chemin (directive AllowOverride) ?

Sans ça, vous pouvez empiler des règles qui ne s’appliqueront jamais.

Une règle orientée extensions

Vous pouvez également bloquer les extensions à risque en forçant un type de traitement non exécutable ou un rejet. Le piège est de ne pas “sur-corriger”. Bloquer trop large peut gêner vos téléchargements légitimes. Par exemple, certains sites autorisent l’envoi de fichiers .php uniquement parce que le besoin a été mal compris, ou parce qu’un plugin a été configuré pour accepter “tout”. Mais si votre site n’a aucune raison de servir du .js ou du .php depuis uploads, il vaut mieux refuser très strictement.

image

Pour être concret, je travaille souvent avec une politique d’interdiction sur une liste d’extensions dangereuses, mais je préfère le faire “proprement” (au niveau WordPress aussi) plutôt que de compter uniquement sur le serveur.

Nginx: empêcher toute interprétation dans uploads

Nginx est souvent plus direct. Si PHP est servi via php-fpm, il suffit de s’assurer qu’une requête vers uploads n’est jamais routée vers le fastcgi pour interprétation.

Typiquement, le configuration doit contenir une séparation claire entre:

    la racine WordPress (qui peut inclure index.php), et les dossiers statiques uploads qui doivent être servis tels quels.

Un mauvais “location” peut déclencher l’envoi vers fastcgi_pass pour des extensions inattendues. C’est rare sur une config soignée, mais j’ai déjà rencontré des templates d’hébergement où un location ~ \.php$ était trop permissif et finissait par attraper des fichiers dans uploads.

Le contrôle le plus sain consiste à ajouter une règle qui donne la priorité à uploads en tant que contenu statique, puis à vérifier que location ne recoupe pas la logique PHP.

Ensuite, gardez un œil sur les chemins où la normalisation peut surprendre, notamment si votre serveur suit des liens symboliques. Dans ce cas, un attaquant peut tenter de déposer un fichier vers un chemin qui finit par être interprété. Une politique “non exécutable + pas de liens symboliques dans uploads” est alors une sécurité utile.

Le cœur côté WordPress: réduire l’upload de fichiers risqués

Même si vous verrouillez le serveur, je conseille d’agir aussi dans WordPress. D’une part, parce que c’est une barrière efficace contre les erreurs humaines et certains vecteurs. D’autre part, parce que WordPress gère l’acceptation des fichiers, la taille, et l’enregistrement dans la base.

Contrôler les types de fichiers autorisés

WordPress a des mécanismes pour gérer les extensions autorisées et la logique de téléchargement. En pratique, beaucoup de sites se contentent du réglage “par défaut” et laissent une grande liberté à l’import. Or, un réglage trop large, comme permettre des extensions “médias” qui ne sont pas des médias, augmente la surface d’attaque.

Je préfère une approche pragmatique: partir de ce que votre site utilise réellement. Si vous n’avez jamais besoin d’uploader des .svg, des .js, ou des archives contenant des scripts, vous gagnez à les refuser.

Voici une règle d’hygiène simple que j’applique souvent:

    limiter strictement les extensions autorisées, refuser les fichiers à risque, et, si possible, refuser les “double extensions” qui contournent des contrôles naïfs (par exemple image.jpg.php).

Le cas des fichiers “légitimes mais dangereux”

Tous les fichiers ne sont pas dangereux par extension seule. Un .svg peut contenir du JavaScript si votre navigateur et votre chaîne de rendu le permettent. Un .html peut servir à livrer du contenu malveillant. Un .pdf peut contenir des éléments exploitables selon les lecteurs. Là, le serveur ne fait pas tout, parce que ce n’est pas seulement “exécuter côté serveur”, c’est “nuire côté client”.

Votre demande porte sur l’exécution des scripts côté serveur dans uploads, mais en sécurisation réelle, je pense toujours à l’ensemble du flux. Pour rester concret, un bon compromis consiste à:

    interdire l’exécution côté serveur, mais aussi limiter ce qui entre dans uploads pour réduire les surprises côté navigateur et utilisateurs.

Mettre en place une politique d’exécution “zéro confiance” pour uploads

La partie la plus importante, c’est la cohérence. Vous voulez une politique qui ne dépend pas d’un plugin, d’un réglage d’hébergement unique, ou d’une variante de PHP. L’objectif est d’obtenir un résultat stable même si un composant change.

En pratique, cela ressemble à une check-list de validations techniques. Pas pour tout faire d’un coup, mais pour vérifier que chaque couche fait bien sa part.

    Vérifier que uploads n’est pas interprété comme PHP, ni via Apache, ni via Nginx. Contrôler que les règles location et les mécanismes de rewriter ne recouvrent pas accidentellement uploads. S’assurer que les .htaccess sont bien pris en compte sur le chemin concerné (si vous en utilisez). Revoir les types de fichiers autorisés dans WordPress pour refuser ce qui n’a aucune utilité.

C’est court, mais ce sont exactement les points qui font échouer des sécurisations “presque correctes”.

Pièges classiques et décisions à prendre

Il y a plusieurs pièges qui reviennent. Je vous les résume, parce que ce sont souvent les décisions qui demandent du jugement.

1) Confondre “WordPress ne l’exécute pas” avec “le serveur n’exécute pas”

WordPress ne lance pas un fichier uploadé comme du code. Mais le serveur, lui, peut interpréter certaines extensions si les règles le permettent. Du coup, la protection doit se situer au bon niveau.

2) Se limiter à une extension PHP

Bloquer .php uniquement peut laisser passer des variantes selon l’environnement. Par exemple, certains systèmes interprètent d’autres extensions ou utilisent des mécanismes de transformation. Le bon réflexe consiste à bloquer “ce qui est exécuté” plus que “ce qui s’appelle php”. En pratique, ça revient à s’assurer qu’aucun chemin ne mène à un interpréteur.

3) Ajouter une règle trop restrictive

Quand vous bloquez des extensions, attention aux besoins réels. Des sites e-commerce ou des sites éditoriaux reçoivent parfois des fichiers de type .json, .xml, .csv, ou des archives. Si vous interdisez tout sans comprendre le besoin, vous allez casser des workflows. J’ai déjà vu des imports utilisateurs qui se mettaient à échouer, puis les gens contournaient en compressant en .zip, ce qui finit par introduire une nouvelle surface d’attaque.

4) Oublier la surface “upload via API et plugins”

Même avec un contrôle dans l’interface d’upload, un plugin peut offrir un endpoint différent. Là encore, le serveur reste votre dernière ligne de défense. WordPress ne devrait pas être une seule barrière, mais un ensemble de garde-fous.

Exemple de stratégie réaliste (sans magie)

Imaginons un site WordPress sur un hébergement standard. Objectif: personne ne doit pouvoir exécuter du code depuis wp-content/uploads.

Je construirais la stratégie en deux axes.

D’abord, côté serveur, je verrouille de sorte que uploads serve uniquement du statique. Sur Apache, je vérifie que les directives liées à PHP ne s’appliquent pas. Sur Nginx, je m’assure que les location ne redirigent pas vers fastcgi pour des chemins dans uploads.

Ensuite, côté WordPress, je réduis les extensions autorisées à celles qui correspondent aux usages. Si le site reçoit des images et des PDF, le reste devient un “non” par défaut. Cette décision a un impact immédiat, et elle simplifie la maintenance.

Enfin, j’ajoute une surveillance. Pas forcément un truc “lourd”. Le minimum consiste à inspecter les derniers fichiers uploadés, et à garder un historique des changements côté médiathèque quand c’est possible. Si vous constatez un upload d’extension inattendue, vous réagissez avant que l’attaquant ne tente un autre angle.

Comprendre les compromis entre “bloquer” et “sanitiser”

Il y a une tentation: au lieu de bloquer, “sanitiser” ce qui est dangereux. Par exemple, convertir un SVG en PNG. Ou nettoyer du HTML.

En matière de scripts côté serveur, la sanitisation est rarement le meilleur levier. La raison est simple: si le chemin vers l’interprétation existe encore, le nettoyage peut être contourné. Dans ces cas, “ne pas exécuter” est plus fiable.

En revanche, pour des contenus qui posent surtout des risques côté navigateur, une conversion ou une politique de rendu peut être une bonne idée. Mais elle ne remplace pas un verrou serveur sur l’exécution.

Je résume la logique en une comparaison utile, parce que c’est là https://gardewp.fr/securite-wordpress/ que les projets partent souvent dans tous les sens:

image

| Objectif | Approche la plus robuste | Ce qu’elle évite | |---|---|---| | Empêcher l’exécution côté serveur | Bloquer l’interprétation dans uploads | Les exécutions PHP ou traitements d’exécution inattendus | | Réduire les risques côté navigateur | Limiter les types de fichiers, éventuellement convertir | Les scripts déclenchés dans le client | | Réduire la probabilité d’upload malveillant | Restreindre les extensions dans WordPress | Les fichiers “exotiques” injectés via médiathèque |

Test et vérification: comment savoir que c’est vraiment fermé

La partie “validation” est souvent bâclée. Pourtant, c’est elle qui évite les fausses certitudes.

Je fais typiquement un test contrôlé: depuis un environnement de staging, je tente d’uploader un fichier dont l’extension correspond à un scénario dangereux, puis je vérifie le comportement côté URL. Est-ce que le serveur renvoie juste un contenu statique, une page d’erreur, ou est-ce qu’il déclenche une route PHP ? Le bon résultat varie selon vos choix (erreur 403, téléchargement refusé, 404), mais il ne doit jamais y avoir d’exécution.

Ensuite, je vérifie les en-têtes et le chemin réel de service. Par exemple, un serveur peut renvoyer le fichier mais avec un traitement qui reste problématique. L’idée, c’est de vérifier à la fois le code de réponse et l’absence de mécanisme d’interprétation.

Enfin, je teste un cas “bête mais fréquent”: le même fichier renommé avec une double extension ou une variante. Si vous avez mis en place une interdiction basée uniquement sur une extension “simple”, ce genre de test révèle rapidement les failles.

Au-delà de uploads: quand durcir devient un projet

Une sécurisation sérieuse ne s’arrête pas à un seul dossier. Une fois que vous avez verrouillé uploads, vous pouvez élargir progressivement, sans tout casser:

    réviser les rôles WordPress et les capacités d’upload, limiter les plugins qui autorisent l’upload, durcir la politique de mises à jour, surveiller les erreurs serveur et les accès anormaux.

Mais garder uploads non exécutable reste un socle. C’est une décision structurelle, et elle réduit drastiquement l’impact d’une compromission partielle.

Ce que je ferais en priorité sur un site existant

Si vous devez prioriser, je ne commencerais pas par des optimisations. Je commencerais par les verrous qui empêchent l’exécution.

Une approche réaliste, en gardant votre temps et vos risques sous contrôle, ressemble à ceci:

    Identifier votre serveur (Apache ou Nginx) et la manière dont PHP est servi. Appliquer une règle “uploads en statique” sur la couche serveur. Réduire la liste des extensions autorisées côté WordPress. Faire un test en staging et un contrôle rapide en production sur un fichier refusé ou neutralisé.

C’est généralement suffisant pour éliminer le scénario “upload dans uploads puis exécution”, qui est celui qui motive la démarche.

Checklist de sécurité finale (à utiliser avant de se reposer)

La dernière étape, c’est de s’assurer que vos garde-fous tiennent dans le temps, surtout après un changement d’hébergement ou une mise à jour de configuration.

    Confirmer que les règles serveur sont actives après tout changement (template d’hébergement, mise à jour, reload). Vérifier que les .htaccess ne sont pas ignorés (si vous en utilisez). Vérifier que la médiathèque refuse les extensions non souhaitées. Contrôler que les fichiers uploadés ne déclenchent jamais de traitement PHP.

Si vous obtenez ce résultat de manière stable, vous avez gagné quelque chose de très concret: même en cas d’upload indésirable, le serveur ne deviendra pas une machine à exécuter.

Sécuriser WordPress, ce n’est pas appliquer une seule règle “magique”. C’est rendre la compromission moins utile, moins immédiate, et plus coûteuse. Verrouiller l’exécution dans wp-content/uploads est l’une des décisions les plus rentables, parce qu’elle vise exactement le point où l’on imagine souvent à tort que “ce n’est que des fichiers”.