
PoC para CVE-2026-87902 — path traversal não autenticado na resolução de page-template do WordPress (inclusão local de PHP, RCE condicional) com um laboratório vulnerável fixado
Inclusão local de ficheiro PHP não autenticada no WordPress Core através de um valor pagename com dupla codificação — e, sob condições específicas de implementação, execução de código PHP com os privilégios da conta do servidor web.
| CVE | CVE-2026-87902 |
| Aviso do fornecedor | GHSA-7hp8-65ch-5whp |
| Análise | CVE-2026-87902: Critical WordPress file inclusion and conditional RCE |
| Afetado | WordPress Core 4.7.0 – 7.1.1 (todos os ramos, de acordo com os intervalos por ramo do aviso); reproduzido dinamicamente na 7.0.2 |
| Corrigido | 7.1.2 (ramo 7.1), 7.0.6 (ramo 7.0) e um backport para todos os ramos até 4.7.37 (de acordo com o aviso) |
| Fraquezas | CWE-98 (controlo inadequado do nome do ficheiro em 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 (suplementar) | 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 |
| Autenticação | nenhuma — sem conta, cookie, nonce, sessão, plugin ou pedido de saída |
| Interação do utilizador | nenhuma |
| Autor | Robert Ressl — ressl.ch |
| PoC verificado | 2026-09-22 contra o laboratório neste repositório — ver Resultados verificados |
O WordPress resolve um template de Página através de uma cadeia que nunca prova que o ficheiro selecionado permanece dentro de uma raiz de tema:
pagename e page_id são variáveis de consulta públicas que WP::parse_request() aceita de um corpo POST anónimo.pagename sobrevive à sanitização inicial da consulta como octetos %xx escapados (wp_basename() não consegue ver %2f como separador, e sanitize_title_with_dashes() mantém deliberadamente octetos válidos).page_id válido seleciona então uma Página publicada real, enquanto o pagename malicioso permanece no objeto de consulta.get_page_template() chama posteriormente urldecode() sobre esse valor e adiciona um candidato como page-templates/../../../../../../../usr/local/lib/php/pearcmd.php à hierarquia de templates.locate_template() e o carregador final de templates apenas verificam a existência, a legibilidade e o sufixo .php/.html — nunca que o caminho canónico ainda esteja dentro de uma raiz de tema permitida — e depois fazem include do mesmo.Isto é uma primitiva de inclusão local de ficheiro remota não autenticada no WordPress Core. No runtime oficial testado (wordpress:php8.3-apache, que inclui o PEAR e não carrega nenhum php.ini, pelo que register_argc_argv está On), a inclusão foi encadeada através do pearcmd.php do PEAR: um primeiro pedido anónimo faz com que config-create escreva PHP controlado pelo atacante em /tmp, um segundo pedido anónimo inclui esse ficheiro e executa-o como www-data.
A ausência de contenção de caminho no Core é a vulnerabilidade. O PEAR é apenas uma rota dependente do ambiente da inclusão para a execução de código — não é uma dependência do WordPress e não está presente ou utilizável em todas as implementações.
Localizações de código verificadas (WordPress 7.0.2):
| # | Localização | Papel |
|---|---|---|
| 1 | wp-includes/class-wp.php:18,322-330 | pagename e page_id são query vars públicas e são lidas de $_POST |
| 2 | wp-includes/class-wp-query.php:2205 | sanitize_title_for_query( wp_basename( $query_vars['pagename'] ) ) — %2f não é um separador para wp_basename() |
| 3 | wp-includes/formatting.php:2283-2289 | sanitize_title_with_dashes() preserva octetos %xx válidos em vez de os remover |
| 4 | wp-includes/template.php:492 | $pagename_decoded = urldecode( $pagename ); — o traversal torna-se ativo após a sanitização |
| 5 | wp-includes/template.php:722-736 | locate_template() concatena o candidato abaixo de cada raiz de tema e apenas chama file_exists() |
| 6 | wp-includes/template-loader.php:116-132 | realpath() normaliza o caminho, depois include é executado sem qualquer verificação canónica de contenção na raiz |
| 7 | wp-includes/canonical.php:42-47 | redirect_canonical() retorna antecipadamente para pedidos não-GET/HEAD, pelo que um POST não é canonicalizado para fora |
A primitiva de inclusão autónoma necessita de:
| # | Pré-condição | Razão |
|---|---|---|
| 1 | Uma Página publicada e acessível anonimamente, selecionada por page_id numérico | a página tem de ser devolvida pela consulta após a falha da pesquisa do pathname |
| 2 | Nenhum template de página personalizado resolvível anteriormente | um template atribuído válido é ordenado antes do candidato malicioso |
| 3 | Um diretório de nível superior no tema ativo (filho ou pai) cujo nome começa por page-, por exemplo page-templates/ | o WordPress pré-fixa o prefixo fixo page-, pelo que um traversal .. não pode começar na posição 0; o diretório apenas tem de existir e ser atravessável — não tem de ser gravável |
| 4 | Um ficheiro .php local selecionado que exista e seja legível pelo utilizador PHP | o carregador verifica is_file()/is_readable() e exige um sufixo .php |
| 5 | Nenhum confinamento do sistema de ficheiros a bloquear esse ficheiro | uma política open_basedir ou MAC pode impedir a inclusão |
A fase PEAR demonstrada necessita adicionalmente de um pearcmd.php legível (mais as suas dependências), register_argc_argv=On para o SAPI web, e um diretório de saída gravável. Os ficheiros php.ini de produção definem register_argc_argv=Off; a imagem testada não carrega nenhum php.ini, pelo que se aplicou o valor predefinido compilado (On). Este é um limite importante na prevalência da cadeia de execução de código demonstrada.
Os temas incluídos Twenty Twenty-Three/Four/Five não incluem nenhum diretório page-* de nível superior, pelo que o laboratório padrão necessita da fixture descrita abaixo. Os temas personalizados podem legitimamente usar uma estrutura page-templates/ (documentação do WordPress).
O aviso regista onde estas condições ocorrem em software distribuído: a condição do tema é satisfeita pelos temas legados Twenty Twelve e Twenty Fourteen e por temas de terceiros como Neve, Hestia e Sydney, enquanto a transição PEAR se aplica à imagem oficial PHP Docker e a configurações predefinidas do cPanel a executar PHP anterior ao 8.5. (Twenty Twelve e Twenty Fourteen incluem ambos um diretório page-templates/ de nível superior; as restantes afirmações são do aviso.) Este repositório não mede a frequência com que a cadeia completa se aplica.
./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 necessita de Python 3.6+ (apenas biblioteca padrão) e alcança o laboratório em http://127.0.0.1:8091 por predefinição. Saída esperada: