Vulnerabilidad crítica en PHP: por qué actualizar hoy
El 21 de octubre, el equipo de PHP publicó un aviso con un título que nadie quiere leer: "Critical remote code execution vulnerabilities, update now". Son dos fallas. Juntas, permiten que alguien ejecute código en tu servidor sin iniciar sesión. Esta vulnerabilidad crítica en PHP afecta a casi cualquier instalación que atienda peticiones web. Y en mod_php el ataque ocurre antes de que corra cualquier línea de tu aplicación.
Quizás estés pensando: "Diego, mi stack es TypeScript y Bun, esto no va conmigo". Calma. Yo también lo pensé durante unos dos segundos. Después me acordé del WordPress del cliente, del panel viejo que nadie se atreve a apagar y de aquel VPS que corre un phpMyAdmin "solo para emergencias".
Qué pasó, en palabras simples
El comunicado oficial de PHP describe dos fallas distintas. Las dos tienen que ver con cómo PHP gestiona la memoria y el estado entre una petición y otra.
CVE-2026-109618, corrupción por falta de memoria. Cuando PHP se queda sin memoria en medio del armado de los datos internos de la petición, se detiene a mitad de camino. Después, la rutina de limpieza mira esos datos incompletos y los trata como si fueran válidos. Un dato inválido tratado como válido, dentro del runtime, es la receta clásica para la ejecución de código.
CVE-2026-109619, estado global "viejo". Un pedazo de estado interno, que pertenece a una petición, sigue vivo después de que su memoria fue liberada. La siguiente petición atendida por el mismo worker puede usar ese estado antes de que la inicialización normal lo limpie todo. Es un use-after-free entre peticiones.
Los detalles técnicos completos están en los advisories de GitHub (GHSA-w8hw-w9fx-j79w y GHSA-6wmf-4rv5-p3rp).
Las versiones corregidas son estas:
- PHP 8.5.12
- PHP 8.4.27
- PHP 8.3.36
- PHP 8.2.35
¿Quién está expuesto a esta vulnerabilidad crítica en PHP?
Respuesta corta: casi todo el que sirve PHP por la web. El aviso menciona PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP y LiteSpeed. El mayor riesgo está en mod_php, donde ya existe un exploit demostrado con una petición sin autenticación.
Ese detalle de "antes de que corra cualquier código de la aplicación" es lo que más pesa. Significa que tu framework no te salva. Tu middleware de autenticación no te salva. El WAF de la aplicación, ese if ingenioso en la entrada de Laravel, nada de eso llega a ejecutarse.
Cuando la falla está en el runtime, el código bonito de tu aplicación ni siquiera llega a consultarse.
Y está el caso que más me preocupa: PHP 8.1 y anteriores. El proyecto ya no da soporte a esas versiones. Por la gravedad, el equipo de seguridad publicó backports mínimos en el código fuente para 7.4, 8.0 y 8.1. Pero eso solo ayuda a quien compila desde el fuente. Si instalaste con el gestor de paquetes de una distro vieja, tienes que verificar si tu proveedor (distro o vendor de LTS) hizo el backport.
Cómo verificar y actualizar ahora
Primer paso: descubrir qué tienes corriendo. Parece obvio, pero mucha gente descubre un servidor PHP olvidado justo en un día como este.
# versión del CLI (¡puede ser distinta de la que atiende la web!)
php -v
# versión del FPM
php-fpm8.3 -v
# en un contenedor
docker exec mi-contenedor php -v
Atención al comentario del primer comando. Ya vi servidores con PHP 8.3 en el CLI y un FPM 8.1 atendiendo el sitio, porque alguien instaló las dos versiones y se olvidó de una. Revisa lo que atiende peticiones, no lo que aparece en la terminal.
Una forma rápida de confirmarlo desde la propia aplicación:
<?php
// ponlo temporalmente, accede y BÓRRALO enseguida
echo PHP_VERSION;
Después, actualiza por el camino que uses:
- Distro Linux: las distribuciones recibieron los parches antes de la divulgación. Corre la actualización normal del sistema y revisa si cambió la versión del paquete.
- Docker: cambia el tag de la imagen a la versión corregida, haz el rebuild y el redeploy. Si usas
php:8.3-fpmsin fijar el patch, undocker pulllo resuelve. Pero solo si realmente descargas la imagen nueva y recreas el contenedor. - Hosting compartido: abre un ticket. En serio. Pregunta la versión exacta.
Y reinicia todos los workers. Un PHP-FPM con un proceso viejo vivo sigue corriendo código viejo. Y la segunda falla vive justamente en el estado que sobrevive entre peticiones.
¿Y si no puedo actualizar hoy?
El propio aviso es honesto aquí: solo la actualización lo resuelve de verdad. Las medidas de abajo reducen caminos de ataque conocidos, pero no dejan la instalación segura. Tómalas como una curita para ganar algunas horas, no como solución.
Para la falla de memoria (CVE-2026-109618):
; en el pool del FPM o en la config de Apache, donde la aplicación no lo sobrescriba
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
La lógica es quitarle al atacante el control sobre el momento en que PHP se queda sin memoria. Si la aplicación necesita uploads, el aviso sugiere post_max_size = 32M, siempre con upload_max_filesize por debajo de eso. También conviene limitar en el proxy el tamaño del cuerpo, la cantidad de campos y de partes multipart.
Sí, memory_limit=-1 da escalofríos. Por eso la recomendación es usar un contenedor o un cgroup como límite real, de preferencia matando solo el worker afectado. Y un detalle importante: no uses RLIMIT_AS ni RLIMIT_DATA en su lugar. Pueden causar justamente la falla de asignación que lleva al bug.
Para la falla de estado global (CVE-2026-109619):
session.upload_progress.enabled = 0
; solo en PHP 8.5:
fatal_error_backtraces = 0
Esto tiene que estar en el php.ini, en el pool del FPM o en la config del servidor. No sirve ini_set() ni .user.ini, porque el ataque ocurre antes de que eso se lea. Reinicia todos los workers después.
Hay una recomendación más que me pareció interesante: esconder errores no ayuda. Usar @, display_errors=0 o tocar el error_reporting no cierra nada. Lo que ayuda es quitar configuraciones inválidas u obsoletas aplicadas por petición, que generan diagnósticos antes de la inicialización. Y corregir los fatal errors y timeouts que un atacante pueda provocar.
Por qué esto importa aunque ni escribas PHP
PHP todavía mueve una parte enorme de la web. Si eres dev full stack y atiendes clientes, es muy probable que te toque un WordPress, un Magento o un sistema legado. Muchas veces ni fuiste tú quien lo subió. Pero eres tú quien recibe la llamada cuando el sitio empieza a redirigir a un casino.
La lección de arquitectura también vale para el mundo Node y Bun. La segunda falla nace de la reutilización del worker: el mismo proceso atiende varias peticiones y algo se filtra de una a otra. Quien escribe servidores en Node vive esto todos los días. Una variable en el scope del módulo, un caché en memoria sin clave por usuario, y listo: el dato de una petición aparece en otra. En PHP el bug está en el runtime. En nuestro caso, normalmente está en nuestro código.
// el equivalente "casero" del bug, en cualquier servidor JS
let usuarioActual: User | null = null // estado global compartido
app.get('/perfil', async (req) => {
usuarioActual = await auth(req) // una petición concurrente puede sobrescribirlo
return render(usuarioActual)
})
Si tienes algo así, vale la pena revisarlo hoy también.
Mi opinión
El equipo de PHP hizo el trabajo bien. Avisó a las distros antes, publicó versiones para todas las ramas con soporte, hizo backport para versiones muertas y listó mitigaciones con la franqueza de decir "esto no lo resuelve". Poca gente escribe un aviso de seguridad tan útil.
El problema real no es PHP. Es el servidor que nadie recuerda que existe. Mi regla es simple: si no tiene dueño, se apaga. Si tiene dueño, se actualiza esta semana, no en el próximo sprint. El inventario de servidores es aburrido hasta el día en que te salva.
Si quieres ver cómo organizo el deploy y las actualizaciones en mis proyectos, échale un vistazo a algunos de los proyectos que mantengo.
Resumen para LinkedIn
Tu stack puede ser 100% TypeScript y aun así puede haber un PHP vulnerable con tu nombre en la guardia. El día 21, PHP publicó dos fallas que permiten ejecutar código en el servidor sin login. En mod_php el ataque ocurre antes de que corra cualquier línea de la aplicación. O sea: el framework, el middleware y la autenticación ni siquiera llegan a consultarse. Corrige hoy: 8.5.12, 8.4.27, 8.3.36 o 8.2.35. Revisa la versión que atiende las peticiones, no la de la terminal, y reinicia todos los workers. Mi regla para un servidor olvidado es simple: si no tiene dueño, se apaga. Si tiene dueño, se actualiza esta semana. Escribí el paso a paso completo, con las mitigaciones y el paralelo con Node y Bun. Si tienes un WordPress viejo que cuidar, el link está en los comentarios. #PHP #SeguridadInformática #DevOps #DesarrolloWeb #FullStack