Cuando sale una vulnerabilidad crítica de PHP, actualizar suele ser tan fácil como ejecutar el actualizador de la distribución. Pero ¿qué pasa cuando el servidor afectado todavía depende de una versión de PHP que ya no recibe parches oficiales en forma de paquete, y la aplicación que corre en él no es compatible con ninguna versión moderna?
En este artículo explicamos, de forma genérica y reproducible, cómo corregimos dos vulnerabilidades críticas de PHP (ejecución de código remoto sin autenticación) en un servidor que tenía que seguir funcionando con PHP 7.4, compilando el parche oficial desde el código fuente y sustituyendo solo la pieza vulnerable.
Aviso: compilar y sustituir componentes de PHP a mano es el último recurso, para casos en los que no hay paquete oficial y no se puede migrar de versión a corto plazo. Siempre que exista un paquete oficial, usad el paquete. Y siempre, siempre, probadlo antes en una copia y tened una vía de marcha atrás.
El contexto del problema
El escenario era este:
- Una aplicación antigua (un CRM heredado) que solo funciona con PHP 7.4 y que no se puede migrar a PHP 8 a corto plazo.
- PHP 7.4 está en fin de vida: el equipo de PHP y los repositorios de terceros ya no publican paquetes nuevos para esta rama.
- Ante una vulnerabilidad crítica, el equipo de seguridad de PHP sí publicó el código fuente del parche para las versiones antiguas, pensado precisamente para quien compila desde el código.
El objetivo: aplicar ese parche al servidor en producción con el mínimo riesgo.
La idea clave: sustituir solo el módulo, no reinstalar PHP
En una instalación típica con Apache y mod_php, PHP se ejecuta dentro de Apache como un módulo: un único fichero (libphp7.4.so). La vulnerabilidad estaba en el núcleo de PHP, que está dentro de ese módulo.
Por tanto, no hace falta recompilar todo PHP ni tocar las extensiones (base de datos, imágenes, correo, etc.), que son ficheros separados. Basta con compilar un módulo nuevo con el parche y sustituirlo, manteniendo el resto igual.


Paso 1: recoger la «receta» de la instalación actual
Antes de nada, hay que saber exactamente cómo está compilado el PHP instalado, para reproducirlo de forma idéntica. Los datos importantes son:
- La versión exacta y los números de API de PHP.
- Si está compilado con Thread Safety o sin él (NTS).
- Las opciones de compilación (
./configure). - Qué extensiones van dentro del núcleo y cuáles son ficheros externos.
La versión y la arquitectura se pueden consultar así:
php -i | grep -E "^(PHP Version|PHP API|Thread Safety|extension_dir)"
Las opciones de compilación, si tenéis el paquete -dev instalado:
php-config --configure-options
Y haced una copia del módulo actual, que servirá de referencia y de marcha atrás:
cp /usr/lib/apache2/modules/libphp7.4.so /root/libphp7.4.so.original
Paso 2: compilar en un entorno aislado e idéntico
Compilar directamente en el servidor de producción es una mala idea: consume recursos y deja herramientas de desarrollo instaladas. La solución limpia es compilar en un contenedor con el mismo sistema operativo que el servidor. Así el resultado es compatible, porque dentro del contenedor reproducimos las mismas bibliotecas que tiene el servidor.
# Contenedor con el mismo SO que el servidor
docker run -it --name build -v "$PWD":/out ubuntu:XX.XX bash
Dentro del contenedor, instalamos las herramientas de compilación y las bibliotecas de desarrollo que PHP necesita (XML, SSL, zlib, etc.):
apt-get update
apt-get install -y build-essential autoconf bison re2c pkg-config git \
apache2-dev libxml2-dev libssl-dev libpcre2-dev zlib1g-dev \
libargon2-dev libsodium-dev
Paso 3: descargar el código fuente oficial con el parche
El punto más importante de todo el proceso: el código tiene que venir de la fuente oficial. El equipo de seguridad de PHP publica el parche en el repositorio oficial de PHP, normalmente en una rama específica para la vulnerabilidad, y siempre lo enlaza desde el aviso oficial de php.net.
git clone --depth 1 -b <rama-oficial-del-parche> https://github.com/php/php-src.git
cd php-src
Verificad que el código es el que tiene que ser. Comprobad la diferencia respecto a la versión oficial que tenéis instalada: solo deberían cambiar los ficheros del parche, no cientos de ficheros.
git fetch --depth 1 origin tag php-7.4.XX
git diff --stat php-7.4.XX HEAD
Si la diferencia son unos pocos ficheros del núcleo (gestión de memoria, procesamiento de peticiones…), es el parche mínimo sobre vuestra versión. Si salen cientos, la rama no corresponde y hay que partir de la versión oficial exacta y aplicarle solo el parche.
Paso 4: compilar con la misma configuración
Se usa la misma receta del paso 1, con estas condiciones:
- Conservar las rutas de configuración (
--with-config-file-path, etc.). - Generar el módulo de Apache con
--with-apxs2. - Mantener el mismo modo de Thread Safety (para Apache prefork, NTS).
./buildconf --force
./configure <las-mismas-opciones-que-el-paquete-original> --with-apxs2=/usr/bin/apxs2
make -j"$(nproc)"
Antes de darlo por bueno, comprobad que la configuración ha dejado las mismas extensiones internas que el módulo original y que no activa Thread Safety si no toca.
Paso 5: probar el módulo antes de ponerlo en producción
Dos comprobaciones valiosas:
a) Las pruebas oficiales del código. El código de PHP incluye miles de pruebas automáticas. Ejecutadlas para confirmar que el módulo compilado es sólido:
make test
b) Una instancia de Apache de prueba. Antes de tocar el servicio real, se puede levantar un Apache de test en un puerto interno que cargue el módulo nuevo con la configuración, las extensiones y los complementos reales, y comprobar que la versión es correcta, que carga todas las extensiones y que conecta con la base de datos. Todo ello sin tocar el servicio en producción.
Paso 6: desplegar y, sobre todo, poder volver atrás
Primero, quitad del módulo la información de depuración para que quede de un tamaño parecido al original:
strip --strip-unneeded libs/libphp7.so -o libphp7.4.so.patched
Copiad el módulo al servidor y comprobad su integridad con la huella (SHA-256), para aseguraros de que ha llegado entero:
sha256sum libphp7.4.so.patched
El despliegue se hace de forma que el actualizador de paquetes no vuelva a sobrescribir nuestro módulo. En sistemas basados en Debian/Ubuntu, esto se consigue con dpkg-divert, que «aparta» el fichero original y deja el nuestro en su sitio:
dpkg-divert --local --rename --add /usr/lib/apache2/modules/libphp7.4.so
install -m 644 libphp7.4.so.patched /usr/lib/apache2/modules/libphp7.4.so
apache2ctl configtest && systemctl restart apache2
Marcha atrás en un minuto, si hace falta:
rm /usr/lib/apache2/modules/libphp7.4.so
dpkg-divert --local --rename --remove /usr/lib/apache2/modules/libphp7.4.so
systemctl restart apache2

