Skip to content

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