
PoC pour CVE-2026-87902 — traversée de chemin non authentifiée dans la résolution de modèle de page WordPress (inclusion PHP locale, RCE conditionnelle) avec un lab vulnérable épinglé
Inclusion locale non authentifiée de fichier PHP dans WordPress Core via une valeur
pagename doublement encodée — et, dans des conditions de déploiement spécifiques,
exécution de code PHP avec les privilèges du compte du serveur web.
| CVE | CVE-2026-87902 |
| Avis du fournisseur | GHSA-7hp8-65ch-5whp |
| Analyse détaillée | CVE-2026-87902: Critical WordPress file inclusion and conditional RCE |
| Affecté | WordPress Core 4.7.0 – 7.1.1 (toutes les branches, selon les plages par branche de l'avis) ; reproduit dynamiquement sur 7.0.2 |
| Corrigé | 7.1.2 (branche 7.1), 7.0.6 (branche 7.0) et un backport pour chaque branche jusqu'à 4.7.37 (selon l'avis) |
| Faiblesses | CWE-98 (contrôle incorrect du nom de fichier dans include), CWE-22 / CWE-23 (traversée de chemin) |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — 8.1 Élevé |
| CVSS v4.0 (supplémentaire) | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N — 9.2 Critique |
| Authentification | aucune — aucun compte, cookie, nonce, session, plugin ou requête sortante |
| Interaction utilisateur | aucune |
| Auteur | Robert Ressl — ressl.ch |
| PoC vérifié | 2026-09-22 contre le lab de ce dépôt — voir Résultats vérifiés |
WordPress résout un modèle de Page via une chaîne qui ne prouve jamais que le fichier sélectionné reste à l'intérieur d'une racine de thème :
pagename et page_id sont des variables de requête publiques que WP::parse_request()
accepte depuis un corps POST anonyme.pagename survit à l'assainissement précoce
de la requête sous forme d'octets %xx échappés (wp_basename() ne peut pas voir %2f
comme un séparateur, et sanitize_title_with_dashes() conserve délibérément les octets
valides).page_id valide sélectionne alors une vraie Page publiée, tandis que le pagename
malveillant reste dans l'objet de requête.get_page_template() appelle ensuite urldecode() sur cette valeur et ajoute un
candidat tel que page-templates/../../../../../../../usr/local/lib/php/pearcmd.php
à la hiérarchie des modèles.locate_template() et le chargeur de modèle final ne vérifient que l'existence, la
lisibilité et le suffixe .php/.html — jamais que le chemin canonique se trouve
encore dans une racine de thème autorisée — puis l'include.Il s'agit d'une primitive d'inclusion locale de fichier distante non authentifiée dans
WordPress Core. Sur le runtime officiel testé (wordpress:php8.3-apache, qui embarque
PEAR et ne charge aucun php.ini, donc register_argc_argv est On), l'inclusion a été
enchaînée via le pearcmd.php de PEAR : une première requête anonyme fait écrire par
config-create du PHP contrôlé par l'attaquant dans /tmp, une seconde requête anonyme
inclut ce fichier et l'exécute en tant que www-data.
Le confinement de chemin manquant dans Core est la vulnérabilité. PEAR n'est qu'une route dépendante de l'environnement de l'inclusion vers l'exécution de code — ce n'est pas une dépendance de WordPress et il n'est pas présent ou utilisable dans tous les déploiements.
Emplacements source vérifiés (WordPress 7.0.2) :
| # | Emplacement | Rôle |
|---|---|---|
| 1 | wp-includes/class-wp.php:18,322-330 | pagename et page_id sont des variables de requête publiques et sont lues depuis $_POST |
| 2 | wp-includes/class-wp-query.php:2205 | sanitize_title_for_query( wp_basename( $query_vars['pagename'] ) ) — %2f n'est pas un séparateur pour wp_basename() |
| 3 | wp-includes/formatting.php:2283-2289 | sanitize_title_with_dashes() conserve les octets %xx valides au lieu de les supprimer |
| 4 | wp-includes/template.php:492 | $pagename_decoded = urldecode( $pagename ); — la traversée devient active après l'assainissement |
| 5 | wp-includes/template.php:722-736 | locate_template() concatène le candidat sous chaque racine de thème et n'appelle que file_exists() |
| 6 | wp-includes/template-loader.php:116-132 | realpath() normalise le chemin, puis include s'exécute sans vérification canonique de confinement à la racine |
| 7 | wp-includes/canonical.php:42-47 | redirect_canonical() retourne tôt pour les requêtes non-GET/HEAD, donc un POST n'est pas canonicalisé |
La primitive d'inclusion autonome nécessite :
| # | Précondition | Raison |
|---|---|---|
| 1 | Une Page publiée, accessible anonymement, sélectionnée par un page_id numérique | la page doit être retournée par la requête après l'échec de la recherche du nom de chemin |
| 2 | Aucun modèle de page personnalisé résoluble antérieur | un modèle assigné valide est ordonné avant le candidat malveillant |
| 3 | Un répertoire de premier niveau dans le thème actif (enfant ou parent) dont le nom commence par page-, par ex. page-templates/ | WordPress préfixe le préfixe fixe page-, donc une traversée .. ne peut pas commencer à la position 0 ; le répertoire doit seulement exister et être traversable — il ne doit pas être inscriptible |
| 4 | Un fichier .php local sélectionné qui existe et est lisible par l'utilisateur PHP | le chargeur vérifie is_file()/is_readable() et exige un suffixe .php |
| 5 | Aucun confinement du système de fichiers bloquant ce fichier | un open_basedir ou une politique MAC peut empêcher l'inclusion |
L'étape PEAR démontrée nécessite en plus un pearcmd.php lisible (ainsi que ses
dépendances), register_argc_argv=On pour le SAPI web, et un répertoire de sortie
inscriptible. Les fichiers php.ini de production définissent register_argc_argv=Off ;
l'image testée ne charge aucun php.ini, donc la valeur par défaut compilée (On)
s'appliquait. C'est une limite importante sur la prévalence de la chaîne d'exécution de
code démontrée.
Les thèmes fournis Twenty Twenty-Three/Four/Five n'embarquent aucun répertoire
page-* de premier niveau, donc le lab standard nécessite la fixture décrite ci-dessous.
Les thèmes personnalisés peuvent légitimement utiliser une disposition page-templates/
(documentation WordPress).
L'avis consigne où ces conditions se produisent dans les logiciels livrés : la condition
de thème est remplie par les thèmes hérités Twenty Twelve et Twenty Fourteen et par des
thèmes tiers tels que Neve, Hestia et Sydney, tandis que la transition PEAR s'applique à
l'image Docker PHP officielle et aux configurations cPanel par défaut exécutant PHP
antérieur à 8.5. (Twenty Twelve et Twenty Fourteen embarquent tous deux un répertoire
page-templates/ de premier niveau ; les autres affirmations sont celles de l'avis.) Ce
dépôt ne mesure pas la fréquence d'application de la chaîne complète.
./lab/up.sh # WordPress 7.0.2 + MySQL 8.4 + the page-* fixture, installed and ready
python3 cve-2026-87902.py # two anonymous POSTs, prints the proof marker
cve-2026-87902.py nécessite Python 3.6+ (bibliothèque standard uniquement) et atteint le
lab à http://127.0.0.1:8091 par défaut. Sortie attendue :