Les failles que nous avons corrigées en 2026, et pourquoi nous les publions

En juillet, une revue de sécurité de StormPress a trouvé quatre défauts, dont trois sérieux, corrigés depuis la version 4.1.4 et complétés en 4.1.8. En octobre, la préparation de la 4.2.0 en a fait apparaître cinq autres, moins graves. Voici ce qu'ils permettaient, ce que nous avons changé, et ce que nous en avons appris.

Publier ses propres failles n'est pas confortable. Nous le faisons parce que vous ne pouvez pas évaluer le risque de vos sites sans savoir ce qui a été corrigé, et depuis quand ; et parce qu'une extension qui prétend vous aider à maîtriser WordPress doit s'appliquer l'exigence qu'elle vous propose. Chaque défaut est décrit au niveau de détail utile pour juger du risque, sans mode d'emploi.

1. Des droits vérifiés par rôle, pas par article

Le défaut. Les routes de StormPress qui lisent, modifient, publient ou suppriment un article vérifiaient que l'utilisateur pouvait éditer des articles en général, pas celui-là. Un contributeur — le rôle le plus bas qui puisse écrire — pouvait ainsi agir sur les articles d'autres auteurs.

La correction. Chaque route contrôle désormais le droit sur l'objet visé : edit_post, delete_post ou le droit de publier cet article. C'est la règle de WordPress lui-même, et nous aurions dû l'appliquer dès le départ. En septembre, une revue complémentaire a trouvé la même omission sur la restauration depuis la corbeille, et une liste qui laissait voir les brouillons des autres : les deux sont corrigés en 4.1.8.

2. Un jeton de session qui ouvrait trop de portes

Le défaut. L'interface de StormPress reçoit un jeton de session pour parler à son API. Ce jeton était émis pour tout utilisateur connecté, y compris un simple abonné, et l'explorateur de fichiers l'acceptait. Or cet explorateur pouvait atteindre toute l'installation et y écrire des fichiers PHP. Mis bout à bout, ces éléments permettaient à un compte abonné de faire exécuter du code sur le serveur. C'est le défaut le plus grave de tous.

La correction. Elle agit à plusieurs niveaux, pour qu'aucune défaillance isolée ne suffise :

  • le jeton porte une portée limitée à l'interface d'administration, que l'explorateur de fichiers refuse ;
  • depuis le navigateur, l'explorateur est réservé aux administrateurs ; les appels de StormeoOS restent dans le périmètre que StormeoOS leur attribue : le dossier précisé dans leur jeton signé, ou à défaut l'installation WordPress ;
  • l'écriture, la suppression et le renommage refusent les fichiers sensibles ;
  • depuis la 4.1.8, le jeton n'est plus émis qu'aux utilisateurs qui peuvent éditer des contenus, et il n'est accepté qu'accompagné de la session WordPress de son titulaire.

Entre la 4.1.4 et la 4.1.7, sur un site relié à StormeoOS, un abonné recevait donc encore ce jeton. Il n'ouvrait plus l'explorateur de fichiers, mais les autres routes l'acceptaient : il permettait notamment de lire la liste des commandes WooCommerce, avec le nom et l'adresse e-mail des clients. Depuis la 4.1.8, quand la demande vient du navigateur, cette liste exige en outre le droit de gérer les commandes (edit_shop_orders).

3. Des clés stockées en clair

Le défaut. Les clés d'API des fournisseurs d'IA, et la clé qui relie le site à StormeoOS, étaient enregistrées en clair dans la base. Toute copie de la base — sauvegarde, préproduction, export — les emportait lisibles.

La correction. Elles sont chiffrées au repos en AES-256-GCM, avec une clé dérivée des clés secrètes de wp-config.php (AUTH_KEY, SECURE_AUTH_KEY), qui ne sont pas stockées en base sur une installation standard. Les clés existantes sont chiffrées automatiquement à leur première lecture après la mise à jour. Le mécanisme et ses limites sont détaillés dans un article dédié.

4. Un chemin d'authentification mort

Le défaut. Une ancienne méthode d'authentification appelait une fonction qui n'existait plus : pour un client anonyme, la requête finissait en erreur fatale PHP. Pas d'accès indu, mais du code mort sur un chemin d'authentification est toujours un mauvais signe.

La correction. La branche a été supprimée.

Pourquoi la version 4.1.4, et pas plus tôt

Les correctifs ont été écrits mi-juillet, mais ne sont arrivés sur les sites qu'avec la 4.1.4, début août. La cause est banale : le numéro de version dans les sources n'avait pas été incrémenté, et la version distribuée était antérieure aux correctifs. Les sources portaient même un numéro inférieur à celui de la version déjà installée : aucun site ne pouvait se voir proposer une mise à jour vers un numéro plus ancien que le sien.

Nous en avons tiré une règle : un correctif de sécurité n'est pas livré quand il est écrit, ni quand il est fusionné, mais quand l'archive téléchargeable le contient. Nous le vérifions en ouvrant l'archive elle-même.

Octobre : cinq défauts trouvés en préparant la 4.2.0

En reprenant l'écran Médias, la recherche ⌘K, les commandes de la palette et le connecteur embarqué pour la 4.2.0, nous avons trouvé cinq autres défauts. Aucun n'approche la gravité du jeton de session de juillet : tous supposent un compte déjà connecté. Deux permettaient de voir ou de modifier un peu plus que ce que WordPress accorde, un troisième de charger lourdement le site en une requête ; les deux autres, la génération d'extrait et, sur WordPress 6.9 et suivants, la lecture d'intégrité, étaient latents, faute de chemin pour les atteindre.

La médiathèque ouverte aux contributeurs. La liste et le détail des médias ne demandaient que le droit de rédiger. Un contributeur, à qui WordPress n'ouvre pas la médiathèque faute de pouvoir téléverser, pouvait y lister les médias du site (hors médias privés) : titres, noms de fichiers et adresses, y compris ceux rattachés aux brouillons d'autres auteurs, et en afficher le détail par identifiant : auteur, texte alternatif, légende, description. La liste et le détail exigent désormais upload_files, comme la médiathèque de WordPress ; sans ce droit, les deux sont refusés, même pour ses propres fichiers. En sens inverse, les médias privés apparaissent désormais dans la liste pour qui peut lire les contenus privés (read_private_posts).

Une génération d'extrait sans contrôle par article. La génération d'un extrait par IA vérifiait le droit de rédiger en général, pas celui de modifier cet article : la même omission que le premier défaut de juillet. Elle contrôle désormais edit_post sur l'article visé, avant tout appel au modèle. Jusqu'à la 4.1.8, la commande /excerpt de la palette échouait toujours et n'atteignait pas cette fonction ; la remettre en service sans ce contrôle aurait ouvert la porte.

Des médias rangés sans droit sur eux. Déplacer un média dans un dossier ou lui poser des étiquettes ne vérifiait aucun droit sur ce média : un compte autorisé à rédiger pouvait réorganiser le classement des fichiers d'autrui, sans toucher aux fichiers eux-mêmes. Chaque route qui vise un média contrôle désormais le droit sur l'objet avant d'agir : edit_post pour déplacer et étiqueter, delete_post pour supprimer. Créer un dossier exige upload_files.

Une liste sans limite. La liste des articles acceptait per_page=-1 et renvoyait alors en une réponse tous les articles visibles par l'utilisateur. Rien de plus n'était exposé, mais une seule requête pouvait peser lourd sur un site bien rempli. Le nombre de résultats par page est désormais borné entre 1 et 100, comme pour la liste des commandes WooCommerce, qui acceptait la même valeur.

Une lecture d'intégrité qui écrivait. Dans le connecteur embarqué, l'ability stormeo/get-core-integrity compare l'empreinte de fichiers sensibles du cœur de WordPress à une référence. Exposée aux agents MCP comme une lecture, et réservée aux administrateurs, elle enregistrait pourtant cette référence à son premier appel quand il n'en existait pas : sur un site déjà altéré, l'état altéré serait devenu la référence. Depuis le connecteur 3.13.7, elle n'écrit plus rien : sans référence, chaque fichier est signalé no_baseline. La référence est établie par le scan de sécurité quotidien du connecteur, à son premier passage, que le site soit relié à StormeoOS ou non ; sur un site relié, StormeoOS peut aussi lancer ce scan ou réinitialiser la référence. Jamais par une lecture d'agent. Sur WordPress 6.9 et suivants, ce défaut est resté latent : jusqu'au connecteur 3.13.6, un autre défaut y empêchait l'enregistrement de toutes ses abilities, celle-ci comprise.

Ce qui a changé dans notre façon de travailler

  • Le contrôle par objet est la règle par défaut, y compris dans nos exemples publics, comme notre tutoriel sur les outils du copilote.
  • Les secrets ne sont jamais stockés en clair, même ceux qui ne servent « que » en interne.
  • Chaque version passe une suite de tests automatisés (PHPUnit côté serveur, Vitest côté interface) et une analyse statique PHPStan.
  • Nous testons sur une vraie installation WordPress avant de déclarer une fonction terminée ; quand cet essai n'a pas pu être fait, la limite est écrite dans l'annonce de la version. C'est ainsi qu'est apparu le défaut d'enregistrement des abilities, que nos tests simulés masquaient. D'autres trouvailles du même ordre sont racontées dans un article à part.

Signaler un problème de sécurité

Si vous pensez avoir trouvé une faille dans StormPress, écrivez-nous à hello@stormeo.io en précisant « sécurité » dans l'objet, plutôt que de la publier. Nous répondons, nous corrigeons, et nous le racontons ici.

StormPress Lite est gratuit, sous licence GPL, et se désactive en un clic. Deux minutes pour l'essayer sur votre site.

Télécharger StormPress Lite