Sécurité WordPress : sécuriser l’upload des fichiers et prévenir l’exécution de code

L’upload de fichiers est l’endroit où WordPress montre le plus vite ses limites, non pas parce qu’il est “mauvais”, mais parce qu’il touche à une réalité brute : un serveur qui accepte des octets arbitraires peut aussi, si on le laisse faire, accepter l’exécution arbitraire qui vient avec. J’ai vu des sites compromis non pas via un plugin “maison”, mais via un formulaire d’upload banal, parfois lié à un thème ou à un plugin tiers, parfois exposé par une configuration trop ouverte.

Dans les incidents que j’ai rencontrés, le schéma revenait souvent. Quelqu’un gagne un accès (compte faible, session volée, plugin vulnérable, erreur de permission). Ensuite, il dépose un fichier dans wp-content/uploads/ ou ailleurs, puis cherche à le faire exécuter. Même si WordPress “devrait” empêcher l’exécution dans les dossiers d’upload, ce n’est pas une garantie universelle. Le comportement dépend de la configuration du serveur web, de la présence de règles .htaccess, de la façon dont le fichier est servi, et de ce que le reste de l’application fait autour de l’upload.

Ce guide se concentre sur la sécurisation de l’upload et sur la prévention de l’exécution de code. Je vais rester concret, orienté opération, avec des arbitrages réalistes.

Comprendre le vrai risque : ce qui doit être empêché

Quand on parle d’upload, on a tendance à penser “contrôle du type de fichier”. C’est important, mais pas suffisant. Le risque principal se décompose en deux objectifs que l’attaquant cherche à atteindre.

D’abord, déposer un fichier qui ne devrait pas être autorisé. Les formats “visuellement inoffensifs” peuvent être piégés: un fichier avec une extension trompeuse, un fichier qui contient du code dans des métadonnées, ou un contenu qui n’est pas ce que l’application croit lire.

Ensuite, transformer ce dépôt en exécution. Pour exécuter du code, il faut un chemin où le serveur interprète le contenu comme du code (PHP, parfois d’autres langages) ou au moins un mécanisme de rebond (par exemple, une règle serveur qui sert certains suffixes autrement que prévu).

Ce qu’on veut, c’est empêcher simultanément :

    l’upload de types dangereux (ou, mieux, la capacité d’upload à un utilisateur non autorisé), l’aboutissement d’un fichier déposée vers un état exécutable sur le serveur, et, quand c’est impossible, au minimum réduire le nombre de chemins exploitables.

Le périmètre : où WordPress écrit réellement

WordPress centralise les uploads dans un dossier dédié, généralement wp-content/uploads/, sous des sous-dossiers par année et mois. C’est une bonne nouvelle, car on peut verrouiller cet espace de façon ciblée.

Mais “ciblée” ne veut pas dire “automatique”. Les configurations varient :

    Apache et Nginx n’ont pas les mêmes mécanismes, certaines règles .htaccess peuvent être absentes, ignorées, ou contredites, des directives serveurs peuvent changer la façon de traiter une extension, des reverse proxies ou serveurs en amont peuvent modifier la requête ou la réponse.

Avant de modifier quoi que ce soit, je conseille un contrôle simple : sur votre serveur, vérifiez la manière dont les fichiers sont servis dans wp-content/uploads/. Un exemple concret: si vous mettez un fichier de test inoffensif (même extension “suspecte”), est-il renvoyé avec Content-Type attendu, ou est-il interprété ? Si vous ne savez pas, c’est normal, mais c’est justement un signal pour renforcer la configuration plutôt que de “faire confiance à défaut”.

Séparer uploads et exécution : la règle d’or côté serveur

La défense la plus robuste contre l’exécution de code, c’est de rendre le dossier d’uploads non exécutable. WordPress ne suffit pas à lui seul, car l’exécution dépend du serveur.

Sur Apache : .htaccess et interdiction d’exécution

Si votre WordPress fonctionne avec Apache et que les .htaccess sont activés, le principe consiste à empêcher l’interprétation PHP dans wp-content/uploads/ et, selon votre configuration, dans ses sous-dossiers.

Typiquement, on cherche à neutraliser ce qui pourrait déclencher un parseur, par exemple en redirigeant ou en empêchant l’exécution. Les détails exacts dépendent de l’architecture (mod_php, PHP-FPM via proxy, etc.). Le bon réflexe est de garder une logique claire: tout ce qui se trouve dans uploads doit être servi comme fichier statique.

Une erreur fréquente vient du faux sentiment de sécurité : “WordPress met déjà la bonne règle”. Parfois elle existe, parfois elle a été supprimée, parfois un .htaccess utilisateur au mauvais niveau la casse. Faites vérifier le fichier et son héritage, pas seulement son existence.

Sur Nginx : règles location pour bloquer l’interprétation

Sur Nginx, le levier principal est la configuration location qui sert les statiques. L’objectif est que la requête vers wp-content/uploads/ ne passe pas par un bloc qui exécute PHP.

Avec PHP-FPM, les configurations Nginx utilisent souvent des règles du type “si l’URI correspond à un pattern PHP, envoyer vers le fastcgi”. Si uploads tombe par accident dans ce pattern, le risque augmente. Le correctif est donc généralement de s’assurer que les location prioritaires pour uploads servent en statique et ne déclenchent pas le fastcgi.

Si vous gérez un environnement managé, votre fournisseur peut déjà le faire. Mais l’expérience m’a appris une règle pragmatique: la meilleure configuration est celle qu’on a comprise en observant les directives réelles, et pas seulement celle qu’on “pense” être en place.

Empêcher les téléchargements dangereux avant qu’ils n’atterrissent

Verrouiller le dossier côté serveur est une barrière solide, mais il ne remplace pas une politique d’upload stricte. L’approche la plus saine est de réduire la surface d’entrée.

WordPress gère déjà l’upload avec validation et restrictions basées sur les types autorisés, mais dans les systèmes réels, ce sont souvent les “accroches” autour qui créent les failles:

    un plugin qui ajoute un formulaire d’upload, un thème qui autorise des extensions, un endpoint REST qui accepte des fichiers, une page “media” accessible sans la bonne capacité, ou un réglage qui élargit les extensions autorisées pour des besoins métier, puis oublie de resserrer.

Contrôles du côté WordPress : capacités et types autorisés

Le premier levier, c’est de ne pas laisser n’importe qui uploader. “n’importe qui” ne doit pas exister. WordPress a un modèle de capacités (rôles, permissions) et une logique autour de l’accès aux médias.

Quand vous déployez une fonctionnalité d’upload pour des utilisateurs, la question qui compte n’est pas seulement “est-ce que WordPress accepte le fichier ?”, mais “est-ce que l’utilisateur a réellement le droit de déposer ce fichier dans ce contexte ?”.

Deuxième levier, restreindre aux extensions utiles. Les extensions autorisées devraient refléter votre besoin réel. Si votre site ne sert que des images et des PDF, il faut être très prudent avec les archives compressées, les scripts, et les formats ambigus. Une extension autorisée ne dit pas tout, mais c’est un premier filtre.

Vérifier plus que l’extension : contenu et MIME

Dans la pratique, je vois trop souvent une logique qui se limite à l’extension. Les attaquants misent précisément sur cette fragilité, parce que le contrôle par extension est trivial à tromper.

Le bon niveau de prudence consiste à vérifier le contenu (ou à minima un indicateur de type) au moment de l’upload, pas seulement le suffixe. Le niveau exact dépend des bibliothèques disponibles, du serveur, et de la façon dont WordPress traite les uploads dans votre contexte. L’idée n’est pas de viser la perfection cryptographique, c’est de casser l’évidence que l’attaquant utilise.

Il y a un compromis à assumer : certains fichiers “exotiques” ou mal formatés peuvent être rejetés. Si vous gérez un site public avec des utilisateurs non techniques, c’est souvent acceptable, et vous pouvez offrir un processus de fallback, par exemple une demande manuelle ou une alternative.

Réduire l’impact : nommage, répertoire, et comportements de l’application

Même si le dossier d’uploads ne peut pas exécuter de code, un fichier malveillant peut toujours nuire d’autres façons :

    spam ou injection de contenu si le fichier est ensuite affiché sans échappement, exploitation via une URL “servie” avec un comportement spécial, pollution d’un stockage, ou surcharge, ou rebond vers une autre faiblesse.

Nommage : éviter les surprises côté application

WordPress renomme parfois les fichiers pour éviter les collisions, ce qui est utile. Mais si votre application affiche l’URL, stocke le chemin, ou réutilise le nom de fichier pour une logique interne, il faut éviter les dépendances dangereuses.

Dans la plupart des systèmes, le chemin ne doit jamais être utilisé comme base de requête directe pour exécuter quelque chose. C’est là que certaines “petites” intégrations se retournent contre vous, par exemple un plugin qui construit une commande, ou https://gardewp.fr/securite-wordpress/ un traitement arrière qui appelle des outils sur des fichiers au nom incertain.

Dossiers et permissions : moins de droit, moins d’opportunités

Si le serveur d’exécution a des droits excessifs, l’attaquant a plus de leviers après l’upload. On ne cherche pas à rendre votre serveur “fragile”, mais on veut une logique de moindre privilège. Les fichiers dans uploads doivent être lisibles pour servir, modifiables uniquement là où cela est nécessaire pour WordPress, et jamais “exécutables” au sens PHP.

Sur certains hébergements, la combinaison propriétaire/groupe, l’héritage des permissions et la configuration des exécutions peut varier. Si vous n’êtes pas sûr, faites au minimum valider la politique d’exécution par votre admin système ou votre hébergeur.

Cas concrets qui causent des incidents

Je vais décrire des scénarios que j’ai vus, sans dépendre d’un fournisseur précis.

Le formulaire d’upload “rapide” dans un plugin

Un plugin permet aux abonnés d’envoyer une pièce jointe. Le développeur a autorisé “zip” et “php” par commodité pour “test”. Résultat: même si le serveur ne fait pas exécuter le fichier PHP dans uploads, le contenu est accessible. Dans un deuxième temps, une autre partie de l’application affiche ou traite le fichier, ce qui déclenche un comportement inattendu. Le site ne “tombe” pas forcément sur le coup, mais l’exposition augmente.

Ce qui manque généralement: une politique d’upload par rôle, un filtre de type strict, et un parcours clair de traitement après upload.

L’oubli côté configuration Nginx

Sur Nginx, un bloc de configuration peut envoyer certains suffixes vers PHP-FPM. Si un pattern est trop large, il attrape des fichiers dans uploads. On a alors un scénario où l’upload devient directement exécutable, sans même que l’application fasse quoi que ce soit de “malveillant”.

Je recommande de traiter ce sujet comme une configuration, pas comme un bug d’application. On valide par test et par inspection des règles, et pas par intuition.

Le “multipart upload” via endpoint REST

Certains plugins exposent des endpoints REST pour l’upload, parfois pour des interfaces plus modernes. Là, les contrôles se déplacent. Vous pouvez avoir un comportement d’upload qui contourne des filtres UI, car l’interface front contrôle peu. L’exploit devient alors une requête HTTP construite, avec un type trompeur, ou un manque de contrôle sur les permissions.

Ici, le point clé est de s’assurer que l’endpoint impose les capacités adéquates et applique les validations identiques à celles du parcours standard.

Mise en pratique : une approche en quatre garde-fous

Plutôt que de chercher une “recette unique”, j’utilise une approche en garde-fous. Elle suit la logique suivante: même si un garde-fou échoue, les autres doivent empêcher l’exploitation complète.

1) Verrouiller la capacité d’upload

Un utilisateur doit pouvoir uploader uniquement ce que vous acceptez pour son rôle. L’upload public, même pour des images, mérite un contrôle additionnel. Sinon, vous transformez votre site en cible de dépôt.

