El parche era solo el principio – Cómo proteger de verdad un servidor PHP después de PHP2Shell

Bastidores de servidores en un centro de datos

Idioma / Language: Español · English

Guía de defensa para administradores y programadores, rama por rama

Actualizar PHP cerró la puerta, pero no dice si alguien entró antes ni evita la próxima crisis. Esta guía técnica, continuación de nuestro análisis de la filtración de PHP2Shell, explica rama por rama cómo saber qué versión ejecuta realmente un servidor, qué condiciones lo dejaban expuesto, qué hacer si no puede parchearse hoy y cómo endurecer un entorno PHP para la próxima vez.

Manuel Castelló González

¿Por qué hacemos una segunda entrega tan pronto?

En «La crisis autoinducida de PHP» contamos y analizamos cómo una filtración rompió el embargo de PHP2Shell veintitrés horas antes del parche y cómo esa precipitación convirtió dos fallos graves en una crisis de servicios públicos desconectados. Aquel artículo se detenía en la historia, en la ética de la divulgación y en sus consecuencias jurídicas. Este es su continuación práctica, y está pensado para quienes tienen que mantener en pie un servidor o escribir el código que corre en él: administradores de sistemas, equipos de operaciones, programadores y responsables técnicos de pequeñas organizaciones que dependen de PHP sin tener un departamento de seguridad detrás.

La situación ha evolucionado desde entonces. El 26 de octubre, la agencia estadounidense de ciberseguridad (CISA) incorporó CVE-2026-109618 y CVE-2026-109619 a su catálogo de vulnerabilidades explotadas activamente, después de que se confirmara una ejecución remota de código real contra un servidor señuelo con Apache y mod_php en PHP 8.4.26. VirusTotal estima que hay más de cinco millones de servidores PHP accesibles desde Internet. Y sigue abierta una tercera vía, CVE-2026-115560, que no tendrá corrección hasta la próxima versión ordinaria de PHP.

Una advertencia antes de empezar. Esta guía explica cómo comprobar, mitigar, revisar y endurecer, y se queda deliberadamente en la superficie cuando se trata de cómo funciona un ataque. Para proteger un servidor hace falta saber qué condiciones lo hacen vulnerable y qué rastro deja un intento de explotación; la receta para provocarlo no aporta nada a la defensa y sí mucho a quien quiere atacar. Todas las recomendaciones distinguen la rama de PHP a la que se refieren, porque una misma directiva puede no existir, comportarse de otra forma o estar ya obsoleta según la versión.

Primero, saber qué PHP ejecuta realmente el servidor

La pregunta parece trivial y es la que más errores provoca. En un mismo servidor pueden convivir varias versiones de PHP, y la que responde en la línea de comandos no tiene por qué ser la que sirve las páginas web. Además, las distribuciones de Linux suelen corregir los fallos sin cambiar el número de versión: Ubuntu 24.04, por ejemplo, sigue mostrando PHP 8.3.6 aunque el paquete corregido es el 8.3.6-0ubuntu0.24.04.13. Fiarse del número que devuelve php -v puede llevar a creer que un servidor parcheado es vulnerable o, lo que es peor, al revés.

Identificar la forma de ejecución (SAPI)

PHP se conecta al servidor web mediante una SAPI. Las más comunes son el módulo de Apache (mod_php), PHP-FPM (con Nginx o con Apache mediante proxy_fcgi), php-cgi, LiteSpeed (LSAPI) y FrankenPHP. Para saber cuál se usa:

# Apache: ¿está cargado el módulo de PHP?
apachectl -M 2>/dev/null | grep -i php
# PHP-FPM: ¿qué servicios y versiones hay en marcha?
systemctl list-units --type=service | grep -i fpm
ps -eo user,cmd | grep -E "php-fpm|php[0-9.]*-fpm" | grep -v grep

Si aparece un módulo php_module o php7_module en Apache, el servidor usa mod_php, que es la configuración de mayor riesgo para PHP2Shell. Si aparecen procesos php-fpm: pool …, usa FPM, y conviene anotar qué versión corre cada pool.

Comprobar el parche, no el número de versión

En las versiones oficiales del proyecto, la comprobación es directa: están corregidas PHP 8.5.12, 8.4.27, 8.3.36 y 8.2.35 o posteriores. En los paquetes de las distribuciones hay que buscar el identificador del fallo en el registro de cambios del paquete:

# Debian y Ubuntu (ajustar el nombre del paquete a la versión instalada)
zgrep -E "CVE-2026-10961[89]" /usr/share/doc/php8.3-common/changelog.Debian.gz
apt changelog php8.3-fpm | grep -E "CVE-2026-10961[89]"
# RHEL, AlmaLinux, Rocky y derivados
rpm -q --changelog php-common | grep -E "CVE-2026-10961[89]"

Si el comando no devuelve nada, el paquete instalado no incluye la corrección o su mantenedor no la anotó; en ambos casos hay que consultar el aviso de seguridad de la distribución antes de dar el servidor por protegido.

La versión que sirve las páginas es la que cuenta

La configuración efectiva de PHP-FPM o de mod_php puede diferir de la de la línea de comandos, porque cada SAPI lee su propio php.ini y cada pool de FPM puede sobrescribir directivas. Para ver los valores reales hay dos caminos seguros. El primero, revisar la configuración de FPM sin arrancarlo:

php-fpm8.3 -tt 2>&1 | grep -E "php_admin_value|php_value"

El segundo, colocar temporalmente un archivo con phpinfo() en una ruta con nombre aleatorio, protegida por contraseña o limitada a la IP del administrador, consultarlo una vez y borrarlo de inmediato. Un phpinfo() olvidado en producción es, en sí mismo, una fuga de información.

Contenedores, paneles y alojamientos gestionados

En contenedores, la versión la fija la imagen. Las imágenes oficiales de Docker siguen las versiones del proyecto, así que en las ramas 8.2 a 8.5 basta con usar una etiqueta igual o posterior a las corregidas; para PHP 8.1 o anteriores no hay corrección en las imágenes oficiales, según Sansec. Actualizar el servidor anfitrión no sirve de nada si los contenedores siguen ejecutando la imagen antigua: hay que reconstruirlos y redesplegarlos. En los paneles de alojamiento (cPanel, Plesk, DirectAdmin), la actualización se hace desde el propio panel, comprobando en su registro de cambios que menciona los identificadores CVE. Y en el alojamiento compartido gestionado, la pregunta es para el proveedor: qué compilación de PHP usa cada versión que ofrece y desde cuándo incluye la corrección.

Situación por versión de PHP

El proyecto PHP solo mantiene las ramas 8.2 a 8.5. Para las anteriores no hay versión oficial corregida, aunque, dada la gravedad, el equipo de seguridad publicó adaptaciones mínimas del código fuente para 7.4, 8.0 y 8.1, pensadas para quien compila PHP por su cuenta. El cuadro resume la situación a 28 de octubre de 2026.

Rama Versión corregida Fin del soporte de seguridad Qué hacer
Ramas con soporte oficial
8.58.5.1231-12-2029Actualizar; hasta hacerlo, fatal_error_backtraces = 0
8.48.4.2731-12-2028Actualizar
8.38.3.3631-12-2027Actualizar
8.28.2.3531-12-2026Actualizar y planificar ya el salto a 8.3 u 8.4
8.6 (en pruebas)8.6.0 RC4Aún no publicadaSolo para pruebas, nunca en producción
Ramas sin soporte oficial
8.1, 8.0 y 7.4Sin versión oficial; adaptación mínima del código fuente publicada por PHPAgotado (8.1: 31-12-2025; 8.0: 26-11-2023; 7.4: 28-11-2022)Paquete corregido de la distribución o del soporte extendido; si no lo hay, migrar
7.3 y anteriores (desde 5.4)Sin corrección del proyectoAgotadoPaquete de soporte extendido si existe; si no, aislar el servidor y migrar

Las fechas de fin de soporte proceden de la página oficial de versiones de PHP. Conviene subrayar la de la rama 8.2: dentro de dos meses dejará de recibir correcciones de seguridad, de modo que quien la actualice ahora por PHP2Shell debería planificar ya el salto a 8.3 o, mejor, a 8.4.

