Resumen
El investigador reportó un fallo en la línea de desarrollo de WordPress 7.1 (confirmado desde la Beta 2 hasta la compilación 7.1-RC2-63095-src) que no afecta a la rama estable 7.0 (7.0.3 en sus pruebas). El reporte se envió antes del lanzamiento final, previsto para el 19 de agosto de 2026, precisamente para que se corrigiera antes de llegar a usuarios estables.
Bajo unas condiciones concretas, un usuario autenticado con el rol Autor (el de menor privilegio que puede subir medios) podía aprovechar una condición de carrera entre varios flujos de tratamiento de imágenes para conseguir que WordPress/Imagick generara ficheros de imagen persistentes con extensión .php cuyos metadatos XMP contenían código PHP válido. El resultado es ejecución remota de código persistente en el servidor.
La clave está en combinar tres flujos que, por separado, parecen inofensivos: la finalización de un medio, la descarga de un medio por URL y el post-procesado de imágenes.
Precondiciones
El camino de ejecución directa por HTTP exige que el directorio temporal elegido por WordPress quede bajo una ruta servida por PHP. Si /tmp no es escribible por el usuario que ejecuta PHP, WordPress recurre a otro directorio temporal escribible; en la reproducción en Docker, dejar /tmp escribible solo por root hacía que WordPress cayese al directorio wp-content.
El investigador señala que este escenario puede darse en contenedores con readOnlyRootFilesystem, según cómo se monte /tmp: si no se ofrece un /tmp escribible (tmpfs/emptyDir) para el usuario web, WordPress puede escoger otro directorio de respaldo.
Además, la demostración de RCE requiere Imagick, que el directorio temporal escogido se sirva por PHP, que se ejecute PHP en la ubicación resultante y, por supuesto, ganar la ventana de la carrera.
Pasos de reproducción
El flujo de ataque usa la subida inicial de una imagen más tres acciones REST adicionales.
Paso 1 — Subir una imagen sin generar subtamaños:
POST /wp-json/wp/v2/media?generate_sub_sizes=false
Paso 2 — Finalizar el medio:
POST /wp-json/wp/v2/media/{id}/finalize
Esto envenena los metadatos de la adjunta semilla para que original_image apunte a la ruta temporal .php elegida por el atacante.
Paso 3 — Subir un medio por URL:
POST /wp-json/wp/v2/media
con el parámetro url. WordPress descarga el JPEG controlado por el atacante; una cabecera Content-Disposition remota hace que el nombre base .php predecible exista temporalmente en el directorio temporal escogido por WordPress.
Paso 4 — Correr la carrera contra el post-procesado:
POST /wp-json/wp/v2/media/{id}/post-process?action=create-image-subsizes
Se lanza esta petición en carrera con la del paso 3. Cuando la carrera tiene éxito, Imagick abre el fichero como imagen y WordPress genera derivados de imagen persistentes con extensión .php.
La imagen maliciosa se sirve con estas cabeceras para forzar el nombre con extensión ejecutable:
Content-Type: image/jpeg
Content-Disposition: attachment; filename="payload.php"
El reporte indica que el fichero de prueba pesaba unos 50 MB y llevaba en sus metadatos de imagen una llamada phpinfo(); como prueba de ejecución de PHP.
Causa raíz
El investigador descompone el problema en tres piezas, cada una con su referencia al código de wordpress-develop:
- El flujo de medios por URL (
/wp-json/wp/v2/media) descarga la imagen remota condownload_url(), que respeta el valor deContent-Disposition: attachment; filename=malicious.phpdel servidor remoto y renombra el fichero temporal local a ese nombre base influido por el atacante. Así, una imagen válida existe brevemente con una extensión ejecutable como.php.
- El post-procesado (
/wp-json/wp/v2/media/{seed_id}/post-process?action=create-image-subsizes) corre contra ese fichero temporal e invocawp_update_image_subsizes(), que toma la ruta de la imagen original de los metadatos de la adjunta y se la pasa al editor de imágenes. Si la extensión no corresponde a un tipo MIME conocido,wp_get_image_editor()deja deliberadamente que el editor determine el formato por el contenido en vez de rechazarlo por la extensión. Además, el generador de nombres de los derivados reutiliza la extensión de origen cuando no se le pasa una de reemplazo, de modo que.phpse propaga a los nombres de los subtamaños generados.
- La ruta de finalización (
/wp-json/wp/v2/media/{id}/finalize) permite persistir metadatos de nombre/ruta controlados por el atacante y tratarlos luego como ruta local de confianza. La ruta aceptafileyoriginal_imagecomo cadenas sin restricciones, yfinalize_item()asigna directamente eloriginal_imagerecibido a los metadatos de la adjunta sin validación suficiente.
Impacto
Con las precondiciones cumplidas, un atacante con una cuenta Autor por defecto podía lograr ejecución remota de código persistente. Los derivados .php generados:
- Persisten después de borrarse el fichero temporal de origen.
- Se pueden invocar por HTTP sin autenticación.
- Se ejecutan con los privilegios del usuario del servidor web/PHP.
- Pueden exponer credenciales de base de datos, salts de autenticación, ficheros privados y otros secretos de la aplicación.
- Habilitan manipulación de la base de datos, creación de cuentas de administrador, compromiso de contenidos, persistencia de malware y movimiento lateral.
Qué aprender de este caso
- Cuando un endpoint de subida acepta descargar un recurso por URL (como
download_url()de WordPress), revisa si honra la cabeceraContent-Disposition: filename=del servidor remoto para nombrar el fichero local: eso permite inyectar extensiones ejecutables en un nombre que de otro modo sería inofensivo. - Desconfía de rutas o nombres de fichero que el cliente guarda en un sitio (aquí,
original_imagevíafinalize) y que otro flujo lee más tarde como ruta de confianza. Busca campos de ruta/nombre aceptados como cadenas libres y validados en el momento de persistir, no en el de usar. - Las ventanas de carrera aparecen cuando un fichero temporal con nombre predecible existe un instante entre dos operaciones; si un segundo endpoint puede dispararse en paralelo contra ese fichero, el atacante puede colar un estado intermedio. Al diseñar, trabaja sobre rutas no adivinables y confirma el tipo real antes de generar derivados que hereden la extensión de origen.
- No dejes que el nombre de un derivado reutilice la extensión del fichero fuente sin forzar una extensión segura acorde al tipo MIME detectado por contenido: separar "qué formato es" de "con qué extensión se escribe" corta que
.phpse propague a los ficheros generados.