2) Restreindre les types autorisés, pas seulement l’extension

C’est le filtre de première ligne. Il doit être cohérent avec votre politique produit, et le plus strict possible sans casser votre usage.

3) Rendre le dossier d’uploads non exécutable

C’est la barrière de sécurité système. À défaut, un exploit d’interprétation reste envisageable.

4) Réduire l’exposition applicative autour du fichier

Le fichier uploadé doit être stocké, servi, et référencé sans traitement risqué. L’affichage doit échapper correctement les sorties, et un éventuel traitement (redimensionnement, extraction, conversion) doit être isolé et robuste.

Ces garde-fous ne sont pas “théoriques”. Ils correspondent à des actions concrètes, avec des impacts mesurables sur la surface d’attaque.

Checklist opérationnelle avant de pousser un changement

Voici une liste courte qui me sert de point de départ quand je dois durcir un site sur un délai réaliste. Elle évite les ajustements au hasard.

    Vérifier comment wp-content/uploads/ est servi sur votre serveur (Apache ou Nginx), et confirmer que l’interprétation PHP ne s’applique pas dans ce dossier. Confirmer que les rôles autorisés à uploader correspondent au besoin réel, et que les endpoints d’upload (classique, média, REST, formulaires) utilisent les mêmes contrôles. Réduire la liste des types autorisés aux formats réellement attendus. Tester un upload “trompeur” (extension différente ou contenu incohérent) pour vérifier le rejet ou le traitement correct. Inspecter le comportement après upload, en particulier l’affichage dans l’interface et tout traitement ultérieur (redimensionnement, conversion, preview).

Si vous devez choisir, privilégiez d’abord le non-exécutabilité côté serveur, puis les permissions et le contrôle de type. Les deux premiers font la différence quand un incident arrive.

Tests réalistes : comment valider sans se mettre en danger

Tester la sécurité, ça peut vite devenir dangereux si on teste n’importe comment. Le but n’est pas de “réussir l’exécution” sur un serveur de production, mais de valider que vos barrières fonctionnent.

Une méthode raisonnable consiste à :

    utiliser des fichiers inoffensifs mais “ambigus” (par exemple des fichiers qui portent une extension trompeuse), observer la réponse HTTP (code statut, en-têtes Content-Type, et chemin), vérifier s’ils sont accessibles en lecture directe, vérifier surtout qu’ils ne déclenchent aucune exécution.

Si vous travaillez avec un environnement de staging, vous pouvez faire des tests plus proches de la réalité. En production, restez sur des tests d’exposition et de contrôle de type, et documentez ce que vous observez. Une validation par observation vaut mieux que des promesses.

Le piège des “PDF et images” : les cas où ça se complique

Vous pourriez vous dire, “on n’accepte que des images et des PDF”. C’est souvent vrai, et pourtant les complications apparaissent.

Un fichier image peut être malformé et déclencher des bugs dans une chaîne de traitement (redimensionnement, prévisualisation). Un PDF peut aussi être lourd, ou contenir du contenu qui ne doit pas être rendu naïvement côté navigateur ou côté serveur si vous faites de l’extraction.

Il y a un autre piège: l’intégration. Parfois l’upload est autorisé correctement, mais une autre fonctionnalité de l’application lit le fichier et en extrait du texte, ou l’intègre dans un workflow. À ce moment-là, même si l’exécution directe est bloquée, une autre surface s’ouvre.

La règle pratique que j’applique est simple: traitez l’upload comme une entrée non fiable pour tout le pipeline. Même si vous bloquez l’exécution PHP, vous devez garder l’hygiène sur les traitements secondaires.

Réduire aussi le risque “avant l’upload” (anti-abus)

Même si votre objectif premier est l’exécution de code, l’abus par upload est un levier d’attaque en soi. Les bots peuvent inonder le stockage, déclencher des coûts, ou tenter de pousser des types exotiques.