Qué han publicado las distribuciones

Las grandes distribuciones publicaron sus paquetes el mismo 21 de octubre. Las versiones del cuadro proceden de sus avisos oficiales; en todos los casos se instalan con la actualización ordinaria del sistema.

Distribución Paquete Versión corregida Aviso
Ubuntu (actualización ordinaria)
Ubuntu 26.10php8.58.5.9-0ubuntu2.1USN-8948-1
Ubuntu 26.04 LTSphp8.58.5.4-0ubuntu1.5USN-8948-1
Ubuntu 24.04 LTSphp8.38.3.6-0ubuntu0.24.04.13USN-8948-1
Ubuntu 22.04 LTSphp8.18.1.2-1ubuntu2.28USN-8948-1
Ubuntu con suscripción Ubuntu Pro
Ubuntu 20.04 LTSphp7.47.4.3-4ubuntu2.29+esm5USN-8948-2
Ubuntu 18.04 LTSphp7.27.2.24-0ubuntu0.18.04.17+esm14USN-8948-2
Ubuntu 16.04 LTS (Legacy)php7.07.0.33-0ubuntu0.16.04.16+esm22USN-8948-2
Ubuntu 14.04 LTS (Legacy)php55.5.9+dfsg-1ubuntu4.29+esm18USN-8948-2
Debian
Debian 13 (trixie)php8.48.4.27-1~deb13u1DSA-6574-1
Debian 12 (bookworm)php8.28.2.35-1~deb12u1DLA-4848-1

Para las ramas antiguas existen otras vías, unas gratuitas y otras de pago. Según la recopilación de Sansec, el repositorio de Ondřej Surý (deb.sury.org y su PPA para Ubuntu) y el de Remi para la familia RHEL publicaron correcciones para las ramas antiguas que mantienen; AlmaLinux y Red Hat lo hicieron para sus versiones con soporte; y los servicios de soporte extendido (Freexian ELTS para Debian 9 a 11, TuxCare, ZendPHP LTS, CloudLinux para cPanel) cubren a sus clientes. Quien dependa de uno de estos canales debe comprobar el registro de cambios del paquete con los comandos anteriores.

¿Estaba mi servidor realmente expuesto?

Esta es la pregunta que más interesa a un responsable técnico, porque de ella dependen la urgencia, la revisión posterior y, si llega el caso, la valoración de la diligencia ante un juez o una autoridad de protección de datos. La respuesta no es igual para todos los servidores. Los avisos oficiales y los análisis de los investigadores permiten construir una lista de condiciones que, combinadas, determinan el nivel de exposición. Se presentan aquí como lo que son, una herramienta para evaluar el riesgo.

Condiciones comunes a los dos fallos

  • PHP atiende peticiones web de clientes no autenticados. Los dos fallos se producen al procesar una petición HTTP, antes de que se ejecute el código de la aplicación, de modo que el inicio de sesión de la aplicación no protege. Un PHP que solo se usa en la línea de comandos para tareas programadas no está expuesto por esta vía.
  • La versión no incluye la corrección. Afecta a todas las versiones desde PHP 5.4, publicada en 2012, hasta las anteriores a 8.2.35, 8.3.36, 8.4.27 y 8.5.12, salvo que el paquete lleve una adaptación del parche.
  • El mismo proceso atiende varias peticiones seguidas. Es lo normal en mod_php y en PHP-FPM, y es lo que permite que un estado dañado en una petición afecte a la siguiente.

Condiciones según la forma de ejecución

  • Apache con mod_php: riesgo más alto. Es la configuración en la que se demostró la ejecución remota con los valores por defecto. El proceso de Apache sobrevive a una petición fallida y conserva el estado para la siguiente. Además, PHP comparte memoria con el propio Apache, lo que agrava el alcance de una posible fuga de memoria.
  • PHP-FPM: exposición condicionada, sin margen para la tranquilidad. El aviso de CVE-2026-109618 indica que FPM limita la exposición porque descarta el proceso cuando falla el arranque de la petición, aunque la falta de memoria también puede producirse después. Para CVE-2026-109619, FPM está expuesto con el seguimiento del progreso de subida activado y cuando el servidor web le pasa directivas de configuración por petición (PHP_VALUE o PHP_ADMIN_VALUE) que generan avisos antes de inicializar la petición. La PHP Foundation afirmó que la ejecución remota también se confirmó en FPM.
  • LiteSpeed (LSAPI) y FrankenPHP. Para LSAPI, los avisos consideran probable la exposición, sin haberla validado. FrankenPHP es probablemente susceptible al primer fallo y, con su configuración por defecto, no presenta la vía conocida del segundo.
  • php-cgi. Figura entre las afectadas en el aviso oficial.

Condiciones de configuración que aumentaban el riesgo

  • Seguimiento del progreso de subida activado (session.upload_progress.enabled = 1). Es el valor por defecto en todas las versiones desde la 5.4, y es la vía validada para el segundo fallo tanto en mod_php como en FPM. Es también la vía por la que se registraron la mayoría de los intentos reales, según Hadrian.
  • PHP 8.5 con trazas en los errores fatales (fatal_error_backtraces = 1). La directiva apareció en PHP 8.5.0 y viene activada por defecto. Es el caso más sólido del segundo fallo y la base de la explotación confirmada en mod_php. No existe en versiones anteriores.
  • Directivas por directorio que generan avisos antes de inicializar la petición. En mod_php, los valores de php_value y php_flag de los archivos .htaccess o de la configuración de Apache se aplican antes del arranque de la petición; si alguno es inválido u obsoleto, el aviso que emite puede activar la vía vulnerable. Ocultar esos avisos con error_reporting, display_errors o el operador @ no sirve.
  • Límite de memoria finito. El primer fallo depende de que una petición agote memory_limit (128 MB por defecto) en un momento concreto.
  • Errores fatales o agotamientos de tiempo que un visitante puede provocar. El aviso oficial pide corregirlos, incluidos los que se producen al procesar subidas, importaciones o tareas de limpieza.

Lo que matiza el cuadro

Dos datos ayudan a dimensionar el riesgo sin caer en el alarmismo ni en la complacencia. Sansec recoge que las pruebas de concepto publicadas no funcionaban contra una instalación de PHP por defecto, porque la explotación requiere condiciones concretas en el servidor. Pero la confirmación de un ataque con éxito contra mod_php en PHP 8.4.26 y la inclusión en el catálogo de CISA demuestran que esas condiciones se dan en el mundo real. Un servidor con mod_php, una versión sin corregir y el progreso de subida activado debe tratarse como potencialmente comprometido hasta que se revise.

Qué consigue un atacante si lo logra

Entender el alcance sirve para decidir qué revisar y qué endurecer. En el peor caso, el atacante ejecuta código con los privilegios del usuario con el que corre PHP (www-data, apache, nginx o el usuario del pool de FPM). Puede leer todo lo que ese usuario lee, incluidos los archivos de configuración con las contraseñas de la base de datos, escribir donde ese usuario escribe y abrir conexiones hacia fuera si nada lo impide. Una de las variantes permite además leer memoria del proceso con restos de otras peticiones: contraseñas, tokens de sesión o claves de API de otros usuarios. De ahí se deducen las dos prioridades de cualquier estrategia defensiva: limitar lo que puede hacer el usuario de PHP y dar por expuestos los secretos que pasaron por un servidor vulnerable.

Si hoy no se puede actualizar

Hay servidores que no pueden actualizarse en el acto: una aplicación antigua que no funciona con PHP 8, un proveedor que no responde, un contrato de mantenimiento que exige pruebas previas. Para esos casos, el aviso oficial propone medidas temporales que reducen la exposición a vías concretas sin hacer segura la instalación. Conviene repetirlo, porque es fácil olvidarlo con el paso de los días: son un puente hasta el parche y nunca un sustituto.

Medidas válidas para todas las versiones

Desactivar el seguimiento del progreso de subida. Es la medida más eficaz frente a la vía más explotada y sirve también para la tercera vía, CVE-2026-115560. Solo la necesitan las aplicaciones que muestran una barra de progreso en las subidas de archivos basada en esta función de PHP, algo poco frecuente. Debe fijarse en php.ini, en el pool de FPM o en la configuración del servidor web, nunca con ini_set() ni en un .user.ini, porque la petición vulnerable se procesa antes de que esos mecanismos entren en juego. Después hay que reiniciar todos los procesos.

; php.ini (todas las versiones desde la 5.4)
session.upload_progress.enabled = 0

; pool de PHP-FPM (por ejemplo /etc/php/8.3/fpm/pool.d/www.conf)
php_admin_flag[session.upload_progress.enabled] = off

# Apache con mod_php (en la configuración del servidor, no en .htaccess)
php_admin_flag session.upload_progress.enabled off

Ajustar el límite de memoria y el tamaño de las peticiones. El aviso propone fijar memory_limit = -1 de forma que la aplicación no pueda cambiarlo (php_admin_value), para eliminar el agotamiento controlado de memoria. La medida tiene un coste evidente: sin límite, una sola petición puede consumir toda la memoria del servidor. Por eso debe acompañarse de un límite externo por contenedor o por grupo de control (cgroup), con una política que termine solo el proceso afectado. El aviso desaconseja expresamente sustituirlo por RLIMIT_AS o RLIMIT_DATA (los límites de ulimit), porque provocan precisamente el tipo de fallo de reserva de memoria que se quiere evitar. A la vez, conviene bajar post_max_size a 8 MB si la aplicación no admite subidas, o a 32 MB si las necesita, con upload_max_filesize siempre por debajo.

; pool de PHP-FPM
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
php_admin_value[max_input_vars] = 1000
php_admin_value[max_input_nesting_level] = 32
php_admin_value[max_file_uploads] = 10

# Límite externo de memoria para el servicio (systemctl edit php8.3-fpm)
[Service]
MemoryMax=2G

Poner límites también delante de PHP. El aviso recomienda que el servidor web o el proxy inverso limiten el tamaño de las peticiones, el número de campos y de partes de los formularios y la profundidad de anidamiento, también en las peticiones que no declaran su longitud. Las directivas básicas son client_max_body_size en Nginx y LimitRequestBody, LimitRequestFields y LimitRequestFieldSize en Apache; los cortafuegos de aplicaciones permiten afinar más. Cloudflare, Fastly, Vercel y los principales fabricantes de cortafuegos publicaron reglas específicas para PHP2Shell, útiles como protección temporal mientras se actualiza.

Valorar el bloqueo de los formularios con subida de archivos. Todos los desencadenantes conocidos llegan en peticiones de formulario con varias partes (multipart/form-data). Si la aplicación no necesita subir archivos, o puede prescindir de ello unos días, Sansec propone rechazar esas peticiones en el servidor web. Rompe funciones legítimas, así que hay que probar la aplicación después y retirar la regla en cuanto se aplique el parche.

Corregir los errores fatales y los agotamientos de tiempo que un visitante pueda provocar, y retirar de .htaccess o de la configuración por directorio cualquier directiva inválida u obsoleta que genere avisos. El aviso también menciona pm.max_requests = 1 en FPM, que obliga a usar un proceso nuevo para cada petición; reduce la reutilización de estado, aunque su coste en rendimiento solo es asumible en servidores con poco tráfico.

Medidas propias de cada rama

  • PHP 8.5: desactivar las trazas de los errores fatales, que cierra la vía confirmada del segundo fallo en mod_php. Puede desactivarse con fatal_error_backtraces = 0 en php.ini o con php_admin_flag[fatal_error_backtraces] = off en el pool. En las ramas anteriores la directiva no existe y no hay que añadirla.
  • PHP 7.4 a 8.4: las medidas generales son las únicas disponibles. En 7.4 y posteriores conviene comprobar además que zend.exception_ignore_args = On, el valor del php.ini de producción, para que las trazas de error no incluyan los argumentos de las funciones.
  • PHP 7.3 y anteriores: sin corrección oficial ni adaptación del equipo de seguridad. Las medidas temporales reducen el riesgo, pero lo prudente es aislar el servidor (acceso solo por VPN o desde direcciones conocidas) hasta migrarlo, salvo que el proveedor de soporte extendido ofrezca un paquete corregido, como hace Ubuntu Pro con sus versiones antiguas.

En todos los casos, después de cambiar la configuración hay que reiniciar todos los procesos de PHP (systemctl restart php8.3-fpm o apachectl graceful) y comprobar el valor efectivo en una petición servida por la web, no en la línea de comandos.

Después de parchear: lo que la actualización no arregla

Instalar el paquete corregido no termina el trabajo. Las versiones nuevas solo protegen cuando los procesos antiguos se han detenido, y no dicen nada sobre lo que pudo pasar antes.

Confirmar que el parche está activo

Tras actualizar, hay que reiniciar PHP-FPM o Apache y verificar que no quedan procesos con la versión anterior. Un indicio útil es buscar procesos que todavía usan bibliotecas ya borradas del disco:

# Procesos que siguen usando archivos sustituidos por la actualización
sudo lsof -nP +L1 2>/dev/null | grep -E "php|apache2|httpd"
# En Debian y Ubuntu, con el paquete needrestart instalado
sudo needrestart -r l

En contenedores, la comprobación es la etiqueta o el resumen (digest) de la imagen que está corriendo, no la del registro.

Dar por expuestos los secretos

Si el servidor fue vulnerable mientras circulaban ataques, es decir, al menos desde el 17 de octubre según la telemetría de Amazon, lo prudente es asumir que los secretos que pasaron por la memoria de PHP pudieron leerse. Eso incluye las contraseñas de la base de datos y de servicios externos, las claves de API, las claves de firma de la aplicación (en Laravel, APP_KEY; en WordPress, las salts de wp-config.php) y las sesiones abiertas. La respuesta es rotarlas, invalidar las sesiones y obligar a los usuarios con privilegios a volver a autenticarse. Si el servidor usaba mod_php y la evaluación de riesgo lo aconseja, conviene valorar también la renovación de los certificados TLS.

Buscar señales de intrusión

La revisión debe cubrir, como mínimo, desde el 17 de octubre hasta la fecha en que se aplicó el parche. Las señales que describieron Wiz, Amazon y Fortinet son estas:

  • Errores de memoria agotada («Allowed memory size … exhausted») concentrados en el tiempo, sobre todo si coinciden con peticiones de formulario con subida de archivos a rutas que no las esperan.
  • Ráfagas de respuestas HTTP 500 y reinicios o caídas de los procesos de PHP sin causa aparente.
  • Peticiones POST de tipo multipart/form-data que incluyen un campo PHP_SESSION_UPLOAD_PROGRESS, el nombre que usa PHP para el seguimiento del progreso de subida, en aplicaciones que no usan esa función.
  • Cookies de sesión PHPSESSID de tamaño o formato anómalo.
  • Archivos .php nuevos o modificados en el directorio web, sobre todo en las carpetas de subidas, de caché o temporales; tareas programadas nuevas en el crontab del usuario del servidor web; y conexiones salientes inesperadas desde los procesos de PHP.

Algunos comandos sirven como punto de partida, siempre adaptados a cada instalación:

# Archivos PHP creados o modificados desde el 17 de octubre en el directorio web
find /var/www -name "*.php" -newermt "2026-10-17" -ls
# Archivos PHP en carpetas de subidas, donde nunca deberían estar
find /var/www -path "*upload*" -name "*.php*" -ls
# Tareas programadas del usuario del servidor web
sudo crontab -u www-data -l
# Conexiones de red abiertas por procesos de PHP o del servidor web
sudo ss -tnp | grep -E "php|apache2|httpd|nginx"

Amazon publicó varias direcciones IP observadas en sus señuelos, con la advertencia de que pertenecen a redes de anonimato compartidas y deben investigarse, no bloquearse sin más. Si aparece cualquier indicio, lo primero es preservar las pruebas, es decir, copiar los registros del servidor web y de PHP antes de que la rotación los borre, calcular su resumen criptográfico y documentar la cadena de custodia. Solo después se limpia. Y conviene recordar que el parche no expulsa a quien ya entró: un servidor con signos de intrusión se reconstruye desde una copia limpia, no se «arregla» en caliente.

