Imagen Docker PHP Vulnerable: Reconstrucción Correcta tras PHP2Shell

<code>docker pull</code> no soluciona PHP2Shell. Aprende a reconstruir imágenes <code>php:8.x</code> y FrankenPHP, y qué ajustar en tu CI/CD para aplicar el parche en producción.

por Cleverson Gouvêa

Imagen Docker PHP Vulnerable: Reconstrucción Correcta tras PHP2Shell

Una imagen Docker PHP vulnerable no se corrige sola cuando PHP lanza el parche. El contenedor en producción sigue ejecutando el binario con el que fue creado, incluso después de que Docker Hub haya publicado la nueva versión. En esta guía te muestro cómo encontrar cada imagen afectada por PHP2Shell, cómo reconstruir las imágenes php:8.x y FrankenPHP de la manera correcta, qué cambiar en el pipeline de CI/CD y por qué el límite de memoria del contenedor no cierra la vulnerabilidad.

En resumen

  • PHP2Shell son dos vulnerabilidades críticas (CVE-2026-109618 y CVE-2026-109619, CVSS 4.0 = 9.2) que permiten ejecución remota de código sin autenticación. La corrección se publicó el 21/10/2026 en las versiones 8.5.12, 8.4.27, 8.3.36 y 8.2.35.
  • Las imágenes oficiales php:8.x con el parche ya están en Docker Hub desde el 21/10. FrankenPHP lo corrigió en la v1.13.2, el mismo día.
  • docker pull no corrige nada por sí solo. Es necesario reconstruir con una base nueva, hacer push y recrear cada contenedor.
  • Una etiqueta fija antigua, un digest fijado y la caché de construcción retienen la versión antigua sin avisar.
  • El límite de memoria del contenedor (cgroup) protege la disponibilidad, no cierra la vulnerabilidad. La otra CVE ni siquiera depende de la memoria.

¿Por qué una imagen Docker PHP vulnerable sigue siendo vulnerable después del parche?

En un servidor tradicional, apt upgrade reemplaza el paquete y el reinicio de PHP-FPM lo resuelve. En un contenedor, la lógica es diferente: la imagen es inmutable. El PHP que se ejecuta dentro fue compilado y congelado en el momento de la construcción.

Esto crea tres capas que deben actualizarse, en este orden:

  1. La imagen base (php:8.4-fpm, php:8.3-apache, dunglas/frankenphp) necesita tener la versión corregida.
  2. La imagen de tu aplicación, que hereda de la base vía FROM, necesita ser reconstruida y enviada al registro.
  3. Los contenedores en ejecución necesitan ser recreados a partir de la nueva imagen.

Saltarse cualquier capa deja una imagen Docker PHP vulnerable en producción, con apariencia de estar actualizada. El escenario más común que veo es que el equipo ejecute docker pull en el servidor, vea "Downloaded newer image" y cierre la incidencia. La base nueva está en el disco, pero el contenedor antiguo sigue en pie, y la imagen de la aplicación ni siquiera fue reconstruida.

Trabajo con PHP desde 2008 y uso contenedores todos los días, desde el entorno de desarrollo con DDEV hasta los servicios que ofrecemos en desarrollo de sistemas a medida. La diferencia entre un parche aplicado y un parche "descargado" es siempre esta: el proceso que realmente atiende las peticiones.

PHP2Shell en dos minutos: qué cambió entre el 17 y el 23 de octubre

PHP2Shell es el nombre dado a dos vulnerabilidades en el núcleo de PHP. El aviso oficial de php.net indica que están en riesgo "todas las instalaciones PHP que atienden peticiones web", citando PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP y LiteSpeed. El mayor riesgo, según el propio PHP, es mod_php: allí el ataque se dispara antes de cualquier código de la aplicación.

  • CVE-2026-109618 (corrupción por falta de memoria): PHP se queda sin memoria en medio del montaje de la petición, y la limpieza trata datos a medias como válidos.
  • CVE-2026-109619 (estado global obsoleto, un use-after-free): un estado interno sobrevive después de que la memoria ha sido liberada, y la siguiente petición en el mismo worker utiliza ese remanente.