Sans transformer ça en usine à gaz, pensez à :

    limiter la taille des fichiers acceptés (côté serveur et côté application), appliquer une politique de taux d’envoi si un upload est exposé à des utilisateurs non connectés, exiger une authentification quand c’est pertinent, et vérifier les logs autour des uploads.

Les contrôles anti-abus sont souvent perçus comme “secondaires”, mais ils empêchent les tentatives répétées et rendent vos journaux plus exploitables quand un vrai incident survient.

Ce que vous pouvez et ne pouvez pas garantir

Il faut être honnête sur un point: dans une application complexe, aucune mesure isolée n’offre une sécurité parfaite. Le blocage de l’exécution via la configuration serveur est extrêmement efficace, mais si un autre composant traite les fichiers de manière dangereuse, une exploitation différente reste possible.

À l’inverse, un contrôle strict du type côté application est utile, mais si la configuration serveur interprète mal certains fichiers, l’upload peut quand même créer un risque.

La combinaison des deux est le cœur de la sécurité WordPress dans ce cas précis.

Politique recommandée pour les cas typiques

Plutôt que d’imposer une doctrine rigide, je donne des règles de décision qui marchent bien.

Si votre site accepte des uploads d’utilisateurs, vous pouvez viser une politique où :

    les types autorisés sont limités et justifiés, le dossier d’uploads est non exécutable, et les traitements de post-upload sont minimaux.

Si vous avez besoin d’accepter des archives compressées, traitez-les comme un cas à part, idéalement avec une extraction dans un environnement isolé et une politique d’extension très stricte à l’intérieur. Les archives sont utiles, mais elles combinent plusieurs couches de risque, parce qu’elles emballent potentiellement plusieurs formats.

Remise en ordre des fichiers existants

Quand vous durcissez, pensez aussi au “passé”. Des fichiers potentiellement dangereux peuvent déjà exister dans uploads. Dans beaucoup de cas, ils ne sont pas exécutables, mais vous ne voulez pas laisser une collection de contenus suspects sans raison.

La question pragmatique est : ces fichiers ont-ils des chances d’être atteignables et exploitables ? Si oui, planifiez un nettoyage ciblé ou une mise en quarantaine. Si non, au minimum, documentez ce que vous avez constaté pour justifier votre décision.

Ce travail est souvent plus lourd que prévu, mais il évite un débat plus tard, quand un audit ou un incident vous force à revenir sur le sujet.

Étapes concrètes pour durcir sans casser le site

Pour terminer, une séquence réaliste, dans l’ordre où j’ai le plus souvent vu les erreurs éviter :

Mettre en place ou vérifier la non-exécutabilité du dossier wp-content/uploads/ sur votre serveur (Apache ou Nginx), puis confirmer le résultat par test. Reserrer les permissions: seuls les rôles qui doivent uploader peuvent le faire, et les endpoints d’upload suivent les mêmes règles. Restreindre les types autorisés aux formats utiles, et ajuster si nécessaire pour éviter les faux rejets. Tester le comportement sur staging, notamment les flux de preview, de téléchargement, et de traitement post-upload. Relire les logs, et surveiller après déploiement, surtout les pics d’uploads et les erreurs de validation.

Dernier regard : l’upload n’est pas “un détail”

Sécuriser l’upload de fichiers, ce n’est pas un ajout ponctuel, c’est une discipline. Le dossier d’uploads doit être un espace où le serveur n’a aucune raison d’exécuter du code. Les validations doivent être cohérentes, côté application et côté serveur. Et autour du fichier, chaque étape doit traiter l’entrée comme non fiable.

image

Quand ces points sont alignés, la sécurité WordPress gagne un niveau de confiance tangible. Et surtout, vous transformez un risque “bruyant” en un problème beaucoup plus difficile à exploiter, même en cas d’erreur ailleurs.

Si vous me décrivez votre configuration (Apache ou Nginx, plugin(s) d’upload, rôles qui uploadent, quels types de fichiers sont autorisés), je peux vous proposer une stratégie plus ciblée, avec les points à vérifier en priorité pour votre contexte.