Backend 7 min de lectura 1306 palabras

PHP2Shell: ya hay parche, y así he actualizado mi imagen Docker de PHP

EN
PHP2Shell: ya hay parche, y así he actualizado mi imagen Docker de PHP

Esta mañana escribía sobre qué hacer con los servidores PHP mientras llegaba el parche para PHP2Shell. A las 14:00 UTC, como estaba previsto, PHP ha publicado las versiones corregidas y los detalles de los dos fallos. Toca actualizar, y en mi caso eso empezaba por una imagen Docker que mantengo y que usan todos mis servicios PHP.

Las versiones corregidas

RamaVersión corregida
PHP 8.58.5.12
PHP 8.48.4.27
PHP 8.38.3.36
PHP 8.28.2.35

PHP 8.6.0 RC4, que aún está en pruebas, también incluye las correcciones. Para PHP 8.1 y anteriores no hay versiones oficiales, pero dada la gravedad, el equipo de seguridad ha publicado parches mínimos para 7.4, 8.0 y 8.1 para quien compile desde el código fuente. Y recuerdan que PHP 8.2 deja de tener soporte el 31 de diciembre de 2026.

Según el anuncio de php.net, están en riesgo todas las instalaciones de PHP que sirven peticiones web: PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP y LiteSpeed. El riesgo más alto es el de mod_php, donde han demostrado que una petición sin autenticar puede lanzar el ataque antes de que se ejecute una sola línea de la aplicación.

Los dos fallos

Ahora que los avisos son públicos, se entiende mejor lo que pasaba. Los dos son de tipo use-after-free, con una puntuación CVSS v4 de 9,2.

CVE-2026-109618, RCE por corrupción en falta de memoria (GHSA-w8hw-w9fx-j79w). Cuando PHP se queda sin memoria, abandona la petición con un longjmp(). Si eso ocurre mientras hay datos internos de la petición a medio inicializar, la limpieza posterior puede tratarlos como válidos. Un atacante puede provocar la falta de memoria sin pasarse de los límites aparentes de PHP, por ejemplo con peticiones multipart sin una longitud de contenido utilizable, con un número ilimitado de cabeceras por parte o con campos de fichero vacíos y nombres anidados. En mod_php lo han validado con los límites por defecto. En PHP-FPM lo consideran vulnerable por el análisis del código, aunque no han demostrado un exploit completo. La corrección elimina el longjmp() y lo sustituye por una salida ordenada.

CVE-2026-109619, RCE por estado global obsoleto (GHSA-6wmf-4rv5-p3rp). Un worker de PHP guarda estado global (errores, sesión, objetos, extensiones) que puede apuntar a memoria de una petición concreta. Si un error fatal se salta el reinicio de ese estado, la siguiente petición que atienda el mismo worker puede usarlo después de liberado. Hacen falta dos piezas: una petición que deje el estado colgando y otra posterior que lo use antes de inicializarse. Los caminos confirmados son el progreso de subida de las sesiones, validado en mod_php y en FPM, y las trazas de errores fatales de PHP 8.5.

Una corrección a lo que escribí esta mañana

Por la mañana, siguiendo lo que se sabía, contaba que desactivar session.upload_progress cubría uno de los dos caminos y que el otro no tenía solución conocida dentro de PHP. Con los avisos oficiales, el reparto queda más claro:

FalloMedidas provisionales que proponen los avisos
CVE-2026-109618 (falta de memoria)memory_limit=-1 fijado por el administrador, post_max_size bajo (8M sin subidas, 32M con subidas), límites de tamaño, campos y partes en el proxy, bloquear multipart si no se usa, límite de memoria del contenedor o cgroup
CVE-2026-109619 (estado obsoleto)session.upload_progress.enabled=0, fatal_error_backtraces=0 en PHP 8.5, evitar errores fatales y tiempos de ejecución agotados que pueda provocar un atacante

Hay dos cosas que conviene corregir de la guía de esta mañana. La primera: desactivar el upload progress sólo afecta al segundo fallo, y ni siquiera lo cierra entero. La segunda: un post_max_size bajo sí reduce la exposición al primer fallo, según el aviso oficial. Y hay un detalle importante para quien use contenedores: limitar la memoria con RLIMIT_AS o RLIMIT_DATA es contraproducente, porque provoca justo el fallo de asignación vulnerable, mientras que un límite de cgroup como el de Docker sirve como barrera.

Aun así, los dos avisos insisten en lo mismo: la única mitigación completa es actualizar. El upload progress sí se puede dejar desactivado después de parchear si no se usa, como recomienda el propio aviso.

Cómo he actualizado mi imagen Docker

Mantengo drzippie/php-nginx, una imagen con PHP-FPM 8.4 y Nginx sobre Alpine Linux. Es un fork de TrafeX/docker-php-nginx al que he ido añadiendo lo que necesitaban mis proyectos: Swoole, ImageMagick, APCu, Redis, Composer, Node.js y soporte para WordPress y las colas de Laravel. Todos mis servicios PHP con Docker parten de ella, así que era lo primero que había que actualizar.

La última versión publicada llevaba PHP 8.4.14 sobre Alpine 3.21, vulnerable. El salto ha sido mayor que cambiar un número de versión:

  • Alpine 3.24 como base, con PHP 8.4.27, Nginx 1.30 y Node.js 24.
  • Una vía para compilar PHP cuando Alpine todavía no tiene el paquete. En una actualización urgente, las distribuciones pueden tardar horas o días en publicar el paquete, así que he añadido al Dockerfile una etapa opcional que compila la versión de PHP indicada con el propio APKBUILD de Alpine.
  • Sólo AMD64 y ARM64. Alpine 3.24 no tiene el paquete de Swoole para PHP 8.4 en ARM de 32 bits, así que he dejado de publicar ARMv6 y ARMv7.

La etapa de compilación descarga la receta php84 de aports, cambia la versión y comprueba el SHA-256 del código fuente contra el que publica php.net antes de compilar:

# pkgrel=100 makes our build win over the same version in the official repo
RUN sed -i -e "s/^pkgver=.*/pkgver=${PHP_VERSION}/" -e 's/^pkgrel=.*/pkgrel=100/' \
      -e '/^pkgrel=/a options="!check"' APKBUILD && \
    export SRCDEST=/distfiles REPODEST=/packages && \
    abuild -F checksum && \
    echo "${PHP_SHA256}  /distfiles/php-${PHP_VERSION}.tar.xz" | sha256sum -c - && \
    apk update && abuild -F -r

Por defecto se usan los paquetes oficiales de Alpine, y la compilación se activa con argumentos de construcción:

docker buildx build --build-arg PHP_SOURCE=build \
  --build-arg PHP_VERSION=8.4.27 --build-arg PHP_SHA256=<sha256 de php.net> .

El pkgrel=100 hace que el paquete compilado gane al oficial de la misma versión, y comprobar el SHA-256 enlaza con lo que contaba hace unos días sobre la integridad del código en los gestores de paquetes: si vas a compilar PHP con prisa, al menos que sea el PHP que ha publicado php.net.

También he actualizado la documentación en Docker Hub con un aviso de seguridad al principio y una tabla con la versión de PHP de cada etiqueta, dejando claro que las anteriores son vulnerables:

EtiquetaPHPAlpineNotas
latest, 2026-10-218.4.273.24Corrige CVE-2026-109618 y CVE-2026-109619
2026-07-188.4.143.21Vulnerable, no usar

Actualizar los servicios

Con la imagen publicada, he actualizado todos mis servicios que van con Docker. Con Compose, el proceso para cada uno es descargar la imagen nueva y recrear los contenedores:

docker compose pull
docker compose up -d

Y comprobar la versión, tanto en la imagen como en el contenedor de cada servicio:

docker run --rm drzippie/php-nginx php -v
docker compose exec app php -v

Lo que más cuesta en estos casos no es el comando, sino no dejarse nada: el servicio que usa una etiqueta fija en lugar de latest, el contenedor que no se ha recreado porque nadie le ha hecho up o la imagen construida hace meses a partir de una base que ya no se actualiza. Por eso me ha parecido importante la tabla de etiquetas en Docker Hub: quien use la imagen puede comprobar de un vistazo si la suya es vulnerable.

Lo que me llevo

Ha sido un día intenso, pero con un final ordenado: aviso previo, parches a la hora anunciada, avisos técnicos detallados y hasta correcciones mínimas para versiones sin soporte. Mantener una imagen base propia tiene sus ventajas, porque actualizarla actualiza todo lo que depende de ella, pero también la responsabilidad de hacerlo rápido. Y en un día como hoy, la diferencia entre esperar a que Alpine empaquete la versión y poder compilarla uno mismo puede contarse en horas de exposición.

Fuentes: anuncio de seguridad de PHP, GHSA-w8hw-w9fx-j79w y GHSA-6wmf-4rv5-p3rp.