Endurecer PHP para la próxima vez

PHP2Shell no será el último fallo grave de PHP, y la diferencia entre un susto y una intrusión la marca a menudo lo que ya estaba configurado antes del aviso. Las medidas siguientes son de aplicación general; cuando dependen de la versión, se indica.

Esquema de las seis capas de defensa de un servidor PHP
Capas de defensa de un servidor PHP: cada una reduce lo que puede conseguir un fallo como PHP2Shell. Esquema propio de Actum Forense Press.

Versiones con soporte y un calendario de actualización

La primera medida es estar en una rama con soporte y tener prevista la siguiente. Con los datos actuales, PHP 8.3 recibe correcciones de seguridad hasta el 31 de diciembre de 2027, PHP 8.4 hasta el 31 de diciembre de 2028 y PHP 8.5 hasta el 31 de diciembre de 2029. Conviene suscribirse a los avisos de php.net, a la lista de seguridad de la distribución y a los del INCIBE-CERT o del CCN-CERT, y fijar un plazo interno para aplicar los parches críticos medido en horas, no en semanas. La investigación sobre los parches lo justifica: Li y Paxson comprobaron, en un estudio de miles de correcciones, que las vulnerabilidades suelen permanecer mucho tiempo en el código antes de corregirse; Nappa y sus colegas midieron que la aplicación de los parches en los equipos es lenta e incompleta; y Brumley y su equipo demostraron hace casi dos décadas que un parche publicado puede convertirse en un ataque de forma automática.

Abandonar mod_php

mod_php obliga a usar el modelo de procesos prefork de Apache, ejecuta PHP dentro del propio servidor web, con sus privilegios y su memoria, y fue la configuración de mayor riesgo en PHP2Shell. PHP-FPM, con un pool por sitio y un usuario del sistema distinto para cada uno, aísla las aplicaciones entre sí y permite fijar límites por pool. La migración es sencilla en Apache 2.4 con proxy_fcgi y es la recomendación más rentable de esta guía.

; /etc/php/8.3/fpm/pool.d/misitio.conf
[misitio]
user = misitio
group = misitio
listen = /run/php/misitio.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.max_requests = 500
clear_env = yes
security.limit_extensions = .php
php_admin_value[open_basedir] = /var/www/misitio:/tmp
php_admin_flag[session.upload_progress.enabled] = off

pm.max_requests hace que cada proceso se renueve tras un número de peticiones, lo que limita el tiempo que puede sobrevivir un estado dañado. security.limit_extensions impide que FPM ejecute como PHP archivos con otras extensiones, y open_basedir restringe las rutas accesibles; es una capa adicional útil, aunque no debe tratarse como una barrera infalible.

Una configuración base de php.ini

expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/error.log
zend.exception_ignore_args = On      ; PHP 7.4 y posteriores
allow_url_include = Off              ; obsoleta desde 7.4, debe seguir en Off
allow_url_fopen = Off                ; solo si la aplicación no la necesita
session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax        ; PHP 7.3 y posteriores
session.upload_progress.enabled = 0  ; salvo que la aplicación la use
max_execution_time = 30
fatal_error_backtraces = 0           ; solo PHP 8.5, mientras haya dudas

La directiva disable_functions, con la que muchas guías bloquean exec, shell_exec, system, passthru, proc_open o popen, añade fricción a un atacante, pero no detiene a uno con ejecución de código en memoria y puede romper aplicaciones legítimas; conviene probarla en preproducción y no confiar en ella como única defensa.

Permisos, directorios y red

  • El código de la aplicación debe pertenecer a un usuario distinto del que ejecuta PHP, que solo debe poder leerlo. Las únicas carpetas escribibles son las de subidas, caché y temporales.
  • En esas carpetas, PHP no debe ejecutarse nunca. En Nginx, una regla location que devuelva 403 para *.php dentro de las rutas de subida; en Apache, php_admin_flag engine off o la eliminación del manejador de PHP en ese directorio.
  • AllowOverride None en Apache, o al menos sin permitir Options ni directivas de PHP en .htaccess, evita que un archivo escrito por un atacante cambie la configuración, y reduce de paso la vía de las directivas por directorio que afectó a PHP2Shell.
  • El servidor web no necesita, casi nunca, abrir conexiones salientes arbitrarias. Filtrar la salida con el cortafuegos, dejando solo la base de datos, los servicios de correo y las API que use la aplicación, frena la descarga de herramientas y el envío de datos robados.
  • La cuenta de la base de datos de la aplicación debe tener solo los permisos que necesita, nunca los de administrador.

Aislamiento del servicio con systemd

Las distribuciones modernas permiten restringir el servicio de PHP-FPM sin tocar la aplicación, con un archivo de sustitución (systemctl edit php8.3-fpm):

[Service]
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
MemoryMax=2G

Cada opción debe probarse, porque algunas aplicaciones guardan datos en rutas que ProtectSystem o ProtectHome dejan en solo lectura; en ese caso se abren con ReadWritePaths=. Algunas distribuciones ya incluyen parte de estas restricciones en la unidad del paquete.

Contenedores

En contenedores, las buenas prácticas son conocidas y PHP2Shell las ha puesto a prueba: fijar la imagen por su resumen criptográfico y no por una etiqueta móvil, reconstruirla en cuanto hay un parche, ejecutar PHP con un usuario sin privilegios, montar el sistema de archivos raíz en solo lectura con volúmenes escribibles únicamente donde hace falta y limitar la memoria del contenedor, que actúa como el límite externo que recomienda el aviso.

Vigilancia

Un registro de errores de PHP centralizado y revisado, alertas ante picos de errores de memoria o de respuestas 500, y un control de integridad de archivos (AIDE, por ejemplo) que avise de cualquier .php nuevo en el directorio web convierten un ataque silencioso en un incidente visible. Ninguna de estas medidas evita un fallo como PHP2Shell, pero reducen mucho el tiempo que un intruso puede permanecer sin ser detectado.

Para programadores: que la aplicación no ponga las cosas fáciles

PHP2Shell es un fallo del motor de PHP y ningún código de aplicación lo corrige. Aun así, el aviso oficial y el tercer identificador muestran que algunas prácticas de programación amplían o reducen las condiciones de riesgo.

  • No cambiar el gestor de serialización de sesiones en tiempo de ejecución. CVE-2026-115560 solo se produce cuando la configuración fija session.serialize_handler en php_binary o php_serialize y la aplicación lo cambia a php con ini_set() antes de session_start(), con el progreso de subida activado. Si una aplicación necesita un gestor concreto, debe fijarse en la configuración y no tocarse desde el código.
  • No deserializar datos que no sean de confianza. unserialize() sobre datos que controla el usuario es una fuente clásica de vulnerabilidades. Lo recomendable es usar json_decode(). Si no hay alternativa, desde PHP 7.0 puede limitarse con unserialize($datos, ['allowed_classes' => false]).
  • Evitar los errores fatales y los agotamientos de tiempo que un visitante pueda provocar. Validar pronto el tamaño y la estructura de las entradas, limitar la recursión y el tamaño de los archivos que se procesan, y fijar límites razonables en importaciones y exportaciones. Desde PHP 7.0, muchos errores son excepciones (Throwable) que pueden capturarse; el agotamiento de memoria, en cambio, sigue siendo fatal.
  • Proteger los secretos en las trazas. Desde PHP 8.2, el atributo #[\SensitiveParameter] oculta el valor de un parámetro en las trazas de error. Es especialmente útil en PHP 8.5, donde los errores fatales también generan trazas.
  • Auditar las dependencias. Desde Composer 2.4, composer audit consulta las bases de vulnerabilidades conocidas para los paquetes instalados. Kula y sus colegas comprobaron que buena parte de los proyectos no actualiza sus dependencias aunque tengan fallos conocidos; integrarlo en la integración continua elimina esa inercia.
  • Mantener los secretos fuera del código. Las credenciales en variables de entorno o en un gestor de secretos, con permisos mínimos, se rotan con facilidad cuando hace falta, como ahora.

