php2shell Mitigation: 7 Steps Before Patching
On October 21, 2026, two remote code execution vulnerabilities in PHP went public: php2shell, CVE-2026-109618 and CVE-2026-109619, both CVSS v4.0 9.2.
The real fix is to update. Some environments cannot update today, though, because of testing or migration work. A production server still on PHP 7.4, which gets no official fix at all, is the hardest case.
This post collects 7 mitigations you can apply before you can patch, using php.net’s advisory as the primary source. For each one it also lists what it does not cover.

What php2shell does
According to php.net, the issue affects “all PHP installations serving web requests”: PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. The risk is highest for mod_php, where a demonstrated exploit lets an unauthenticated request trigger the attack before any application code runs.
| CVE | What it is (summarized from php.net) |
|---|---|
| CVE-2026-109618 | Out-of-memory handling can interrupt PHP while internal request data is only partially initialized. Request cleanup may then treat the invalid data as valid |
| CVE-2026-109619 | Request-owned internal state can survive after its memory has been freed. A later request on the same worker may use that stale state before initialization resets it |
Sansec describes two outcomes for an attacker: code execution without login, and a Heartbleed-like memory leak that can expose passwords, session tokens and API keys from other users’ requests. Sansec also notes that every known trigger is a multipart/form-data request.
The official bottom line: no complete mitigation
php.net puts it plainly:
There are no complete mitigations, updating is the only safe choice.
The seven steps below buy time until you update. Applying all of them does not make a server safe.
| Branch | Status |
|---|---|
| 8.2 / 8.3 / 8.4 / 8.5 | Fixed (8.2.35 / 8.3.36 / 8.4.27 / 8.5.12) |
| 7.4 / 8.0 / 8.1 | No more releases from the PHP project. Minimal source backports only |
7 mitigations before you can patch
1. Set session.upload_progress.enabled to 0 (CVE-2026-109619)
php.net lists this as the mitigation for the confirmed session paths of CVE-2026-109619.
; /etc/php.d/99-php2shell-mitigation.ini
session.upload_progress.enabled = 0
In an FPM pool:
; /etc/php-fpm.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off
Two things to watch:
ini_set()and FPM.user.inido not work. Use php.ini, a pool or server configuration that applies before request parsing- Restart every worker
php.net is clear that this blocks the confirmed paths only, not other variants.
2. Set memory_limit to -1 (CVE-2026-109618)
CVE-2026-109618 starts from the PHP memory limit failure. php.net suggests removing that limit in a way applications cannot override:
; FPM pool
php_admin_value[memory_limit] = -1
Check that no pool, directory or application setting puts a finite limit back. php.net adds that an underlying allocation failure can still reach the vulnerable path.
Without a limit, one request can eat all the memory. Pair this with step 4.
3. Lower post_max_size and limit size at the edge (CVE-2026-109618)
php.net’s numbers:
| Application | post_max_size |
|---|---|
| No file uploads | 8M |
| Needs uploads | 32M |
Keep upload_max_filesize below post_max_size.
php.net also recommends enforcing limits at the web server or proxy for decoded request size, input field count, multipart part count, header count and nesting depth, including requests without a usable Content-Length. In nginx, client_max_body_size at least caps the whole body.
4. Put a container or cgroup memory limit around PHP
Step 2 removes PHP’s own limit, so put an availability boundary outside it. php.net recommends a policy that kills only the affected worker.
With systemd:
# systemctl edit php-fpm
[Service]
MemoryMax=4G
OOMPolicy=continue
Without OOMPolicy, a single worker killed by the OOM killer can take the whole service down, because systemd’s default is stop.
Do not use RLIMIT_AS or RLIMIT_DATA instead. php.net warns they can cause the vulnerable allocation failure themselves.
5. Block multipart at the edge
Per Sansec, every known trigger is multipart/form-data. Pages that never take uploads have no reason to receive multipart. Blocking it in a WAF or reverse proxy removes paths before they reach PHP.