La cronología explica la urgencia. Según las Preguntas Frecuentes de Tenable, Amazon detectó un intento de explotación el 17/10. Los detalles se filtraron el 20/10 y el parche se publicó el 21/10. Palo Alto Networks identificó más de 22,6 millones de instancias expuestas, según TechTarget. El 23/10, PHP publicó una alerta adicional (CVE-2026-115560) sobre un vector improbable relacionado con el upload progress de las sesiones, que se corregirá en la próxima versión regular.

Rama de PHP Situación en PHP Imagen oficial php en Docker Hub
8.5 Corregido en la 8.5.12 Actualizada el 21/10/2026
8.4 Corregido en la 8.4.27 Actualizada el 21/10/2026
8.3 Corregido en la 8.3.36 Actualizada el 21/10/2026
8.2 Corregido en la 8.2.35 (fin de vida el 31/12/2026) Actualizada el 21/10/2026
7.4, 8.0 y 8.1 Solo backports mínimos de código fuente Sin imagen nueva: la etiqueta 8.1-fpm no recibe push desde el 31/12/2025
5.4 a 7.3 Sin corrección Abandonadas (la 7.4-fpm se detuvo el 15/11/2022)

Las fechas de la última columna las verifiqué directamente en la API de Docker Hub y en el repositorio de la imagen oficial. La etiqueta 8.4.27-fpm recibió el primer push el 21/10, a las 16:43 (UTC).

Cómo descubrir qué imágenes son vulnerables

Antes de reconstruir, haz un inventario de cada imagen Docker PHP vulnerable que tengas. En empresas con más de un proyecto, siempre aparece un contenedor olvidado: el panel antiguo, el worker de cola, el entorno de homologación que quedó expuesto.

Pregunta al contenedor, no al host

El PHP del host no dice nada sobre el PHP del contenedor, y una imagen Docker PHP vulnerable puede estar ejecutándose en un servidor cuyo php -v muestre 8.4.27. Tampoco confíes solo en el php -v de la CLI: Datadog advierte que la instalación de línea de comandos puede ser diferente del runtime que atiende la web. Ejecuta la verificación dentro de cada servicio:

# versión de PHP en cada contenedor en ejecución
for c in $(docker ps -q); do
  echo "$(docker inspect --format '{{.Name}}' $c): $(docker exec $c php -r 'echo PHP_VERSION;' 2>/dev/null || echo 'sin php')"
done

# imagen que usa el contenedor vs imagen más nueva en disco
docker inspect --format '{{.Image}}' mi-contenedor
docker image ls --digests php

Revisa el registro, no solo los servidores

Las imágenes oficiales compilan PHP a partir del código fuente y graban la versión en la variable PHP_VERSION. Se puede auditar sin levantar nada:

docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' registry.ejemplo.com/app:prod | grep PHP_VERSION

Un detalle que confunde a mucha gente: como el PHP de la imagen oficial no proviene de un paquete de Debian o Alpine, los escáneres que solo leen la base de datos de paquetes del sistema pueden no detectarlo. La propia Datadog recomienda un inventario separado para runtimes compilados. Si tu imagen Docker PHP vulnerable no aparece en el informe del escáner, esto no prueba que esté segura.

Qué incluir en la lista

  • Contenedores web (FPM, Apache, FrankenPHP).
  • Workers de cola, cron y consumidores que usan la misma imagen.
  • Imágenes de homologación y de previsualización generadas por el CI.
  • Imágenes de terceros (paneles, herramientas internas) que incrustan PHP. Esta suele ser la imagen Docker PHP vulnerable que nadie recuerda reconstruir.

Reconstrucción de imágenes php:8.x: el paso a paso que funciona

Con el inventario listo, corregir una imagen Docker PHP vulnerable sigue una secuencia corta. Lo que cambia de un proyecto a otro es cómo está escrito el FROM.

Etiqueta flotante, etiqueta fija o digest

Cómo está el FROM Ejemplo ¿Recibe el parche solo? Qué hacer
Etiqueta de rama php:8.4-fpm Sí, en la próxima construcción con --pull Reconstruir con --pull
Etiqueta exacta php:8.4.26-fpm No Editar a 8.4.27-fpm y reconstruir
Digest php:8.4-fpm@sha256:... No Actualizar el digest y reconstruir
Rama sin soporte php:8.1-fpm Nunca Migrar la aplicación a 8.2+

La etiqueta exacta y el digest son buenas prácticas de reproducibilidad. El precio es exactamente este: en una corrección urgente, alguien necesita editar el archivo. Si tu proyecto usa digest, la imagen Docker PHP vulnerable queda bloqueada hasta que un humano (o un bot de dependencias) cambie la línea.

Los comandos

# 1. reconstruir con base nueva (y sin caché, si la caché retiene la versión antigua)
docker build --pull --no-cache -t registry.ejemplo.com/app:2026-10-24 .

# 2. verificar la versión ANTES de publicar
docker run --rm registry.ejemplo.com/app:2026-10-24 php -r 'echo PHP_VERSION, PHP_EOL;'

# 3. publicar y recrear los contenedores
docker push registry.ejemplo.com/app:2026-10-24
docker compose pull && docker compose up -d --force-recreate

# 4. validar lo que está en ejecución
docker compose exec web php -r 'echo PHP_VERSION;'

El --no-cache es importante. Sin él, una capa en caché puede reutilizar la base antigua, y la construcción termina "verde" con la misma imagen Docker PHP vulnerable de antes. Recrear el contenedor reinicia todos los workers de PHP. Los volúmenes montados permanecen intactos.

Variantes: actualiza todas

Cada variante es una imagen separada: -fpm, -apache, -cli, -zts, Debian (bookworm, trixie) y -alpine. Comienza por las -apache, que usan mod_php, el escenario que PHP considera de mayor riesgo. Y no publiques el puerto de FPM (9000) fuera de la red interna: FastCGI no fue diseñado para estar expuesto.

FrankenPHP: imagen Docker y binario estático

FrankenPHP merece un capítulo aparte, porque viene en dos formatos y cada uno se actualiza de una manera.

Imagen Docker

La versión v1.13.2, del 21/10/2026, trajo las imágenes con PHP 8.2.35, 8.3.36, 8.4.27 y 8.5.12, citando explícitamente PHP2Shell. Dos días después salió la v1.13.3, que corrige bloqueos (SIGILL) de los binarios estáticos musl en CPUs más antiguas. Una imagen Docker PHP vulnerable basada en FrankenPHP se corrige por el mismo camino que las oficiales. Si tu Dockerfile usa dunglas/frankenphp:1-php8.4 o 1.13-php8.4, la reconstrucción con --pull lo resuelve. Si usa una etiqueta fija anterior, como 1.13.1-php8.4, edita a 1.13.3 o superior.

Binario estático

Aquí reside la trampa. El binario estático de FrankenPHP incrusta PHP dentro del ejecutable. Actualizar el sistema operativo no cambia nada. Actualizar el paquete php de la distribución tampoco. La única corrección es reemplazar el binario por una versión que incluya PHP 8.5.12 (la v1.13.2 o posterior) y reiniciar el servicio.

Quien empaqueta la aplicación entera en un único ejecutable (la característica de "embed" de FrankenPHP) también necesita rehacer esa construcción. Es exactamente el caso que Datadog describe como "aplicaciones que traen su propio intérprete PHP".

Modo worker

En modo worker, FrankenPHP mantiene la aplicación cargada en memoria entre peticiones. La CVE-2026-109619 explota precisamente el estado que sobrevive de una petición a la siguiente en el mismo worker. No hay atajo: actualiza y reinicia todos los workers.

Pipelines CI/CD: cómo no ser atrapado de nuevo

PHP2Shell mostró el tamaño de la ventana entre "parche publicado" y "parche en ejecución". En un pipeline mal diseñado, una imagen Docker PHP vulnerable permanece semanas en producción. En un pipeline bueno, es de horas.

Qué configurar

  1. Reconstrucción programada. Un trabajo semanal (y un disparador manual) que reconstruye la imagen con --pull, incluso sin un nuevo commit. Una base actualizada no genera un commit, por lo que el pipeline que solo se ejecuta con push nunca obtiene el parche.
  2. Bot de dependencias para el FROM. Renovate y Dependabot saben abrir PRs actualizando la etiqueta y el digest de la imagen. Mantienes la reproducibilidad y ganas velocidad.
  3. Puerta de versión mínima. Un paso que falla la construcción si PHP está por debajo del mínimo corregido:
docker run --rm "$IMAGE" php -r 'exit(version_compare(PHP_VERSION, "8.4.27", ">=") ? 0 : 1);' \
  || { echo "PHP por debajo de la versión corregida"; exit 1; }
  1. Etiqueta por fecha o por commit, nunca solo latest. Así el rollback apunta a una imagen conocida, y el registro muestra lo que está en producción.
  2. Escaneo de la imagen final, con el cuidado ya mencionado: verifica la versión de PHP por la variable PHP_VERSION, no solo por la base de datos de paquetes.

Cuánto cuesta

Mantener cada imagen Docker PHP vulnerable bajo control cuesta poco. El coste directo es pequeño: minutos de CI por semana y algo de espacio extra en el registro. El coste real es de ingeniería, de algunas horas a pocos días para estandarizar Dockerfiles, crear el trabajo y probar el rollback. Compara con el coste de una invasión sin autenticación en un servidor expuesto.

Para quienes ejecutan Moodle en un contenedor, la lógica es la misma, con un cuidado extra: verifica la matriz de versiones de PHP de cada lanzamiento de Moodle antes de cambiar de rama. Los requisitos de servidor para Moodle ayudan en este cálculo.

¿Por qué el límite de memoria del contenedor (cgroup) no es una mitigación completa?

Este es el punto que más veo confundido. Como una de las vulnerabilidades implica falta de memoria, parece lógico que un --memory=512m en el contenedor lo resuelva. No lo resuelve. Una imagen Docker PHP vulnerable con límite de memoria sigue siendo vulnerable.

Lo que PHP realmente recomienda

La mitigación oficial para la CVE-2026-109618 es memory_limit=-1, aplicado de forma que la aplicación no pueda sobrescribirlo (php_admin_value en el pool de FPM). Esto elimina el límite controlable de PHP. Pero el propio aviso admite que "un fallo de asignación subyacente aún puede llegar a la ruta vulnerable".

Como memory_limit=-1 permite que un script consuma toda la RAM, PHP sugiere usar un límite de contenedor o cgroup "como barrera de disponibilidad", preferiblemente con una política que mate solo al worker afectado. La palabra es disponibilidad. No es seguridad.

Cuatro razones para no confiar en cgroup

  1. La CVE-2026-109619 no tiene nada que ver con la memoria. Explota un estado obsoleto entre peticiones. Ningún límite de RAM interfiere en ella.
  2. El límite protege al host, no a PHP. Cuando el contenedor supera el cgroup, el kernel mata un proceso. Esto evita que todo el servidor caiga, pero no corrige la corrupción que busca el atacante.
  3. RLIMIT_AS y RLIMIT_DATA empeoran. PHP pide explícitamente no usar estos límites de proceso en lugar del cgroup, porque pueden provocar precisamente el fallo de asignación vulnerable. Cuidado con ulimit -v en scripts de entrypoint: aplica exactamente un RLIMIT_AS.
  4. Matar el proceso equivocado tumba el servicio. Si el OOM killer acierta el proceso maestro de FPM, el contenedor entero se reinicia. Repetido en bucle, esto se convierte en una denegación de servicio provocada por el propio atacante.

Cómo montar la defensa en capas, mientras actualizas

Capa Configuración Rol
PHP memory_limit=-1 vía php_admin_value Elimina el disparador predecible de la 109618
PHP session.upload_progress.enabled=0 Cierra las rutas confirmadas de la 109619
PHP 8.5 fatal_error_backtraces=0 Cierra la ruta del backtrace fatal
Contenedor --memory / limits.memory en Kubernetes Barrera de disponibilidad
Proxy/WAF límite de tamaño, campos y partes multipart Reduce payloads conocidos

Datadog lo resume correctamente: estos cambios "reducen la exposición mientras actualizas, pero ninguno corrige las vulnerabilidades". Si quieres reforzar el perímetro, la guía de cómo proteger un servidor web contra ataques cubre WAF, fail2ban y rate limit.

Cuándo NO hacerlo: trampas que cuestan caro

En una corrección de emergencia, el error más común es la prisa en el lugar equivocado. Huye de estos:

  • No uses etiquetas de release candidate. Etiquetas como 8.5.12RC1 no son la versión final. Usa solo etiquetas estables.
  • No ejecutes apt upgrade dentro del contenedor en ejecución. Además de no reemplazar el PHP compilado de la imagen oficial, el cambio desaparece en el próximo reinicio.
  • No confíes en ini_set() o .user.ini para mitigar. PHP advierte que se aplican demasiado tarde, después de la lectura de la petición.
  • No reconstruyas una imagen 8.1 esperando una corrección. No llegará. Para 7.4, 8.0 y 8.1, PHP solo publicó backports mínimos de código fuente; la salida realista es migrar a 8.2+, preferiblemente 8.3 u 8.4, ya que 8.2 dejará de tener soporte el 31/12/2026.
  • No hagas todo directamente en producción. Cambiar la imagen Docker PHP vulnerable por una de otra rama sin pruebas, cambia un problema por otro. Un cambio de rama (8.1 a 8.4, por ejemplo) puede romper extensiones y código legado. Un parche dentro de la misma rama (8.4.26 a 8.4.27) es de bajo riesgo. Un cambio de rama exige homologación.
  • No olvides el hardening del host. El contenedor se ejecuta sobre un Linux que también necesita estar al día; el checklist de hardening de servidor Linux cubre esta capa.

Y después de actualizar, busca señales de que el ataque ocurrió antes del parche. Datadog enumera indicios como errores de memoria agotada con "Unknown on line 0" en el log, segfaults (señal 11) y, el más directo, un worker web abriendo un shell. Si aparece cualquiera de estos, trátalo como un incidente, no como mantenimiento.

Cómo Agathas Web resuelve esto

Si tienes una o más aplicaciones PHP en un contenedor y no estás seguro de que no quede ninguna imagen Docker PHP vulnerable en producción, es exactamente este tipo de trabajo el que ofrecemos en nuestro servicio de desarrollo y soporte de sistemas.

Qué está incluido

  • Inventario completo: contenedores en ejecución, registro, workers, cron y entornos de homologación, con la versión real de PHP de cada uno.
  • Reconstrucción y publicación: actualización del FROM, construcción sin caché, push, recreación de los contenedores y validación de la versión en ejecución.
  • FrankenPHP y binarios estáticos: reemplazo del binario o de la imagen, incluso en aplicaciones empaquetadas con PHP incrustado.
  • Mitigaciones temporales bien aplicadas mientras la migración de rama no termina, en el nivel correcto (pool de FPM, no .user.ini).
  • Pipeline a prueba del próximo fallo: reconstrucción programada, bot de dependencias, puerta de versión mínima y rollback probado.
  • Migración de PHP legado (7.4, 8.0, 8.1) a una rama soportada, con homologación antes de la producción.

Cómo funciona en la práctica

Comenzamos con un diagnóstico breve: nos das acceso de lectura al registro y a los servidores, y te devolvemos la lista de lo que es vulnerable y el orden de corrección. Luego ejecutamos la reconstrucción en una ventana combinada, con rollback listo. El mismo equipo se encarga de Moodle, servidores Linux, alojamiento gestionado e integraciones con la API oficial de WhatsApp, por lo que toda la aplicación queda bajo la misma supervisión, no solo el contenedor.

Cómo contratar

Contacta con nosotros a través de la página de desarrollo de sistemas de Agathas Web, describe tu stack (cuántos proyectos, qué imágenes, dónde se ejecutan) y te responderemos con el alcance y el plazo. Para urgencias relacionadas con PHP2Shell, indícalo en el primer mensaje.

Conclusión: el parche solo vale cuando llega al proceso

La lección de PHP2Shell para quienes usan Docker es simple. El parche se publicó el 21/10, las imágenes oficiales y FrankenPHP se actualizaron el mismo día, pero nada de esto protege un contenedor que no ha sido reconstruido y recreado. Inventario, reconstrucción con --pull --no-cache, push, recreación y validación de la versión: esa es la secuencia. El límite de memoria del contenedor ayuda a mantener el servicio en pie, pero no cierra la vulnerabilidad.

Si quieres terminar esta semana con la certeza de que no queda ninguna imagen Docker PHP vulnerable, y con un pipeline que detecte el próximo fallo en horas, contacta con Agathas Web.