Lista de comprobación

  • Inventariar todos los servidores, contenedores y alojamientos con PHP, incluidos los que no aparecen en el inventario oficial.
  • Identificar la SAPI de cada uno y priorizar los que usan mod_php.
  • Comprobar en el registro de cambios del paquete, no en php -v, que la corrección está instalada.
  • Reiniciar todos los procesos y verificar el valor efectivo en una petición web.
  • Donde no se pueda actualizar, aplicar las medidas temporales de la rama correspondiente y aislar el servidor si es antiguo.
  • Desactivar session.upload_progress.enabled salvo que la aplicación lo use, también después de parchear, hasta que se corrija CVE-2026-115560.
  • Revisar los registros desde el 17 de octubre y buscar archivos, tareas y conexiones anómalos.
  • Rotar los secretos y cerrar las sesiones de los servidores que fueron vulnerables.
  • Planificar la migración de mod_php a PHP-FPM y de PHP 8.2 a una rama con más recorrido.
  • Documentar todo, con fechas y horas, por si hay que acreditar la diligencia.

Una costumbre más que un parche

En el artículo anterior concluíamos que, a veces, lo más valioso que puede hacer quien sabe algo es callarse a tiempo. Para quien administra un servidor, la lección complementaria es la de estar preparado antes de que nadie hable. Los servidores que mejor pasaron por PHP2Shell fueron los que ya usaban una versión con soporte, ejecutaban PHP-FPM con usuarios separados, tenían el progreso de subida desactivado porque nadie lo usaba, impedían ejecutar PHP en las carpetas de subidas y vigilaban sus registros. Ninguna de esas medidas exigía conocer el fallo de antemano. Todas juntas convierten una vulnerabilidad crítica en un incidente acotado, y eso es lo máximo que puede pedirse a la seguridad de cualquier sistema.

The patch was only the beginning – How to really protect a PHP server after PHP2Shell

Leer en español

Updating PHP closed the door, but it does not tell you whether someone got in first, nor does it prevent the next crisis. This technical guide, a follow-up to our analysis of the PHP2Shell leak, explains branch by branch how to find out which version a server really runs, which conditions left it exposed, what to do if it cannot be patched today and how to harden a PHP environment for next time.

Why a second instalment so soon?

In «PHP’s self-inflicted crisis» we told and analysed how a leak broke the PHP2Shell embargo twenty-three hours before the patch, and how that rush turned two serious flaws into a crisis of public services switched off. That article dwelt on the story, on the ethics of disclosure and on its legal consequences. This is its practical sequel, written for those who have to keep a server running or write the code that runs on it: system administrators, operations teams, developers and the technical leads of small organisations that depend on PHP without a security department behind them.

The situation has moved on since then. On 26 October, the US cybersecurity agency (CISA) added CVE-2026-109618 and CVE-2026-109619 to its catalogue of actively exploited vulnerabilities, after a real remote code execution was confirmed against a honeypot running Apache with mod_php on PHP 8.4.26. VirusTotal estimates that more than five million PHP servers are reachable from the Internet. And a third path, CVE-2026-115560, remains open and will not be fixed until the next regular PHP release.

A word of caution before we start. This guide explains how to check, mitigate, review and harden, and it deliberately stays on the surface when it comes to how an attack works. Protecting a server requires knowing which conditions make it vulnerable and what trace an exploitation attempt leaves; the recipe for triggering it adds nothing to defence and a great deal to anyone who wants to attack. Every recommendation states which PHP branch it applies to, because the same directive may not exist, may behave differently or may already be deprecated depending on the version.

First, find out which PHP the server really runs

The question sounds trivial, and it is the one that causes the most mistakes. Several PHP versions can coexist on the same server, and the one that answers on the command line need not be the one serving web pages. Moreover, Linux distributions usually fix flaws without changing the version number: Ubuntu 24.04, for example, still reports PHP 8.3.6 even though the fixed package is 8.3.6-0ubuntu0.24.04.13. Trusting the number returned by php -v can lead you to believe a patched server is vulnerable or, worse, the other way round.

Identify the execution model (SAPI)

PHP connects to the web server through a SAPI. The most common are the Apache module (mod_php), PHP-FPM (with Nginx, or with Apache through proxy_fcgi), php-cgi, LiteSpeed (LSAPI) and FrankenPHP. To find out which one is in use:

# Apache: is the PHP module loaded?
apachectl -M 2>/dev/null | grep -i php
# PHP-FPM: which services and versions are running?
systemctl list-units --type=service | grep -i fpm
ps -eo user,cmd | grep -E "php-fpm|php[0-9.]*-fpm" | grep -v grep

If Apache shows a php_module or php7_module, the server uses mod_php, the highest-risk configuration for PHP2Shell. If there are php-fpm: pool … processes, it uses FPM, and it is worth noting which version each pool runs.

Check the patch, not the version number

For the project’s official releases the check is straightforward: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 or later are fixed. For distribution packages, look for the flaw’s identifier in the package changelog:

# Debian and Ubuntu (adjust the package name to the installed version)
zgrep -E "CVE-2026-10961[89]" /usr/share/doc/php8.3-common/changelog.Debian.gz
apt changelog php8.3-fpm | grep -E "CVE-2026-10961[89]"
# RHEL, AlmaLinux, Rocky and derivatives
rpm -q --changelog php-common | grep -E "CVE-2026-10961[89]"

If the command returns nothing, the installed package does not include the fix or its maintainer did not record it; either way, check the distribution’s security advisory before considering the server protected.

The version serving pages is the one that matters

The effective configuration of PHP-FPM or mod_php may differ from the command line, because each SAPI reads its own php.ini and each FPM pool can override directives. There are two safe ways to see the real values. The first is to review the FPM configuration without starting it:

php-fpm8.3 -tt 2>&1 | grep -E "php_admin_value|php_value"

The second is to place a file with phpinfo() temporarily at a randomly named path, protected by a password or restricted to the administrator’s IP address, check it once and delete it immediately. A phpinfo() forgotten in production is an information leak in itself.

Containers, control panels and managed hosting

In containers, the image sets the version. Official Docker images follow the project’s releases, so on branches 8.2 to 8.5 it is enough to use a tag equal to or later than the fixed versions; for PHP 8.1 or earlier there is no fix in the official images, according to Sansec. Updating the host is useless if the containers still run the old image: they must be rebuilt and redeployed. On hosting control panels (cPanel, Plesk, DirectAdmin), the update is done from the panel itself, checking that its changelog mentions the CVE identifiers. And on managed shared hosting, the question is for the provider: which PHP build each offered version uses and since when it includes the fix.

Status by PHP version

The PHP project only maintains branches 8.2 to 8.5. There is no official fixed release for older ones although, given the severity, the security team published minimal source backports for 7.4, 8.0 and 8.1, intended for those who build PHP themselves. The table summarises the situation as of 28 October 2026.

Branch Fixed version End of security support What to do
Officially supported branches
8.58.5.1231-12-2029Update; until then, fatal_error_backtraces = 0
8.48.4.2731-12-2028Update
8.38.3.3631-12-2027Update
8.28.2.3531-12-2026Update and plan the move to 8.3 or 8.4 now
8.6 (testing)8.6.0 RC4Not yet releasedTesting only, never in production
Branches without official support
8.1, 8.0 and 7.4No official release; minimal source backport published by PHPEnded (8.1: 31-12-2025; 8.0: 26-11-2023; 7.4: 28-11-2022)Fixed package from the distribution or extended support; otherwise, migrate
7.3 and earlier (from 5.4)No fix from the projectEndedExtended support package if available; otherwise, isolate the server and migrate

End-of-support dates come from PHP’s official supported versions page. The 8.2 date deserves emphasis: in two months it will stop receiving security fixes, so anyone updating it now because of PHP2Shell should already be planning the move to 8.3 or, better, 8.4.

What the distributions have published

The major distributions released their packages on 21 October. The versions in the table come from their official advisories; in every case they are installed with the normal system update.

