FlawOpen

flawopen.com/Teardowns/cve-2026-87902-wordpress-page-template-path-traversal

● CVE-2026-87902 · CVSS 9.2[1] · Crítica
Investigación · FlawOpen

CVE-2026-87902: Traversal de rutas en plantillas de WordPress

Un nombre de página decodificado sin validar entró en la resolución de plantillas, permitiendo incluir PHP local sin autenticación y RCE condicional bajo requisitos concretos de tema y entorno.[1]

💡 Explicación en Lenguaje Sencillo (ELI5)

Imagina a una bibliotecaria que comprueba cada petición. WordPress revisaba una ruta hacia las plantillas, pero no la creada desde el nombre decodificado; ciertos marcadores podían sacarla de las estanterías del tema. El arreglo valida el nombre y mantiene la plantilla dentro de un directorio permitido.[1], [2], [3]

Conceptos Clave y Términos

Resolución de plantillas
WordPress ordena candidatos PHP y busca el primer archivo coincidente en el tema activo.[1], [2]
Traversal de rutas
Componentes como .. pueden salir del directorio previsto si la entrada no se valida.[1]
Inclusión local de PHP
Carga de un archivo PHP ya presente en el servidor; la RCE requiere otras condiciones.[1]
`validate_file()`
Ayudante de WordPress que rechaza componentes de ruta no permitidos antes de usarlos.[3]
CWE-98
CWE-98 — Control inadecuado del nombre de archivo en include/require: la entrada no se restringe correctamente antes de seleccionar un archivo PHP.[1], [7]

Mapeos de marcos

Análisis de Causa Raíz

En get_page_template(), WordPress decodificaba pagename y añadía page-{name}.php sin validate_file(), aunque la rama contigua sí validaba. Así, la ruta podía salir del tema. Se requiere un directorio superior page-* y un PHP local legible; la RCE exige condiciones adicionales de PHP/servidor. La versión 7.1.2 valida el valor decodificado y limita la ruta resuelta en locate_template(). El aviso de WordPress clasifica esta debilidad como CWE-98 (control inadecuado del nombre de archivo usado en include/require de PHP) y acredita a Robert Ressl el descubrimiento y la divulgación responsable.[1], [2], [3], [7]

Alcance de CVE-2026-87902: exposición y RCE condicional

Condición del despliegueQué confirman las fuentes primariasAcción del operador
Rama afectada de WordPress CoreEl GHSA enumera las versiones afectadas por rama, desde 4.7.0–4.7.36 hasta 7.1.0–7.1.1. Las correcciones correspondientes van de 4.7.37 (el backport más antiguo) a 7.1.2 (rama actual), con una versión corregida específica para cada rama enumerada.Comprueba la versión instalada y aplica la corrección de su rama. Actualiza las versiones anteriores a 4.7 a una versión con soporte.
Estructura del tema activoEl tema padre o hijo activo debe contener un directorio de nivel superior cuyo nombre empiece por page- (por ejemplo, page-templates).Revisa los directorios de ambos temas activos. Vuelve a comprobar esta condición si cambia el tema activo.
Archivo PHP local legibleDebe existir un archivo .php elegido fuera de los directorios de los temas activos y ser legible por la cuenta del servidor web.Revisa qué archivos PHP locales puede leer el servidor web; el rango de versiones del CVE no demuestra por sí solo que se haya incluido un archivo.
Entorno para RCE condicionalEl aviso describe RCE solo con condiciones adicionales de PHP/servidor. Sus ejemplos incluyen pearcmd.php con register_argc_argv=On, la imagen oficial de PHP para Docker y configuraciones predeterminadas de cPanel con PHP anterior a 8.5.Evalúa el runtime y la configuración PHP reales. Una versión vulnerable de WordPress no demuestra por sí sola RCE ni compromiso.

Flujo de Ataque Paso a Paso

Paso 1

La entrada llega a la selección

Una petición no autenticada aporta el nombre usado para crear candidatos; no necesita cuenta ni clic.[1]

Paso 2

Se decodifica otra vez

get_page_template() vuelve a decodificar el nombre; las versiones afectadas omitían la validación del candidato decodificado.[2], [3]

Paso 3

La búsqueda sale de la raíz

Con un directorio page-* y un PHP local legible, el tema puede resolver un archivo fuera de su directorio.[1]

Paso 4

Impacto condicional

La RCE depende de otros requisitos PHP/servidor; la versión por sí sola no demuestra explotabilidad ni intrusión.[1]

Código Fuente: Vulnerable vs. Seguro

Este fragmento compara las etiquetas oficiales 7.1.1 y 7.1.2. El parche valida el valor después de decodificarlo; la misma versión también limita las rutas resueltas en locate_template(). Es código upstream, no un parche independiente completo ni una demostración de explotación.[2], [3]

Fragmento de origen: wp-includes/template.php — get_page_template()

IMPLEMENTACIÓN VULNERABLE
if ( $pagename ) {
    $pagename_decoded = urldecode( $pagename );

    if ( $pagename_decoded !== $pagename ) {
        $templates[] = "page-{$pagename_decoded}.php";
    }
}
PARCHE SEGURO Y ROBUSTO
if ( $pagename ) {
    $pagename_decoded = urldecode( $pagename );

    if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
        $templates[] = "page-{$pagename_decoded}.php";
    }
}

Lista de Verificación de Seguridad para Ingeniería

Fuentes