Blog
October 23, 2026

PHP2Shell: buy time with a WAF, then patch the servers

PHP2Shell: buy time with a WAF, then patch the servers

Executive summary

On October 21, 2026, the PHP project shipped emergency releases for 2 critical vulnerabilities, CVE-2026-109618 and CVE-2026-109619, now known together as PHP2Shell. Both are rated 9.2 out of 10 on CVSS 4.0, the Critical band. Both allow an unauthenticated attacker to run code on the web server, and the attack completes before a single line of your application runs. The flawed code has been in PHP since version 5.4, so every PHP server your organization operates is affected until it is upgraded, and PHP serves 69.3% of websites.

Attack traffic appeared within hours. Cloudflare reported attacks against its customers on the day of disclosure, with attackers already mutating payloads to slip past filters, and the SANS Internet Storm Center raised its InfoCon level to yellow the same day. Honeypot operators report exploitation attempts against both flaws. Whatever the final count turns out to be, the practical position is simple: an unpatched PHP server exposed to the internet today is a target, and the exploit needs no credentials.

The only complete fix is to upgrade PHP to 8.2.35, 8.3.36, 8.4.27, or 8.5.12 and restart every worker. Servers on PHP 8.1 and older get no numbered release from the PHP project, so the patch comes from your distribution: Ubuntu (USN-8948), Debian, and Red Hat shipped packages on October 21, with Ubuntu 20.04 and older covered through Ubuntu Pro. For most organizations the upgrade takes days to weeks: fleets to inventory, vendors and hosting providers to chase, change windows to book, applications to retest. Those days are the exposure window, and that is what this post is about.

Our recommendation is to close that window today with a virtual patch at the edge, then patch PHP on your own schedule. We built a rule set for the WAF built into Traefik Hub, and we tested it against a real PHP 8.3 application on an isolated cloud VM. It blocked every published attack shape, including the evasions, and let normal logins and file uploads through untouched. The cost was about 7% of throughput on a real PHP page, with the 95th-percentile latency moving from 55 to 60 ms. That is a trade any CISO should accept against unauthenticated remote code execution.

The rest of this post explains the flaws in plain terms, shows the numbers, and gives your platform team the exact configuration to apply. Forward it to them. Then put a date on the PHP upgrade. The virtual patch is a stopgap, not the remediation: the flaw stays in every unpatched worker, and a breach through one of them means exposed customer data, regulator notifications, and a CISO explaining why a published fix was never applied.

In this post: the exact Traefik Hub WAF configuration we tested, what it blocked, what it costs, and how to apply it in minutes.

  • What PHP2Shell is, in plain terms
  • Why the advice to "just patch" takes longer to follow than the attackers need
  • Why a WAF belongs in the architecture, not in the incident plan
  • What we tested, and what it blocked
  • What it costs: the performance trade-off
  • For the platform team: the configuration to apply
  • A stopgap, not the remediation
  • What to do in the next hour

What PHP2Shell is, in plain terms

Both flaws sit in the PHP runtime itself, in the code that prepares an incoming request before your application sees it. That is what makes them unusual. Most web vulnerabilities live in a plugin, a framework, or your own code, and you fix them by updating that component. Here the component is the underlying PHP runtime, so updating WordPress, Laravel, Drupal, Magento, or your own application changes nothing. Only the PHP binary matters, and it has carried this code since PHP 5.4 in 2012.

The first flaw, CVE-2026-109618, is triggered while PHP parses the fields of a form submission. A request with an abnormally long field name, 1,000 bytes or more, drives the worker out of memory while its internal structures are still half built. PHP then tears the request down as if setup had completed, and the mismatch corrupts memory in a way an attacker can steer toward running code. The second, CVE-2026-109619, runs through the session upload-progress feature, which executes on upload requests before application code. PHP keeps a reference to request state after freeing it, and a later request on the same worker reuses that stale memory. The outcome is memory disclosure and, again, remote code execution.