In AWS WAF, block requests whose Content-Type contains multipart and whose Host is not the one that needs uploads:
{
"Name": "block-multipart-except-upload-host",
"Priority": 0,
"Action": { "Block": {} },
"Statement": {
"AndStatement": {
"Statements": [
{ "ByteMatchStatement": {
"SearchString": "multipart",
"FieldToMatch": { "SingleHeader": { "Name": "content-type" } },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "CONTAINS" } },
{ "NotStatement": { "Statement": { "ByteMatchStatement": {
"SearchString": "admin.example.com",
"FieldToMatch": { "SingleHeader": { "Name": "host" } },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "EXACTLY" } } } }
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "block-multipart-except-upload-host"
}
}
Check three things first:
- Which features really use multipart. Look at
FormData/$_FILESin the code and at POST paths in the access logs - Whether the exclusion can be bypassed. The Host header can be spoofed. Make sure the PHP paths on the excluded host are protected, for example by an IP allowlist. If several sites share one Web ACL, sending “the other site’s admin host” to a different ALB slips through. Use one ACL per ALB
- Whether it actually blocks. After switching to Block, send a body-less GET with a multipart Content-Type to a static file. PHP does not parse GET bodies, so this is safe in production
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Content-Type: multipart/form-data; boundary=x' \
https://www.example.com/robots.txt
# 403 means the rule works
Multipart to the excluded host still gets through, so steps 1 to 4 have to cover it.
6. Extra setting on PHP 8.5, and remove early diagnostics (CVE-2026-109619)
php.net lists more for CVE-2026-109619:
- On PHP 8.5, set
fatal_error_backtraces=0and restart every worker. This blocks only the confirmed fatal backtrace path - Remove invalid or deprecated settings applied per request that emit diagnostics before initialization. Hiding them with
error_reporting,display_errorsor@does not help - Fix fatal errors and execution timeouts an attacker can reach, including failures in callbacks, upload or import processing and cleanup
7. On PHP 7.4 / 8.0 / 8.1, apply the backports or Remi’s update
For branches without an official release, php.net published minimal source backports (7.4 / 8.0 / 8.1) for people building from source.
Many RPM-based PHP 7.4 servers use Remi’s repository (rpms.remirepo.net). The php.net advisory said “the repo will not have updates today”, but 7.4.33-31 was published on 10/21 anyway.
# module packages (for SCL use 'php74-php*')
sudo dnf update 'php*'
# confirm it from the changelog
rpm -q --changelog php-common | head -5
You are on the update if the changelog shows:
- Mitigation fix Avoid state corruption via bailout
CVE-2026-109618, CVE-2026-109619
It is labeled a “Mitigation fix”. The changelog alone does not tell you whether it matches the 8.x fixes.
Roll it out one server at a time
Changing ini settings needs a php-fpm reload. With process_control_timeout at its default of 0, in-flight requests return 502 at the moment of the reload.
Behind a load balancer, take servers out one by one:
aws elbv2 deregister-targets --target-group-arn "$TG_ARN" --targets Id="$ID"
aws elbv2 wait target-deregistered --target-group-arn "$TG_ARN" --targets Id="$ID"
# on the server: drop in the config, reload, check the value
# systemctl reload php-fpm
# php-fpm -i | grep session.upload_progress.enabled
aws elbv2 register-targets --target-group-arn "$TG_ARN" --targets Id="$ID"
aws elbv2 wait target-in-service --target-group-arn "$TG_ARN" --targets Id="$ID"
php-fpm -i shows php.ini values. For values set in a pool with php_admin_value, check the output of php-fpm -tt.
With Auto Scaling, settings applied by hand do not reach new instances. Update the launch template or AMI too.
What I saw on my own production system
On the night of the disclosure, I applied step 1 (session.upload_progress.enabled=0) and step 5 (multipart block) to a PHP 7.4 production system I maintain, one server at a time with no downtime.
Right after the WAF went in, multipart POSTs arrived: 6 IPs in 5 countries, each sending the same two payload sizes. Every User-Agent was Python aiohttp.
| Attack | Outcome |
|---|---|
| The first one (to the apex domain) | The apex to www 301 redirect turned the POST into a GET, so the payload never arrived |
| The next 5 IPs | Blocked by the multipart WAF rule |

Within what I checked, there were no signs of compromise. The full timeline is in my Qiita write-up (Japanese).
Where the docs and reality differed
These are things you only learn by stepping on them.
| Expectation | What happened |
|---|---|
| php.net: “Remi will not have updates today” | 7.4.33-31 shipped the same day. It was in the repository before any post on Remi’s blog |
| Switching the WAF rule from Count to Block makes it block | Feeding get-web-acl output straight back into update-web-acl double-encoded the search string, so the rule looked for bXVsdGlwYXJ0 instead of multipart. No error |
| Adding managed rules is a safe extra | CommonRuleSet’s NoUserAgent_Header matched large amounts of legitimate traffic from a native app and WebSockets. Blocking right away would break the app |
After any WAF change, decode the search string and confirm the curl test returns 403:
aws wafv2 get-web-acl --scope REGIONAL --name "$ACL_NAME" --id "$ACL_ID" \
| jq -r '.WebACL.Rules[0].Statement.AndStatement.Statements[0]
.ByteMatchStatement.SearchString' | base64 -d
# should print: multipart
What stays open, and checking for compromise
What remains even with all seven in place:
| Still open | Why |
|---|---|
| Variants beyond the confirmed paths | php.net says steps 1 and 6 cover confirmed paths only |
| Pages that legitimately accept multipart | The host excluded in step 5 still receives it |
| Allocation failures outside PHP’s limit | Can still reach the vulnerable path with step 2 |
| A server that was already compromised | Mitigations close the door. A planted web shell or cron job stays |
For that last row, check for compromise:

- Are php-fpm worker crashes (signal 11 and similar) up?
- Any memory limit or execution timeout errors?
- Any shells or unexpected child processes under php-fpm?
- Are recently modified PHP files and binaries all from your own releases?
- Any cron changes or unfamiliar outbound connections?
When counting log lines per day, do not sort 21-Oct-2026 style dates as plain strings. They do not sort by month, and tail can drop the latest month. Filter by the month first.
Once you have updated, you can remove the mitigations. Before you do, confirm the fixed version from the version string or the changelog.
References
Was this article helpful?