Saltar a contenido

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 off las 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.