Exposure depends on how PHP is run. PHP's own advisory lists every web-serving installation as at risk: PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP, and LiteSpeed. Apache with mod_php is the confirmed exploitation path, and the advisory notes the attack completes before any application code runs. PHP-FPM is harder to exploit, and the PHP project has said no complete FPM exploit was demonstrated for the first flaw, while warning that the path is not closed. If a hosting provider runs PHP for you, the provider must patch, and you should ask them today whether they have.

Configuration workarounds do not make a server safe. Turning off session upload progress blocks one path of the second flaw and does nothing for the first, and PHP's advisory says so explicitly. Before the release, PHP core developer Gina Peter Banyard put it bluntly: "the only mitigation is patching/upgrading PHP." A further complication for defenders is that the exploitation happens below the application layer, so application logs show little. Teams hunting for compromise are looking for PHP fatal errors ending in "in Unknown on line 0", bursts of large POST requests from one address, and shells spawned by the web server process.

Why the advice to "just patch" takes longer to follow than the attackers need

Everyone agrees on the fix. The problem is the calendar. A PHP upgrade across a real estate means finding every place PHP runs, including the appliances and vendor products that embed it, the container images that must be rebuilt, and the hosting accounts nobody has logged into for a year. Commercial applications are often certified against a specific PHP minor version, so the vendor must bless the new release first. Servers on PHP 8.1 and older have no numbered release from the PHP project, only source backports for 7.4, 8.0, and 8.1, so the patch comes from the distribution. Ubuntu, Debian, and Red Hat shipped packages on October 21; Ubuntu 20.04 and older need Ubuntu Pro. Anything the distribution no longer covers is a migration that was already overdue. Then every worker must be restarted, because a running worker keeps the vulnerable code in memory after the package on disk is replaced.

In a well-run organization that is still days. In most organizations it is weeks. The exploit has been public since October 21, and attack traffic started the same day. Every day between those 2 dates is a day an unauthenticated attacker can take over a server that holds your customer data.

This is where a virtual patch pays for itself. The PHP2Shell attacks have a shape that ordinary traffic usually does not have: a form field name 1,000 characters long, a session identifier submitted as a form field, a multipart header folded across lines to hide its length, a request body that fails to parse. A web application firewall (WAF) sitting in front of PHP reads each request body before PHP does and refuses the ones with those shapes. The PHP worker never receives them, so the memory condition that the exploit depends on never occurs. The WAF does not need to understand the exploit. It only needs to recognize its outline, and that outline is documented in the public advisories.

Deploying it is a configuration change at the gateway, applied once per cluster, and it's in effect within seconds. Nothing in the application changes, no PHP process restarts, and no vendor needs to approve anything. That asymmetry, weeks for the real fix against minutes for the shield, is the whole argument.

Why a WAF belongs in the architecture, not in the incident plan

The reason we could respond to PHP2Shell in a day is that the control point already existed. Every request to a PHP service in our test passes through an API gateway, and the gateway carries a WAF as a built-in middleware. When the advisories landed, the question was never "where do we put a filter" but "what does the filter say." The rule set was written, tested, and attached to the routes without a new component, a new vendor, or a change to any application.

That is the architectural argument for a WAF, and it has little to do with PHP2Shell specifically. Application vulnerabilities arrive on their own schedule and your patch cycle runs on yours. A gateway with an embedded WAF is the one place where you can change what reaches every application in minutes, with one configuration object, while the applications themselves stay untouched. It is the difference between having a fire door and ordering one after the alarm.

Virtual patch at the edge: the WAF runs inside the Traefik Hub gateway binary, so a blocked request never reaches PHP
Virtual patch at the edge: the WAF runs inside the gateway binary

