
PoC per CVE-2026-87902 — path traversal non autenticato nella risoluzione dei page-template di WordPress (inclusione PHP locale, RCE condizionale) con un lab vulnerabile bloccato
Inclusione locale non autenticata di file PHP in WordPress Core tramite un valore
pagename con doppia codifica — e, in specifiche condizioni di deployment,
esecuzione di codice PHP con i privilegi dell'account del web server.
| CVE | CVE-2026-87902 |
| Avviso del fornitore | GHSA-7hp8-65ch-5whp |
| Write-up | CVE-2026-87902: Critical WordPress file inclusion and conditional RCE |
| Affetto | WordPress Core 4.7.0 – 7.1.1 (ogni branch, secondo gli intervalli per branch dell'avviso); riprodotto dinamicamente su 7.0.2 |
| Corretto | 7.1.2 (branch 7.1), 7.0.6 (branch 7.0) e un backport per ogni branch fino a 4.7.37 (secondo l'avviso) |
| Debolezze | CWE-98 (controllo improprio del nome file nell'include), CWE-22 / CWE-23 (path traversal) |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — 8.1 High |
| CVSS v4.0 (supplementare) | 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 Critical |
| Autenticazione | nessuna — nessun account, cookie, nonce, sessione, plugin o richiesta in uscita |
| Interazione utente | nessuna |
| Autore | Robert Ressl — ressl.ch |
| PoC verificato | 2026-09-22 contro il lab in questo repository — vedi Risultati verificati |
WordPress risolve un template di Pagina attraverso una catena che non dimostra mai che il file selezionato rimanga all'interno di una root del tema:
pagename e page_id sono variabili di query pubbliche che WP::parse_request()
accetta da un corpo POST anonimo.pagename sopravvive alla sanitizzazione
iniziale della query come ottetti %xx con escape (wp_basename() non riesce a
vedere %2f come separatore, e sanitize_title_with_dashes() mantiene
deliberatamente gli ottetti validi).page_id valido seleziona poi una Pagina pubblicata reale, mentre il
pagename malevolo rimane nell'oggetto query.get_page_template() chiama successivamente urldecode() su quel valore e
aggiunge un candidato come page-templates/../../../../../../../usr/local/lib/php/pearcmd.php
alla gerarchia dei template.locate_template() e il loader finale del template controllano solo l'esistenza,
la leggibilità e il suffisso .php/.html — mai che il percorso canonico sia
ancora all'interno di una root del tema consentita — e poi lo include.Questo è una primitiva di inclusione locale di file remota non autenticata in
WordPress Core. Sul runtime ufficiale testato (wordpress:php8.3-apache, che
include PEAR e non carica alcun php.ini, quindi register_argc_argv è On),
l'inclusione è stata concatenata attraverso pearcmd.php di PEAR: una prima
richiesta anonima fa sì che config-create scriva PHP controllato dall'attaccante
in /tmp, una seconda richiesta anonima include quel file e lo esegue come
www-data.
La mancata contenimento del percorso in Core è la vulnerabilità. PEAR è solo una via dipendente dall'ambiente dall'inclusione all'esecuzione di codice — non è una dipendenza di WordPress e non è presente o utilizzabile in ogni deployment.
Posizioni del codice verificate (WordPress 7.0.2):
| # | Posizione | Ruolo |
|---|---|---|
| 1 | wp-includes/class-wp.php:18,322-330 | pagename e page_id sono query var pubbliche e vengono lette da $_POST |
| 2 | wp-includes/class-wp-query.php:2205 | sanitize_title_for_query( wp_basename( $query_vars['pagename'] ) ) — %2f non è un separatore per wp_basename() |
| 3 | wp-includes/formatting.php:2283-2289 | sanitize_title_with_dashes() preserva gli ottetti %xx validi invece di rimuoverli |
| 4 | wp-includes/template.php:492 | $pagename_decoded = urldecode( $pagename ); — il traversal diventa attivo dopo la sanitizzazione |
| 5 | wp-includes/template.php:722-736 | locate_template() concatena il candidato sotto ogni root del tema e chiama solo file_exists() |
| 6 | wp-includes/template-loader.php:116-132 | realpath() normalizza il percorso, poi include viene eseguito senza alcun controllo canonico di contenimento nella root |
| 7 | wp-includes/canonical.php:42-47 | redirect_canonical() ritorna anticipatamente per richieste non-GET/HEAD, quindi un POST non viene canonicalizzato via |
La primitiva di inclusione standalone necessita di:
| # | Precondizione | Motivo |
|---|---|---|
| 1 | Una Pagina pubblicata, raggiungibile anonimamente, selezionata tramite page_id numerico | la pagina deve essere restituita dalla query dopo che la ricerca del pathname fallisce |
| 2 | Nessun template di pagina personalizzato risolvibile in precedenza | un template valido assegnato è ordinato prima del candidato malevolo |
| 3 | Una directory di primo livello nel tema attivo (child o parent) il cui nome inizia con page-, ad es. page-templates/ | WordPress antepone il prefisso fisso page-, quindi un traversal .. non può iniziare dalla posizione 0; la directory deve solo esistere ed essere attraversabile — non deve essere scrivibile |
| 4 | Un file .php locale selezionato che esiste ed è leggibile dall'utente PHP | il loader controlla is_file()/is_readable() e richiede un suffisso .php |
| 5 | Nessun confinamento del filesystem che blocchi quel file | un open_basedir o una policy MAC possono impedire l'include |
La fase PEAR dimostrata necessita inoltre di un pearcmd.php leggibile (più le sue
dipendenze), register_argc_argv=On per il SAPI web, e una directory di output
scrivibile. I file php.ini di produzione impostano register_argc_argv=Off;
l'immagine testata non carica alcun php.ini, quindi si è applicato il default
compilato (On). Questo è un limite importante sulla prevalenza della catena di
esecuzione di codice dimostrata.
I temi inclusi Twenty Twenty-Three/Four/Five non forniscono nessuna directory
page-* di primo livello, quindi il lab stock necessita del fixture descritto di
seguito. I temi personalizzati possono legittimamente usare un layout
page-templates/
(documentazione WordPress).
L'avviso registra dove queste condizioni si verificano nel software distribuito: la
condizione del tema è soddisfatta dai temi legacy Twenty Twelve e Twenty Fourteen e
da temi di terze parti come Neve, Hestia e Sydney, mentre la transizione PEAR si
applica all'immagine Docker ufficiale di PHP e alle configurazioni cPanel
predefinite che eseguono PHP precedente alla 8.5. (Twenty Twelve e Twenty Fourteen
includono entrambi una directory page-templates/ di primo livello; le restanti
affermazioni sono dell'avviso.) Questo repository non misura quanto spesso si
applichi la catena completa.
./lab/up.sh # WordPress 7.0.2 + MySQL 8.4 + il fixture page-*, installato e pronto
python3 cve-2026-87902.py # due POST anonimi, stampa il marker di prova
cve-2026-87902.py richiede Python 3.6+ (solo libreria standard) e raggiunge il lab
su http://127.0.0.1:8091 per impostazione predefinita. Output atteso: