phpguard: Shield unpatched PHP from php2shell multipart attacks
Requires the Pro plan (or higher) of the GetPageSpeed NGINX Extras subscription.
Installation
You can install this module in any RHEL-based distribution, including, but not limited to:
- RedHat Enterprise Linux 7, 8, 9 and 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 and 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
Enable the module by adding the following at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_phpguard_module.so;
This document describes nginx-module-phpguard v1.0.0 released on Nov 12 2026.
Shield unpatched PHP from php2shell (CVE-2026-109618, CVE-2026-109619 and CVE-2026-115560) at the NGINX layer, without touching PHP.
php2shell is reached through PHP's own multipart/form-data parser. That parser runs before any application code and before any login, so every PHP endpoint is exposed. Plain NGINX rules can only block all multipart requests, which breaks uploads, and they miss the encodings PHP accepts.
phpguard reads each multipart body in NGINX before PHP sees a single byte,
and checks it with a streaming parser that mirrors PHP's own. It has been
verified on PHP 5.6, 7.4, 8.1, 8.4 and 8.5, behind PHP-FPM and behind Apache
mod_php through proxy_pass.
What it blocks
- The session upload progress field, the confirmed php2shell trigger, in every spelling PHP accepts: quoted or not, any case, folded headers, LF-only lines, comma content types, chunked bodies and HTTP/2.
- Smuggled parts. Only canonical multipart is accepted. A preamble, an epilogue, NUL or control bytes, ambiguous quoting, or a boundary string anywhere except a real delimiter is refused. That includes the part PHP finds after 5120 bytes when it skips an upload it does not keep.
- Out-of-memory parses. Each request's parse memory in PHP is estimated
with a cost model measured on every supported PHP version, and the
request is refused above
phpguard_max_memory. Empty file inputs with deeply nested names, for example, can otherwise make PHP allocate over 100 MB for a few kilobytes of input.
Only multipart requests are inspected. GET, JSON and URL-encoded forms cost a single header check.
Configuration
Enable it for the whole server, so the body is checked wherever it is first read:
server {
phpguard block;
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm.sock;
include fastcgi_params;
}
}
Start with phpguard detect; to log what would be refused without
blocking anything.
Directives
| Directive | Default | Context | Description |
|---|---|---|---|
phpguard off \| detect \| block |
off |
http, server, location | block (alias on) refuses; detect only logs |
phpguard_status <code> |
403 |
http, server, location | Status for policy refusals (400-599). Malformed bodies always get 400 |
phpguard_progress_name <name> |
PHP_SESSION_UPLOAD_PROGRESS |
http, server, location | Field name to refuse; repeat for several. Set it if session.upload_progress.name is changed |
phpguard_max_memory <size> |
32m |
http, server, location | Estimated PHP parse memory per request. Keep it well below your smallest memory_limit |
phpguard_max_parts <n> |
1000 |
http, server, location | Parts per body |
phpguard_max_name_length <n> |
512 |
http, server, location | Bytes in a field name |
phpguard_max_nesting <n> |
32 |
http, server, location | [ levels in a field name |
The $phpguard variable holds the refusal reason (empty when the request
passed), for example in a log_format.
Notes
- PHP's upload progress bars (
session.upload_progress) stop working, because their field is exactly what gets refused. - Multipart bodies are fully buffered before PHP receives them. That is
NGINX's default; with
fastcgi_request_buffering offuploads stop streaming to PHP. - A field or file name that ends in a backslash is refused. PHP misparses such names anyway.
- phpguard covers php2shell's request-borne multipart paths. It does not fix PHP: out-of-memory errors your application reaches through other input, and future PHP bugs outside the multipart parser, remain. Update PHP when you can.