Varnish Software Blog

Protecting PHP sites from CVE-2026-109618 with Varnish

Written by Audun Ytterdal | 10/21/26, 4:25 PM

If you run PHP behind Varnish, you can block the reported attack path for CVE-2026-109618 and CVE-2026-109619 at the edge today, before the PHP patch is rolled out. It takes about ten lines of VCL.

The PHP project has announced two critical vulnerabilities, both rated CVSS 9.2 and reported as unauthenticated remote code execution. Early exploitation was reported on 21 October 2026. Only PHP 8.2 and newer will receive fixes. Sites on PHP 8.1 or older have no patch coming.


What we know, and what we don't

The bugs are in how PHP handles multipart/form-data request bodies, the format browsers use for file uploads. PHP had not published full technical details at the time of writing, so treat everything below as provisional.

  • Upload-progress path. A public write-up describes a use-after-free in PHP's session upload-progress handler. A multipart form field named PHP_SESSION_UPLOAD_PROGRESS triggers it, together with a PHPSESSID cookie the attacker chooses. The session.upload_progress.enabled setting is on by default.

  • Multipart parser path. Other reports describe a second bug in the multipart parser itself. It reportedly runs for any multipart POST, whether or not upload progress is enabled.

  • Before your code runs. PHP parses the body before your application starts, so your app doesn't need to use sessions or accept uploads to be exposed. Framework middleware, CSRF checks, login checks and security plugins all run too late to help.

Both reported paths need a request with a multipart Content-Type. Varnish can see and reject that header before the request reaches PHP.

Sources: Mattias Geniar's early-mitigation notes and jackalsyn's write-up. Check php.net security advisories for the official details.

Why Varnish is a good place to stop it

Varnish sees every request first, so a rule there protects every PHP backend behind it at once, with one deploy and no PHP restarts.

The trade-off is that the rule also blocks legitimate file uploads. 

The VCL

Here’s the VCL code that lets Varnish block multipart uploads as a temporary mitigation. Save this as /etc/varnish/php-cve-2026-109618.vcl:

# Early mitigation for PHP CVE-2026-109618 / CVE-2026-109619. # Rejects multipart request bodies before they reach PHP. import std; sub php_cve_block_multipart { # Merge duplicate Content-Type headers so a second header # can't hide the multipart one. std.collect(req.http.Content-Type); # PHP matches the media type case-insensitively, so we do too. if (req.http.Content-Type ~ "(?i)multipart") { return (synth(403, "Forbidden")); } } sub vcl_recv { call php_cve_block_multipart; }

Then include it near the top of your main VCL, before your own vcl_recv:

vcl 4.1; include "php-cve-2026-109618.vcl"; # ... your backends and the rest of your VCL ...

Varnish runs vcl_recv subroutines in the order they appear. Including the file first means the check runs before any of your own logic can return (pass) or return (pipe) a POST straight to the backend.

What each part does:

  • std.collect() merges multiple Content-Type headers into one. Without it, req.http.Content-Type only returns the first one, and an attacker could put a harmless header first.

  • (?i)multipart matches multipart/form-data, MULTIPART/FORM-DATA, mixed case, and values with extra spaces. PHP treats all of these as multipart.

  • synth(403) answers from Varnish itself, so the request body is never sent to the backend.

Load it without a restart:

varnishadm vcl.load php_cve /etc/varnish/default.vcl varnishadm vcl.use php_cve

Allowing uploads where you must

Any exception you make leaves that path open until PHP is patched, so keep each one as narrow as you can. The strongest option is to allow uploads only from networks you trust:

acl php_upload_allowed { "192.0.2.0"/24; # office / VPN range } sub php_cve_block_multipart { std.collect(req.http.Content-Type); if (req.http.Content-Type ~ "(?i)multipart") { if (req.url ~ "^/wp-admin/async-upload\.php" && std.ip(req.http.X-Real-IP, client.ip) ~ php_upload_allowed) { return; } return (synth(403, "Forbidden")); } }

Replace the URL pattern and address range with your own. Use client.ip directly if Varnish isn't behind a TLS terminator or load balancer. If it is, use the header that holds the real client address, and only trust that header from your own proxy.

We don't recommend allowing uploads just because a session cookie is present. Attackers can set any cookie they like, and the reported bug is triggered with a cookie the attacker chooses.

If you can't restrict uploads by network, consider pausing uploads for a few days and showing a maintenance message on those forms instead.

Test it

To test the VCL before you deploy it, save this as php-cve.vtc, change the include line to the absolute path of your VCL file, and run varnishtest php-cve.vtc:

varnishtest "PHP CVE-2026-109618 multipart block" server s1 -repeat 3 { rxreq txresp -body "ok" } -start varnish v1 -vcl+backend { include "php-cve-2026-109618.vcl"; } -start client c1 { txreq -req POST -url /x -hdr "Content-Type: multipart/form-data; boundary=a" -body "--a--\r\n" rxresp expect resp.status == 403 txreq -req PUT -url /x -hdr "Content-Type: MULTIPART/FORM-DATA; boundary=a" -body "--a--\r\n" rxresp expect resp.status == 403 txreq -req POST -url /x -hdr "Content-Type: text/plain" -hdr "Content-Type: multipart/form-data; boundary=a" -body "--a--\r\n" rxresp expect resp.status == 403 txreq -req POST -url /x -hdr "Content-Type: application/x-www-form-urlencoded" -body "a=b" rxresp expect resp.status == 200 txreq -url /x rxresp expect resp.status == 200 } -run

Once the VCL has been deployed, you can run the following commands to validate that multipart requests are blocked and normal traffic still gets through. Every multipart request should get a 403, and the last two should get your normal response:

URL=https://www.example.com/ # Should be 403 curl -s -o /dev/null -w '%{http_code}\n' -F 'a=b' "$URL" curl -s -o /dev/null -w '%{http_code}\n' -X PUT \ -H 'Content-Type: MULTIPART/FORM-DATA; boundary=x' --data-binary $'--x--\r\n' "$URL" curl -s -o /dev/null -w '%{http_code}\n' \ -H 'Content-Type: text/plain' -H 'Content-Type: multipart/form-data; boundary=x' \ --data-binary $'--x--\r\n' "$URL" # Should be your normal status curl -s -o /dev/null -w '%{http_code}\n' -d 'a=b' "$URL" curl -s -o /dev/null -w '%{http_code}\n' "$URL"

To watch blocked requests in production, you can query the Varnish logs for 403 responses that have a multipart content type:

varnishncsa -q 'RespStatus == 403 and ReqHeader:Content-Type ~ "(?i)multipart"'

The logs that appear when you run this command, show what requests were blocked by the mitigation.

Upgrade to the latest version of PHP

The Varnish rule buys you time, but only patching PHP fixes the bug. We recommend enabling our VCL-based mitigation until you have upgraded your PHP version.

As described in PHP’s security advisory: please upgrade your PHP setup to the latest version as quickly as possible. Patches are out for the supported PHP versions, which are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.

If you’re running an older version of PHP, you need to upgrade to a newer version, because no patches are made available for PHP versions 8.1 and older.

Removing the temporary mitigation

Once your PHP installation is patched, you can remove the include from your default.vcl file and run the following command to restore the VCL configuration your server booted with:

varnishadm vcl.use boot

If that config name was not boot, run varnishadm vcl.list to find the available VCLs and load the appropriate config.