Distribution Package Fixed version Advisory
Ubuntu (regular update)
Ubuntu 26.10php8.58.5.9-0ubuntu2.1USN-8948-1
Ubuntu 26.04 LTSphp8.58.5.4-0ubuntu1.5USN-8948-1
Ubuntu 24.04 LTSphp8.38.3.6-0ubuntu0.24.04.13USN-8948-1
Ubuntu 22.04 LTSphp8.18.1.2-1ubuntu2.28USN-8948-1
Ubuntu with an Ubuntu Pro subscription
Ubuntu 20.04 LTSphp7.47.4.3-4ubuntu2.29+esm5USN-8948-2
Ubuntu 18.04 LTSphp7.27.2.24-0ubuntu0.18.04.17+esm14USN-8948-2
Ubuntu 16.04 LTS (Legacy)php7.07.0.33-0ubuntu0.16.04.16+esm22USN-8948-2
Ubuntu 14.04 LTS (Legacy)php55.5.9+dfsg-1ubuntu4.29+esm18USN-8948-2
Debian
Debian 13 (trixie)php8.48.4.27-1~deb13u1DSA-6574-1
Debian 12 (bookworm)php8.28.2.35-1~deb12u1DLA-4848-1

For older branches there are other routes, some free and some paid. According to Sansec’s round-up, Ondřej Surý’s repository (deb.sury.org and its Ubuntu PPA) and Remi’s repository for the RHEL family published fixes for the old branches they maintain; AlmaLinux and Red Hat did so for their supported versions; and extended support services (Freexian ELTS for Debian 9 to 11, TuxCare, ZendPHP LTS, CloudLinux for cPanel) cover their customers. Anyone relying on one of these channels should check the package changelog with the commands above.

Was my server really exposed?

This is the question that matters most to a technical lead, because urgency, the subsequent review and, if it comes to that, any assessment of due diligence before a court or a data protection authority all depend on it. The answer is not the same for every server. The official advisories and the researchers’ analyses make it possible to build a list of conditions which, combined, determine the level of exposure. They are presented here for what they are: a risk assessment tool.

Conditions common to both flaws

  • PHP serves web requests from unauthenticated clients. Both flaws occur while an HTTP request is being processed, before any application code runs, so application login offers no protection. PHP used only on the command line for scheduled tasks is not exposed through this route.
  • The version does not include the fix. Every version from PHP 5.4, released in 2012, up to those before 8.2.35, 8.3.36, 8.4.27 and 8.5.12 is affected, unless the package carries a backport of the patch.
  • The same process handles several requests in a row. This is normal in mod_php and PHP-FPM, and it is what allows damaged state in one request to affect the next.

Conditions by execution model

  • Apache with mod_php: highest risk. This is the configuration where remote execution was demonstrated with default settings. The Apache process survives a failed request and keeps the state for the next one. In addition, PHP shares memory with Apache itself, which widens the reach of any memory leak.
  • PHP-FPM: conditional exposure, with no room for complacency. The CVE-2026-109618 advisory says FPM limits exposure because it discards the process when request start-up fails, although running out of memory can also happen later. For CVE-2026-109619, FPM is exposed with upload progress tracking enabled and when the web server passes it per-request configuration directives (PHP_VALUE or PHP_ADMIN_VALUE) that raise warnings before the request is initialised. The PHP Foundation stated that remote execution was also confirmed on FPM.
  • LiteSpeed (LSAPI) and FrankenPHP. For LSAPI, the advisories consider exposure likely but unvalidated. FrankenPHP is probably susceptible to the first flaw and, with its default configuration, does not present the known path of the second.
  • php-cgi. Listed among the affected SAPIs in the official advisory.

Configuration conditions that increased the risk

  • Upload progress tracking enabled (session.upload_progress.enabled = 1). It is the default in every version since 5.4 and the validated path for the second flaw in both mod_php and FPM. According to Hadrian, it is also the path used in most real-world attempts.
  • PHP 8.5 with backtraces on fatal errors (fatal_error_backtraces = 1). The directive arrived in PHP 8.5.0 and is enabled by default. It is the strongest case of the second flaw and the basis of the confirmed exploitation on mod_php. It does not exist in earlier versions.
  • Per-directory directives that raise warnings before the request is initialised. In mod_php, the php_value and php_flag settings from .htaccess files or the Apache configuration are applied before request start-up; if any of them is invalid or deprecated, the warning it raises can trigger the vulnerable path. Hiding those warnings with error_reporting, display_errors or the @ operator does not help.
  • A finite memory limit. The first flaw depends on a request exhausting memory_limit (128 MB by default) at a specific moment.
  • Fatal errors or timeouts a visitor can trigger. The official advisory asks for them to be fixed, including those that occur while processing uploads, imports or clean-up tasks.

What qualifies the picture

Two facts help to size the risk without sliding into alarmism or complacency. Sansec reports that the published proofs of concept did not work against a default PHP installation, because exploitation requires specific conditions on the server. But the confirmation of a successful attack against mod_php on PHP 8.4.26 and the CISA catalogue listing show that those conditions do occur in the real world. A server with mod_php, an unfixed version and upload progress enabled should be treated as potentially compromised until it has been reviewed.

What an attacker gains if they succeed

Understanding the impact helps decide what to review and what to harden. In the worst case, the attacker runs code with the privileges of the user PHP runs as (www-data, apache, nginx or the FPM pool user). They can read everything that user can read, including configuration files with database passwords, write wherever that user can write, and open outbound connections unless something prevents it. One of the variants also allows reading process memory holding leftovers from other requests: passwords, session tokens or other users’ API keys. Two priorities for any defensive strategy follow from this: limit what the PHP user can do, and treat the secrets that passed through a vulnerable server as exposed.

If you cannot update today

Some servers cannot be updated straight away: an old application that does not run on PHP 8, a provider that does not answer, a maintenance contract that requires prior testing. For those cases, the official advisory proposes temporary measures that reduce exposure to specific paths without making the installation safe. It bears repeating, because it is easy to forget as the days go by: they are a bridge to the patch, never a substitute for it.

Measures valid for every version

Disable upload progress tracking. It is the most effective measure against the most exploited path, and it also covers the third path, CVE-2026-115560. Only applications that show a file-upload progress bar based on this PHP feature need it, which is uncommon. It must be set in php.ini, in the FPM pool or in the web server configuration, never with ini_set() or in a .user.ini file, because the vulnerable request is processed before those mechanisms come into play. All processes must then be restarted.

; php.ini (every version since 5.4)
session.upload_progress.enabled = 0

; PHP-FPM pool (for example /etc/php/8.3/fpm/pool.d/www.conf)
php_admin_flag[session.upload_progress.enabled] = off

# Apache with mod_php (in the server configuration, not in .htaccess)
php_admin_flag session.upload_progress.enabled off

Adjust the memory limit and request size. The advisory proposes setting memory_limit = -1 in a way the application cannot change (php_admin_value), to remove controllable memory exhaustion. The measure has an obvious cost: without a limit, a single request can consume all of the server’s memory. It must therefore be paired with an external limit per container or per control group (cgroup), with a policy that kills only the affected process. The advisory explicitly advises against using RLIMIT_AS or RLIMIT_DATA (the ulimit limits) instead, because they cause precisely the kind of allocation failure one is trying to avoid. At the same time, lower post_max_size to 8 MB if the application does not accept uploads, or to 32 MB if it needs them, always keeping upload_max_filesize below it.

; PHP-FPM pool
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
php_admin_value[max_input_vars] = 1000
php_admin_value[max_input_nesting_level] = 32
php_admin_value[max_file_uploads] = 10

# External memory limit for the service (systemctl edit php8.3-fpm)
[Service]
MemoryMax=2G

Set limits in front of PHP as well. The advisory recommends that the web server or reverse proxy limit request size, the number of form fields and parts, and nesting depth, including for requests that do not declare their length. The basic directives are client_max_body_size in Nginx and LimitRequestBody, LimitRequestFields and LimitRequestFieldSize in Apache; web application firewalls allow finer control. Cloudflare, Fastly, Vercel and the main firewall vendors published PHP2Shell-specific rules, useful as temporary protection while updating.

Consider blocking file-upload forms. Every known trigger arrives in multipart form requests (multipart/form-data). If the application does not need file uploads, or can do without them for a few days, Sansec suggests rejecting those requests at the web server. It breaks legitimate features, so test the application afterwards and remove the rule as soon as the patch is applied.