Paso 7: verificar que el servicio usa el módulo nuevo
El número de versión no cambia (sigue siendo 7.4.XX), así que hay que confirmarlo de otras formas:
- La fecha de compilación (
Build Datede unphpinfo) tiene que ser la de vuestro despliegue, no la original. - La huella del módulo instalado tiene que coincidir con la que calculasteis.
- Ningún proceso del servidor web debe tener el módulo antiguo cargado en memoria (hay que reiniciar el servicio, no solo recargarlo).
Un efecto colateral importante: los escáneres de vulnerabilidades
Como el número de versión no cambia, los escáneres de vulnerabilidades (que a menudo solo leen la versión) seguirán marcando el servidor como vulnerable. Es un falso positivo. Conviene documentar el proceso (origen del código, integridad, huella del módulo desplegado) para poder justificarlo en una auditoría. Es exactamente el mismo criterio con el que se validan los parches retroportados de las distribuciones: no se comprueba el ataque en cada servidor, se verifica que el código corregido es el que se ejecuta.

Medidas complementarias mientras dure la situación
Compilar el parche resuelve la vulnerabilidad, pero un servidor con PHP y sistema operativo sin soporte sigue siendo una deuda técnica. Mientras no se pueda migrar, vale la pena acompañarlo de:
- Las mitigaciones oficiales que recomiende el aviso de seguridad (a menudo, desactivar funcionalidades concretas y limitar el tamaño de las peticiones).
- Reducir la exposición: limitar el acceso a la red interna o por VPN, y cerrar las herramientas de administración sensibles tras una autenticación adicional.
- Un plan de migración real, porque esta solución es un puente, no un destino.
Conclusión
Cuando una vulnerabilidad crítica afecta a una versión de PHP sin soporte, no estáis atrapados entre «dejarlo abierto» y «romper la aplicación». Si el equipo de PHP publica el código del parche, se puede compilar un módulo corregido, probarlo en una copia y desplegarlo con una vía de marcha atrás clara, manteniendo la versión y la aplicación intactas.
No es el camino ideal (lo ideal es migrar a una versión con soporte), pero es una forma responsable de ganar el tiempo necesario para hacer la migración con garantías, sin dejar un agujero crítico abierto mientras tanto.
Traducción hecha con ayuda de IA a partir del artículo original en catalán.
