phpguard: Protege PHP sin parchear frente a ataques multipart de php2shell
Requiere el plan Pro (o superior) de la suscripción GetPageSpeed NGINX Extras.
Instalación
Puedes instalar este módulo en cualquier distribución basada en RHEL, incluyendo, entre otras:
- RedHat Enterprise Linux 7, 8, 9 y 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 y Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-phpguard
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-phpguard
Habilita el módulo añadiendo lo siguiente al principio de /etc/nginx/nginx.conf:
load_module modules/ngx_http_phpguard_module.so;
Este documento describe nginx-module-phpguard v1.0.0 publicado el 12 de noviembre de 2026.
Protege PHP sin parchear frente a php2shell (CVE-2026-109618, CVE-2026-109619 y CVE-2026-115560) en la capa de NGINX, sin tocar PHP.
Se llega a php2shell a través del propio analizador de multipart/form-data de PHP. Ese analizador se ejecuta antes que cualquier código de la aplicación y antes de cualquier inicio de sesión, por lo que todo endpoint PHP queda expuesto. Las reglas simples de NGINX solo pueden bloquear todas las peticiones multipart, lo que rompe las subidas, y pasan por alto las codificaciones que PHP acepta.
phpguard lee cada cuerpo multipart en NGINX antes de que PHP vea un solo byte,
y lo comprueba con un analizador en streaming que replica el propio de PHP. Se ha
verificado en PHP 5.6, 7.4, 8.1, 8.4 y 8.5, detrás de PHP-FPM y detrás de Apache
mod_php mediante proxy_pass.
Qué bloquea
- El campo de progreso de subida de sesión, el desencadenante confirmado de php2shell, en todas las grafías que PHP acepta: entre comillas o no, cualquier uso de mayúsculas y minúsculas, cabeceras plegadas, líneas solo con LF, tipos de contenido con comas, cuerpos chunked y HTTP/2.
- Partes introducidas de forma encubierta. Solo se acepta multipart canónico. Se rechaza un preámbulo, un epílogo, bytes NUL o de control, comillas ambiguas, o una cadena de boundary en cualquier lugar excepto en un delimitador real. Eso incluye la parte que PHP encuentra después de 5120 bytes cuando omite una subida que no conserva.
- Análisis que agotan la memoria. La memoria de análisis de cada petición en PHP se estima
con un modelo de coste medido en cada versión de PHP soportada, y la
petición se rechaza por encima de
phpguard_max_memory. Las entradas de archivo vacías con nombres profundamente anidados, por ejemplo, pueden hacer que PHP asigne más de 100 MB para unos pocos kilobytes de entrada.
Solo se inspeccionan las peticiones multipart. Las peticiones GET, JSON y formularios codificados por URL cuestan una sola comprobación de cabecera.
Configuración
Habilítalo para todo el servidor, de modo que el cuerpo se compruebe allí donde se lea por primera vez:
server {
phpguard block;
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm.sock;
include fastcgi_params;
}
}
Empieza con phpguard detect; para registrar lo que se rechazaría sin
bloquear nada.
Directivas
| Directiva | Valor predeterminado | Contexto | Descripción |
|---|---|---|---|
phpguard off \| detect \| block |
off |
http, server, location | block (alias on) rechaza; detect solo registra |
phpguard_status <code> |
403 |
http, server, location | Estado para rechazos por política (400-599). Los cuerpos malformados siempre reciben 400 |
phpguard_progress_name <name> |
PHP_SESSION_UPLOAD_PROGRESS |
http, server, location | Nombre de campo a rechazar; repítelo para varios. Establécelo si se cambia session.upload_progress.name |
phpguard_max_memory <size> |
32m |
http, server, location | Memoria de análisis de PHP estimada por petición. Mantenla muy por debajo de tu memory_limit más pequeño |
phpguard_max_parts <n> |
1000 |
http, server, location | Partes por cuerpo |
phpguard_max_name_length <n> |
512 |
http, server, location | Bytes en un nombre de campo |
phpguard_max_nesting <n> |
32 |
http, server, location | Niveles de [ en un nombre de campo |
La variable $phpguard contiene el motivo del rechazo (vacía cuando la petición
pasó), por ejemplo en un log_format.
Notas
- Las barras de progreso de subida de PHP (
session.upload_progress) dejan de funcionar, porque su campo es exactamente lo que se rechaza. - Los cuerpos multipart se almacenan completamente en búfer antes de que PHP los reciba. Ese es
el comportamiento predeterminado de NGINX; con
fastcgi_request_buffering offlas subidas dejan de transmitirse en streaming a PHP. - Se rechaza un nombre de campo o de archivo que termine en barra invertida. PHP analiza mal esos nombres de todos modos.
- phpguard cubre las rutas multipart transportadas en la petición de php2shell. No corrige PHP: los errores de agotamiento de memoria a los que llega tu aplicación mediante otras entradas, y los futuros bugs de PHP fuera del analizador multipart, siguen presentes. Actualiza PHP cuando puedas.