Fix fatal errors and timeouts a visitor could trigger, and remove from .htaccess or the per-directory configuration any invalid or deprecated directive that raises warnings. The advisory also mentions pm.max_requests = 1 in FPM, which forces a fresh process for every request; it reduces state reuse, although its performance cost is only acceptable on low-traffic servers.

Branch-specific measures

  • PHP 8.5: disable backtraces on fatal errors, which closes the confirmed path of the second flaw on mod_php. Use fatal_error_backtraces = 0 in php.ini or php_admin_flag[fatal_error_backtraces] = off in the pool. The directive does not exist in earlier branches and should not be added there.
  • PHP 7.4 to 8.4: the general measures are the only ones available. On 7.4 and later, also check that zend.exception_ignore_args = On, the production php.ini value, so that error traces do not include function arguments.
  • PHP 7.3 and earlier: no official fix and no backport from the security team. Temporary measures reduce the risk, but the prudent course is to isolate the server (access only via VPN or from known addresses) until it is migrated, unless an extended support provider offers a fixed package, as Ubuntu Pro does for its older releases.

In every case, after changing the configuration, restart all PHP processes (systemctl restart php8.3-fpm or apachectl graceful) and check the effective value in a request served over the web, not on the command line.

After patching: what the update does not fix

Installing the fixed package does not finish the job. New versions only protect once the old processes have stopped, and they say nothing about what may have happened before.

Confirm that the patch is active

After updating, restart PHP-FPM or Apache and check that no processes are left running the previous version. A useful clue is to look for processes still using libraries that have been deleted from disk:

# Processes still using files replaced by the update
sudo lsof -nP +L1 2>/dev/null | grep -E "php|apache2|httpd"
# On Debian and Ubuntu, with the needrestart package installed
sudo needrestart -r l

In containers, the check is the tag or digest of the image actually running, not the one in the registry.

Treat secrets as exposed

If the server was vulnerable while attacks were circulating, that is, at least since 17 October according to Amazon’s telemetry, the prudent assumption is that secrets passing through PHP’s memory may have been read. That includes database and external service passwords, API keys, application signing keys (APP_KEY in Laravel; the salts in wp-config.php in WordPress) and open sessions. The response is to rotate them, invalidate sessions and force privileged users to log in again. If the server used mod_php and the risk assessment warrants it, also consider renewing the TLS certificates.

Look for signs of intrusion

The review should cover at least the period from 17 October to the date the patch was applied. The signs described by Wiz, Amazon and Fortinet are these:

  • Memory exhaustion errors («Allowed memory size … exhausted») clustered in time, especially when they coincide with file-upload form requests to paths that do not expect them.
  • Bursts of HTTP 500 responses, and PHP processes restarting or crashing for no apparent reason.
  • multipart/form-data POST requests that include a PHP_SESSION_UPLOAD_PROGRESS field, the name PHP uses for upload progress tracking, in applications that do not use that feature.
  • PHPSESSID session cookies of abnormal size or format.
  • New or modified .php files in the web directory, especially in upload, cache or temporary folders; new scheduled tasks in the web server user’s crontab; and unexpected outbound connections from PHP processes.

Some commands serve as a starting point, always adapted to each installation:

# PHP files created or modified since 17 October in the web directory
find /var/www -name "*.php" -newermt "2026-10-17" -ls
# PHP files in upload folders, where they should never be
find /var/www -path "*upload*" -name "*.php*" -ls
# Scheduled tasks of the web server user
sudo crontab -u www-data -l
# Network connections opened by PHP or web server processes
sudo ss -tnp | grep -E "php|apache2|httpd|nginx"

Amazon published several IP addresses seen on its honeypots, warning that they belong to shared anonymisation networks and should be investigated rather than simply blocked. If any sign appears, the first step is to preserve evidence: copy the web server and PHP logs before rotation deletes them, compute their cryptographic hash and document the chain of custody. Only then clean up. And remember that the patch does not evict anyone already inside: a server showing signs of intrusion is rebuilt from a clean backup, not «fixed» while live.

Hardening PHP for next time

PHP2Shell will not be PHP’s last serious flaw, and the difference between a scare and an intrusion is often made by what was already configured before the advisory. The following measures apply generally; where they depend on the version, this is stated.

Diagram of the six layers of defence for a PHP server
Layers of defence for a PHP server: each one reduces what a flaw like PHP2Shell can achieve. Own diagram by Actum Forense Press.

Supported versions and an update schedule

The first measure is to be on a supported branch and to have the next one planned. On current figures, PHP 8.3 receives security fixes until 31 December 2027, PHP 8.4 until 31 December 2028 and PHP 8.5 until 31 December 2029. It is worth subscribing to php.net announcements, to the distribution’s security list and to the advisories of the Spanish INCIBE-CERT or CCN-CERT, and setting an internal deadline for critical patches measured in hours, not weeks. Research on patching supports this: Li and Paxson found, in a study of thousands of fixes, that vulnerabilities tend to remain in code for a long time before being fixed; Nappa and colleagues measured that patch deployment on hosts is slow and incomplete; and Brumley’s team showed almost two decades ago that a published patch can be turned into an exploit automatically.

Move away from mod_php

mod_php requires Apache’s prefork process model, runs PHP inside the web server itself, with its privileges and its memory, and was the highest-risk configuration in PHP2Shell. PHP-FPM, with one pool per site and a different system user for each, isolates applications from one another and allows per-pool limits. The migration is straightforward on Apache 2.4 with proxy_fcgi, and it is the most cost-effective recommendation in this guide.

; /etc/php/8.3/fpm/pool.d/mysite.conf
[mysite]
user = mysite
group = mysite
listen = /run/php/mysite.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.max_requests = 500
clear_env = yes
security.limit_extensions = .php
php_admin_value[open_basedir] = /var/www/mysite:/tmp
php_admin_flag[session.upload_progress.enabled] = off

pm.max_requests makes each process recycle after a number of requests, limiting how long damaged state can survive. security.limit_extensions prevents FPM from executing files with other extensions as PHP, and open_basedir restricts the paths that can be accessed; it is a useful extra layer, but it should not be treated as an infallible barrier.

A baseline php.ini

expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/error.log
zend.exception_ignore_args = On      ; PHP 7.4 and later
allow_url_include = Off              ; deprecated since 7.4, must stay Off
allow_url_fopen = Off                ; only if the application does not need it
session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax        ; PHP 7.3 and later
session.upload_progress.enabled = 0  ; unless the application uses it
max_execution_time = 30
fatal_error_backtraces = 0           ; PHP 8.5 only, while doubts remain

The disable_functions directive, which many guides use to block exec, shell_exec, system, passthru, proc_open or popen, adds friction for an attacker, but it does not stop one with in-memory code execution and it can break legitimate applications; test it in staging and do not rely on it as the only defence.

Permissions, directories and network

  • Application code should belong to a user other than the one running PHP, which should only be able to read it. The only writable folders are those for uploads, cache and temporary files.
  • PHP must never run in those folders. In Nginx, a location rule returning 403 for *.php inside upload paths; in Apache, php_admin_flag engine off or removing the PHP handler for that directory.
  • AllowOverride None in Apache, or at least not allowing Options or PHP directives in .htaccess, prevents a file written by an attacker from changing the configuration, and incidentally narrows the per-directory directive path that played a part in PHP2Shell.
  • The web server almost never needs to open arbitrary outbound connections. Filtering egress at the firewall, allowing only the database, mail services and the APIs the application uses, hinders tool downloads and the exfiltration of stolen data.
  • The application’s database account should have only the permissions it needs, never administrator rights.

Isolating the service with systemd

Modern distributions allow the PHP-FPM service to be restricted without touching the application, through an override file (systemctl edit php8.3-fpm):

[Service]
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
MemoryMax=2G

Each option should be tested, because some applications store data in paths that ProtectSystem or ProtectHome make read-only; in that case they are opened with ReadWritePaths=. Some distributions already include part of these restrictions in the package unit.

Containers