The WAF in Traefik Hub is OWASP Coraza, the open-source engine compatible with ModSecurity's rule language, compiled into the gateway binary rather than bolted on as a separate network hop. Rules are written in the same SecLang syntax that security teams have used for 2 decades, so a rule published by a vendor advisory can be adapted and applied the same afternoon. Traffic that passes the rules continues to the router unchanged. Traffic that the rules catch is refused at the gateway, and the PHP worker never sees it.

What we tested, and what it blocked

We did not want to publish a rule set that had only been checked against the advisories on paper, so we built the attack shapes and ran them. The environment was an isolated virtual machine (4 vCPUs, 16 GB) running a Kubernetes cluster, Traefik Hub 3.21.5 with its native WAF middleware, and a real PHP 8.3 application with a login form and a file upload. Every request shape was derived from the public advisories and built from inert filler, so no working exploit was written or run. The application's own request log told us which requests reached PHP and which never arrived.

The same PHP application was exposed on 2 routes: one with no protection and one with the virtual patch attached. Each attack shape went to both. Without the WAF, every attack shape reached PHP and got a 200. With it, every attack shape was refused at the gateway, and the application log confirms the requests never arrived.

Request Without WAF With the virtual patch
Form field name of 1,500 characters, URL-encoded 200, reached PHP 403
Form field name of 1,500 characters, multipart 200, reached PHP 403
Session ID submitted as a multipart form field 200, reached PHP 403
Same attack, uppercase Content-Type 200, reached PHP 403
Same attack, chunked body with no Content-Length 200, reached PHP 403
Same attack over PUT instead of POST 200, reached PHP 403
Malformed Content-Type meant to confuse the parser 200, reached PHP 400 (fails closed)
Field names of 4 MB, 16 MB, and 60 MB 200, reached PHP 403
Body of 70 MB, above the configured limit 200, reached PHP 413
Ordinary login form 200 200, unchanged
Ordinary PDF upload 200 200, unchanged

A 403 means the request parsed and matched an attack signature. A 400 means the body could not be parsed at all, and the rules refuse it rather than pass it on. A 413 means the body exceeded the configured size limit.

The last 2 rows are the ones a CISO should read first. A virtual patch that breaks logins or uploads will be switched off within the hour, and then you are unprotected again. In our test the normal traffic went through untouched.

The rule set also watches the response. If a request slips through and PHP crashes with the out-of-memory signature the advisories describe, the WAF replaces the crash output with a 502 before it reaches the client, which keeps the crash details out of the attacker's hands. We tested that too: a simulated crash page came back as a 502. One limit of the WAF engine applies here: the response rules read the first 512 KB of a response, so a crash that appears after that much output is not caught. That limit is one more reason to patch quickly.

We also checked what happens if the WAF is left in detection-only mode, where it logs instead of blocks: the oversized-name request reached PHP with a 200. For PHP2Shell, logging is not protection. A log entry only tells you afterwards that you were attacked, so block it.

What it costs: the performance trade-off

The WAF is not free. Inspecting a request body is more work than routing it, and the PHP2Shell rules read every form submission and upload. We measured it with wrk (an open-source HTTP load generator), 64 concurrent connections, 20 seconds per scenario, on the same 4 vCPU virtual machine that ran the gateway and the application, with one gateway replica.

Traffic Requests/s, no WAF Requests/s, with WAF p95 latency, no WAF p95 latency, with WAF
GET to an echo backend 4,728 3,515 29 ms 39 ms
POST login form to the echo backend 4,419 2,917 31 ms 48 ms
POST 30 KB multipart upload to the echo backend 2,680 1,747 47 ms 70 ms
GET to the real PHP page 2,144 1,999 55 ms 60 ms
PHP2Shell attack requests, refused with 403 5,832 24 ms

The echo-backend rows are the worst case by design. The backend there costs almost nothing, so the WAF's share of the total is at its largest: 26% of throughput on plain GETs, 34% on form posts, 35% on uploads that spill to disk. The row that reflects a real deployment is the PHP page, where the application does real work and the WAF's cost shrinks to about 7% of throughput and 5 ms at the 95th percentile. Your own number will fall between those 2 depending on how heavy your PHP pages are, and scaling the gateway horizontally by adding replicas is the usual way to recover headroom.

The attack row at the bottom of the table tells a different story. The gateway refuses attack requests at 5,832 per second, faster than it serves legitimate pages, because a refused request never touches the backend. An attacker who floods you with PHP2Shell payloads is throttled by the cheapest path in the system, and your PHP workers never hear about it. During the whole run the gateway process stayed at about 1 CPU core and 120 MB of memory.

Set against unauthenticated remote code execution, a single-digit percentage of throughput on real pages is an easy decision, and the cost ends when the upgrade lands. It does not finish the job. The virtual patch keeps you out of this week's incident report. Patching PHP keeps you out of next year's.

For the platform team: the configuration to apply

This section is for the people who will do the work. Everything below is what ran in our test, printed as it ran, with only the object name and namespace for you to change. It targets Traefik Hub's native WAF middleware, which is configured with ModSecurity-compatible directives.

The Middleware object is the virtual patch. Apply it in the namespace of the IngressRoutes that front your PHP services.

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: php2shell-virtual-patch
  namespace: app   # the namespace of the IngressRoute that references it
spec:
  plugin:
    coraza:
      directives:
        # ---- Engine ---------------------------------------------------------
        - SecRuleEngine On
        - SecRequestBodyAccess On
        - SecRequestBodyLimit 67108864
        - SecRequestBodyInMemoryLimit 131072
        - SecRequestBodyLimitAction Reject
        # ---- Count/size bounding (aligns with the official php.net advisory) ----
        # Bound the NON-file body: oversized field NAMES are non-file data,
        # so this rejects multi-MB name bodies by size before parsing. Must exceed
        # your largest legitimate non-upload POST.
        - SecRequestBodyNoFilesLimit 1048576
        # Bound upload count and JSON nesting depth (advisory: bound parts/nesting).
        - SecUploadFileLimit 32
        - SecRequestBodyJsonDepthLimit 64
        # Folded multipart part header splits a long name across lines to evade the
        # single-line name rules. Legit clients do not fold part headers.
        - |-
          SecRule MULTIPART_HEADER_FOLDING "@eq 1" "id:3101011,phase:2,deny,status:400,log,msg:'php2shell: folded multipart part header',tag:'php2shell',tag:'CVE-2026-109618',severity:'CRITICAL'"

        # ---- Fail-closed body parsing (primary control for large bodies) --------------
        - |-
          SecRule REQBODY_ERROR "!@eq 0" "id:3101008,phase:2,deny,status:400,log,msg:'Failed to parse request body',logdata:'%{REQBODY_ERROR_MSG}',tag:'php2shell',severity:'CRITICAL'"
        - |-
          SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:3101009,phase:2,pass,log,msg:'Multipart body failed strict validation',tag:'php2shell',severity:'CRITICAL'"

        # ---- CVE-2026-109618: field NAME >= 1000 chars ----------------
        - |-
          SecRule REQBODY_PROCESSOR "@rx ^(?:URLENCODED|MULTIPART)$" "id:3101004,phase:2,deny,status:403,log,msg:'php2shell: form field name >= 1000 chars (parsed)',logdata:'%{MATCHED_VAR_NAME}',tag:'php2shell',tag:'CVE-2026-109618',severity:'CRITICAL',chain"
        - |-
          SecRule ARGS_POST_NAMES|FILES_NAMES "@gt 999" "t:none,t:length"
        - |-
          SecRule REQBODY_PROCESSOR "@streq URLENCODED" "id:3101005,phase:2,deny,status:403,log,msg:'php2shell: urlencoded field name >= 1000 raw chars',tag:'php2shell',tag:'CVE-2026-109618',severity:'CRITICAL',chain"
        - |-
          SecRule REQUEST_BODY "@rx (?:^|&)[^&=]{1000,}"
        - |-
          SecRule REQBODY_PROCESSOR "@streq MULTIPART" "id:3101007,phase:2,deny,status:403,log,msg:'php2shell: multipart part name >= 1000 chars (raw header)',tag:'php2shell',tag:'CVE-2026-109618',severity:'CRITICAL',chain"
        - |-
          SecRule MULTIPART_PART_HEADERS "@rx (?i)\bname\s*=[^\r\n]{1000,}"
        - |-
          SecRule REQBODY_PROCESSOR "@streq URLENCODED" "id:3101010,phase:2,pass,log,msg:'php2shell context: urlencoded body > 1 MiB',setvar:tx.php2shell_suspect=1,tag:'php2shell',severity:'WARNING',chain"
        - |-
          SecRule REQUEST_BODY_LENGTH "@gt 1048576"

        # ---- CVE-2026-109619 php2shell: upload_progress chains ---------------
        - |-
          SecRule REQBODY_PROCESSOR "@streq MULTIPART" "id:3201001,phase:2,deny,status:403,log,msg:'php2shell: session ID in multipart form field',tag:'php2shell',tag:'CVE-2026-109619',severity:'CRITICAL',chain"
        - |-
          SecRule ARGS_POST_NAMES "@streq PHPSESSID"
        - |-
          SecRule REQBODY_PROCESSOR "@streq MULTIPART" "id:3201004,phase:2,pass,log,msg:'php2shell correlate: PHP_SESSION_UPLOAD_PROGRESS field present',setvar:tx.php2shell_suspect=1,tag:'php2shell',severity:'INFO',chain"
        - |-
          SecRule ARGS_POST_NAMES "@streq PHP_SESSION_UPLOAD_PROGRESS"

        # ---- Response side: full transaction correlation ---------------------
        - SecResponseBodyAccess On
        - SecResponseBodyMimeType text/plain text/html text/xml
        - SecResponseBodyLimit 524288
        - SecResponseBodyLimitAction ProcessPartial
        - |-
          SecRule RESPONSE_BODY "@rx Allowed memory size of \d+ bytes exhausted[^\r\n]{0,300}in Unknown on line" "id:3301001,phase:4,deny,status:502,log,msg:'php2shell: PHP OOM outside script execution in response',tag:'php2shell',severity:'CRITICAL'"
        - |-
          SecRule TX:php2shell_suspect "@eq 1" "id:3301002,phase:4,deny,status:502,log,msg:'php2shell: suspect request produced PHP failure in response',tag:'php2shell',severity:'CRITICAL',chain"
        - |-
          SecRule RESPONSE_STATUS|RESPONSE_BODY "@rx ^5\d\d$|Allowed memory size of \d+ bytes exhausted"

Attach it to every route that fronts PHP by adding it to the route's middleware list:

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: my-php-app
  namespace: app
spec:
  entryPoints: [web]
  routes:
    - match: Host(`app.example.com`)
      kind: Rule
      middlewares:
        - name: php2shell-virtual-patch   # the WAF rules applied
      services:
        - name: my-php-app
          port: 80

Then prove it from a shell, with an inert request. The first command must return 403 and the second 200:

# oversized field name: must be refused
head -c 1500 </dev/zero | tr '\0' a | { cat; printf '=x'; } | \
  curl -s -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-binary @- https://app.example.com/login

# ordinary form: must pass
curl -s -o /dev/null -w '%{http_code}\n' -d 'username=alice&password=hunter2' https://app.example.com/login

5 things we learned in testing that you should know before you ship it:

  1. The engine must be On, which means blocking. In DetectionOnly mode the oversized-name request reaches PHP with a 200. There is no "monitor first" phase for this one.
  2. Keep the 2 parser-guard rules (3101008 and 3101009). Above the 128 KB in-memory limit, Coraza spools the body to disk and the strict parser guards are what refuse the large payloads. Removing them to reduce noise reopens the multi-megabyte variants.
  3. Size the gateway's temporary directory. Bodies above 128 KB spool to /tmp. A full /tmp fails closed, which means every upload returns 400 until it is cleared. With the Helm chart, set deployment.tmpVolume.emptyDir.sizeLimit (we used 2 GiB).
  4. Check your largest legitimate non-upload POST against the 1 MB non-file body limit. If you have forms that legitimately post more than 1 MB of fields, raise SecRequestBodyNoFilesLimit before you deploy, or that form will break.
  5. Close the side doors. Add a NetworkPolicy so the PHP pods accept traffic only from the gateway's namespace. A WAF on the route does nothing for a request that bypasses the route.

A stopgap, not the remediation

We want to be as clear about the limits as about the results. A virtual patch blocks the attack shapes that are known today. The flaws themselves remain in every unpatched PHP worker behind the gateway, and the exploit community is already varying payloads, as Cloudflare observed on day 1. If someone finds a request shape that triggers the same memory condition without matching these rules, the shield has a hole and the server behind it is as vulnerable as it was on October 20. Rules can be tightened as new shapes appear, and we will update ours, but a rule set is always a description of the attacks we have seen.

There are also paths the gateway does not see. Traffic that reaches PHP without passing through the gateway, from another workload within the cluster, from a management network, or from a sidecar that talks to the application directly, is unfiltered. In our test cluster we closed that hole with a Kubernetes NetworkPolicy that allows traffic to the PHP pods only from the gateway's namespace, and we recommend the same. A WAF protects the door it stands in front of, so the other doors must be closed by other means.

So treat the virtual patch the way you would treat a tourniquet. Apply it immediately, because the alternative is worse. Then get to the hospital. Our suggested sequence:

  1. Today: attach the virtual patch to every route that fronts PHP, in blocking mode. Confirm with a test request that oversized field names return 403 and a normal login returns 200.
  2. This week: inventory every PHP runtime you operate or rent, including appliances and hosted platforms, and open the upgrade ticket for each with a named owner and a date.
  3. As each upgrade lands: upgrade to PHP 8.2.35, 8.3.36, 8.4.27, or 8.5.12 and restart every worker. Workers on PHP 8.1 and older need the backports or a migration.
  4. After the fleet is patched: leave the rules in place. They cost little, and the next PHP advisory may rhyme with this one.

The mistake we want to prevent is the comfortable one: the virtual patch works so well that the upgrade ticket slides from this sprint to next quarter. The CISO who signs off on the virtual patch should sign off on the patch date in the same meeting.

What to do in the next hour

If you run PHP anywhere on the internet, you are in the window right now. Send this post to the team that owns your gateway and ask for 2 things by end of day: the virtual patch attached in blocking mode to every PHP route, and a dated plan for the PHP upgrade. If you already run Traefik Hub, the configuration above is all you need. If you do not, the WAF is part of the standard Hub distribution: get in touch with us and we will help you put it in front of your PHP services this week. If you use another gateway with a ModSecurity-compatible WAF, the same rules should translate with little effort. We cannot guarantee how they perform on another vendor's engine, so test them there first, but protected on someone else's product still beats exposed on none.

Patch PHP. Until you have, do not run unprotected.

About the Author

Principal Product Manager with 14+ years of tech industry experience, excelling at connecting business needs with technology, driving innovation and strategy. CKA, CPM, CSM, AWS CP, homelabber, former CTO.

Latest from Traefik Labs

Grounded, Not Guessing: Inside the Traefik MCP Server
Blog

Grounded, Not Guessing: Inside the Traefik MCP Server

Read more
One Factory. One Runtime. Every Call Governed.
Blog

One Factory. One Runtime. Every Call Governed.

Read more
The EU Cyber Resilience Act and Your Infrastructure Layer
Report

The EU Cyber Resilience Act and Your Infrastructure Layer

Read more