For containers, good practice is well known and PHP2Shell has put it to the test: pin the image by its digest rather than a moving tag, rebuild it as soon as a patch is out, run PHP as an unprivileged user, mount the root filesystem read-only with writable volumes only where needed, and limit the container’s memory, which acts as the external limit the advisory recommends.

Monitoring

A centralised, reviewed PHP error log, alerts on spikes in memory errors or 500 responses, and file integrity monitoring (AIDE, for example) that flags any new .php file in the web directory turn a silent attack into a visible incident. None of these measures prevents a flaw like PHP2Shell, but they greatly reduce how long an intruder can remain undetected.

For developers: don’t make it easy

PHP2Shell is a flaw in the PHP engine and no application code fixes it. Even so, the official advisory and the third identifier show that some programming practices widen or narrow the risk conditions.

  • Do not change the session serialisation handler at runtime. CVE-2026-115560 only occurs when the configuration sets session.serialize_handler to php_binary or php_serialize and the application switches it to php with ini_set() before session_start(), with upload progress enabled. If an application needs a particular handler, set it in the configuration and leave it alone in code.
  • Do not unserialise untrusted data. unserialize() on user-controlled data is a classic source of vulnerabilities. Use json_decode() instead. If there is no alternative, since PHP 7.0 it can be restricted with unserialize($data, ['allowed_classes' => false]).
  • Avoid fatal errors and timeouts a visitor can trigger. Validate input size and structure early, limit recursion and the size of processed files, and set reasonable limits on imports and exports. Since PHP 7.0 many errors are exceptions (Throwable) that can be caught; memory exhaustion, however, remains fatal.
  • Protect secrets in traces. Since PHP 8.2, the #[\SensitiveParameter] attribute hides a parameter’s value in error traces. It is particularly useful on PHP 8.5, where fatal errors also produce traces.
  • Audit dependencies. Since Composer 2.4, composer audit checks installed packages against known vulnerability databases. Kula and colleagues found that many projects do not update their dependencies even when they have known flaws; building the audit into continuous integration removes that inertia.
  • Keep secrets out of the code. Credentials in environment variables or a secrets manager, with minimal permissions, are easy to rotate when needed, as now.

Checklist

  • Inventory every server, container and hosting account running PHP, including those missing from the official inventory.
  • Identify each one’s SAPI and prioritise those using mod_php.
  • Check the package changelog, not php -v, to confirm the fix is installed.
  • Restart all processes and check the effective value in a web request.
  • Where updating is not possible, apply the temporary measures for the relevant branch and isolate the server if it is old.
  • Disable session.upload_progress.enabled unless the application uses it, also after patching, until CVE-2026-115560 is fixed.
  • Review logs from 17 October onwards and look for anomalous files, tasks and connections.
  • Rotate secrets and close sessions on servers that were vulnerable.
  • Plan the migration from mod_php to PHP-FPM and from PHP 8.2 to a branch with a longer runway.
  • Document everything, with dates and times, in case due diligence has to be demonstrated.

A habit rather than a patch

In the previous article we concluded that sometimes the most valuable thing someone who knows something can do is keep quiet at the right time. For anyone who runs a server, the complementary lesson is to be ready before anyone speaks. The servers that came through PHP2Shell best were those already on a supported version, running PHP-FPM with separate users, with upload progress disabled because nobody used it, preventing PHP from running in upload folders and watching their logs. None of those measures required knowing about the flaw in advance. Together, they turn a critical vulnerability into a contained incident, and that is the most anyone can ask of the security of any system.

Bibliografía y fuentes · References

Alonso, J. F. (2026, octubre). La crisis autoinducida de PHP – Un análisis real de una crisis que no existió. Actum Forense Press. https://actumforensepress.es/archivos/crisis-autoinducida-php-php2shell/

Brumley, D., Poosankam, P., Song, D., & Zheng, J. (2008). Automatic patch-based exploit generation is possible: Techniques and implications. En 2008 IEEE Symposium on Security and Privacy (pp. 143-157). IEEE. https://doi.org/10.1109/SP.2008.17

Canonical. (2026a, 21 de octubre). USN-8948-1: PHP vulnerabilities. Ubuntu Security Notices. https://ubuntu.com/security/notices/USN-8948-1

Canonical. (2026b, 21 de octubre). USN-8948-2: PHP vulnerabilities. Ubuntu Security Notices. https://ubuntu.com/security/notices/USN-8948-2

Debian. (2026, 21 de octubre). DSA-6574-1 php8.4: security update. Debian Security Announce. https://lists.debian.org/debian-security-announce/2026/msg00489.html

Debian Security Tracker. (2026). CVE-2026-109618. https://security-tracker.debian.org/tracker/CVE-2026-109618

Gatlan, S. (2026, 27 de octubre). CISA confirms PHP2Shell vulnerabilities now exploited in attacks. BleepingComputer. https://www.bleepingcomputer.com/news/security/cisa-confirms-php2shell-vulnerabilities-now-exploited-in-attacks/

Kula, R. G., German, D. M., Ouni, A., Ishio, T., & Inoue, K. (2018). Do developers update their library dependencies? Empirical Software Engineering, 23(1), 384-417. https://doi.org/10.1007/s10664-017-9521-5

Lakhani, A. (2026, 23 de octubre; actualizado el 26 de octubre). Understanding PHP2Shell vulnerabilities and reducing risk while you patch. Fortinet Blog. https://www.fortinet.com/blog/industry-trends/understanding-php2shell-vulnerabilities-and-reducing-risk-while-you-patch

Li, F., & Paxson, V. (2017). A large-scale empirical study of security patches. En Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security (CCS ’17) (pp. 2201-2215). ACM. https://doi.org/10.1145/3133956.3134072

Moses, C. J. (2026, 21 de octubre). Amazon threat intelligence identifies PHP2Shell exploitation prior to public disclosure in MadPot. AWS Security Blog. https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/

Nappa, A., Johnson, R., Bilge, L., Caballero, J., & Dumitraş, T. (2015). The attack of the clones: A study of the impact of shared code on vulnerability patching. En 2015 IEEE Symposium on Security and Privacy (pp. 692-708). IEEE. https://doi.org/10.1109/SP.2015.48

OWASP Foundation. (s. f.). PHP configuration cheat sheet. OWASP Cheat Sheet Series. https://cheatsheetseries.owasp.org/cheatsheets/PHP_Configuration_Cheat_Sheet.html

PHP Group. (2026a, 21 de octubre). Security advisory: Critical remote code execution vulnerabilities, update now. https://www.php.net/archive/2026.php

PHP Group. (2026b, 23 de octubre). Informational advisory: Session upload progress. https://www.php.net/archive/2026.php

PHP Group. (2026c). Supported versions. https://www.php.net/supported-versions.php

PHP Group. (s. f.). Runtime configuration: Error handling and logging (fatal_error_backtraces). Manual de PHP. https://www.php.net/manual/en/errorfunc.configuration.php

PHP Security Team. (2026a). RCE via OOM corruption (CVE-2026-109618, GHSA-w8hw-w9fx-j79w). GitHub. https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w

PHP Security Team. (2026b). RCE via stale global state (CVE-2026-109619, GHSA-6wmf-4rv5-p3rp). GitHub. https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp

Sansec Forensics Team. (2026, 21 de octubre). PHP2Shell: PHP RCE (CVE-2026-109618, CVE-2026-109619). Sansec. https://sansec.io/research/php2shell

Tzadik, S., Tamari, S., Ohfeld, N., & Schwartz, T. (2026, 21 de octubre). PHP2Shell: AI uncovers critical RCE vulnerabilities affecting 70% of the Internet. Wiz Blog. https://www.wiz.io/blog/php2shell-php-rce

Créditos de las imágenes · Image credits

Portada / Cover: Datacenter Server Racks. Fotografía de Carl Lender (Sunrise, EE. UU.), CC BY 2.0, vía Wikimedia Commons. Recortada a 850 × 478 px. https://commons.wikimedia.org/wiki/File:Datacenter_Server_Racks_(22370909788).jpg

Capas de defensa / Layers of defence: esquema propio de Actum Forense Press / own diagram by Actum Forense Press.

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *