Operating system, libraries and runtimes

PHP: PHP2Shell and session audit

PHP2Shell Mitigation

We published this script before upstream fixes became available for two zero-day vulnerabilities in PHP. It adds two layers of defense to every installed PHP-FPM from 7.0 through 8.5:

  1. An ini drop-in that turns off session.upload_progress. It is one layer of protection. The patched PHP release is the complete fix.
  2. An AppArmor profile that stops FPM from executing shells and other binaries. This is the layer that keeps a successful exploit from turning into remote code execution. The profile is built from each version's own FPM config (log paths, sessions, sockets, extension directory), so it fits your server instead of a generic template. It lets FPM signal readiness to systemd, which every Debian, Ubuntu and sury unit requires (Type=notify). Where the parser supports it (AppArmor 3.0 and later), it declares abi <abi/3.0>, so the rules mean the same thing on Ubuntu and Debian 13.

The script includes checks and rollback procedures intended to reduce the risk of service disruption. It syntax-checks each profile before installing it. Then it tests FPM with a config check, a restart, a reload, and a socket check. Under systemd it also checks that the unit didn't restart itself during the test. PHP 8.4 and later units use Restart=on-failure, which can make a crash-looping FPM look active. If FPM won't come up, it falls back to complain mode, and if that still fails, it unloads the profile. Each PHP version is handled on its own, so a problem with one never blocks the others.

PHP2Shell quick start

  1. Download it. The repository is mirrored on GitHub and Codeberg; pick a base URL.

    # BASE from GitHub:   https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main
    # BASE from Codeberg: https://codeberg.org/PrivacySafe/security-patches/raw/branch/main
    # curl -fLO "$BASE/php/php2shell-mitigate.sh"
    curl -fLO https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main/php/php2shell-mitigate.sh
    
  2. Read it before running it. Always.

    less php2shell-mitigate.sh
    
  3. Make it executable.

    chmod +x php2shell-mitigate.sh
    
  4. Check the environment. This changes nothing and needs no root.

    ./php2shell-mitigate.sh check
    
  5. Apply, to every installed PHP-FPM, in complain mode.

    sudo ./php2shell-mitigate.sh
    

The repository lives on GitHub and Codeberg. To confirm both serve the same script, compare checksums:

sha256sum php2shell-mitigate.sh
curl -fsSL https://codeberg.org/PrivacySafe/security-patches/raw/branch/main/php/php2shell-mitigate.sh | sha256sum

Every command begins with an environment check and refuses to proceed on unsupported systems. See Environment check.

New profiles start in complain mode, which logs but does not block. Let real traffic run, then review and enforce:

sudo ./php2shell-mitigate.sh status     # what the profile would have blocked
sudo ./php2shell-mitigate.sh enforce    # start blocking

If status lists paths your app legitimately needs, such as an uploads or cache directory, add them to /etc/apparmor.d/local/usr.sbin.php-fpm<version> and rerun the script. That file is never overwritten.

PHP2Shell commands

Command What it does
check Environment report only. Changes nothing and doesn't need root. Exits 0 if clear, 1 on a hard blocker, 2 if --ignore-env would be needed
apply (default) Installs the drop-in and profile, then tests FPM
status Shows mitigation state and the AppArmor events logged under the current profile (since it was last written, at most 24 hours)
enforce Switches to enforce mode. Refuses if complain mode logged accesses you haven't handled (override with --force)
complain Switches back to log-only mode
hunt Read-only log triage. Scans PHP-FPM, Apache and nginx logs for the memory-corruption crashes these bugs cause (including AddressSanitizer/debug-build double-free and use-after-free backtraces, and crashes through the built-in CLI server, php -S), and for signs of exploitation. Changes nothing, doesn't need root. Exits 0 if nothing matched, 1 if it found something to look at
version Prints the script version. Every other command prints it on its first line too
cleanup Removes the ini drop-in once you're patched, except session.upload_progress.enabled = Off, which stays until the build is also fixed for CVE-2026-115560 (set PATCHED_115560_X_Y once PHP names the release, or the package changelog naming the CVE is enough; --force removes it anyway). The AppArmor profile is left loaded, since it confines FPM against the next bug too (the patch doesn't). Once patched, either switch it on for real with enforce, or take it out with remove
remove Removes the AppArmor profile entirely: unloads it from the kernel, deletes the profile, the local/ override and any disable symlink, then confirms aa-status no longer lists FPM. Leaves the ini drop-in (that's cleanup's job) and needs no patch. Refuses to touch a profile this script didn't create

Add --php 8.2 to any command to act on a single version.

PHP2Shell environment check

Before changing anything, the script checks your server in this order and prints what it found:

  1. Operating system. Debian, Ubuntu and Trisquel are supported, and so is any derivative. The check recognizes Linux Mint, Pop!_OS, elementary OS, Zorin OS, KDE neon, Kali, Raspberry Pi OS, PureOS, Parrot, deepin, TUXEDO OS, Devuan, MX Linux, antiX, SparkyLinux, Q4OS, Peppermint, BunsenLabs and siduction by name. It also accepts anything else whose ID_LIKE names Debian or Ubuntu, or that has /etc/debian_version. A derivative never stops the run. The check also reports the base release and the PHP version it ships.
  2. PHP. Every PHP-FPM under /usr/sbin, its package version, whether it came from your distro or the ondrej/sury repository, and whether it's patched (see Patch detection). It flags PHP it can't protect: versions outside SUPPORTED_VERS, PHP-FPM built outside dpkg, LiteSpeed's lsphp, and Ubuntu 14.04's php5 packages (reported against the ESM Legacy fix). Where no package fix is coming, it points to a third-party extension. See Know before you run it.
  3. Web servers. Apache and nginx (plus angie, lighttpd and caddy), whether each is running, and which PHP-FPM version each one actually sends traffic to.
  4. System. Root, the init system, containers, AppArmor in the kernel, SELinux, and required tools. Without systemd (sysvinit or OpenRC, as on Devuan, MX and antiX), the script uses service and the PHP-FPM init scripts. It reads AppArmor events from audit.log, kern.log and dmesg.

Problems fall into two groups. Hard blockers always stop the run: a distribution that isn't Debian-based, no way to restart FPM (no systemd and no init script), no AppArmor in the kernel for apply, enforce or complain, not running as root, or no supported PHP-FPM. Soft blockers stop the run until you've looked at them, and --ignore-env lets you proceed:

  • Apache running an unpatched mod_php. PHP inside Apache is not covered by this script, so you'd get a clean summary while Apache stays exposed. The report prints the upgrade and a one-line drop-in. Patched mod_php is only reported.
  • Your running web server only uses PHP-FPM versions this run won't touch, for example nginx serving everything from PHP 8.4.

--ignore-env doesn't affect --force, and --force doesn't skip the environment check.

PHP2Shell patch detection

For each PHP version, the report, status and cleanup say whether the installed package is fixed. Three checks run in order, and any one is enough:

  1. Upstream version. The package is at or above the first fixed release: 8.2.35, 8.3.36, 8.4.27 or 8.5.12. This covers Debian, which ships the new upstream release (for example 8.4.27-1~deb13u1 in DSA-6574-1), and the sury repository.
  2. Known distro backport. Ubuntu backports fixes without changing the upstream version, so 8.3.6-0ubuntu0.24.04.13 is fixed even though it says 8.3.6. The script carries the fixed builds from USN-8948 for 16.04 through 26.10 and compares the full version. On 14.04, whose php5 packages Ubuntu also fixed (5.5.9+dfsg-1ubuntu4.29+esm18, ESM Legacy), the environment check reports whether php5-fpm, libapache2-mod-php5 and php5-cgi carry the fix, but doesn't mitigate them. Add builds as they're published with FIXED_BUILDS.
  3. Changelog. The package changelog names both CVEs. This catches backports the script doesn't know about. The changelog only counts if its newest entry is the installed version.

If none of those finds a fix, a build below the first fixed release counts as not patched unless it's an Ubuntu build. sury and Debian ship the new upstream release rather than backporting. Their older upstream versions are therefore unfixed even when there is no changelog to read, as on Docker images.

The result is patched, not patched, or unknown. Unknown usually means an Ubuntu build on a minimized image that dropped /usr/share/doc, with no entry in the table. Package dates aren't used: distributions prepare embargoed fixes before the public release, and a later unrelated rebuild doesn't contain the fix.

A patched package isn't enough on its own, because a running FPM keeps executing the old binary until it restarts. The script checks for FPM processes still running a binary that dpkg has replaced, and for Apache holding an old libphp*.so, and tells you which service to restart.

PHP2Shell hunting for exploitation

These vulnerabilities affect PHP below the application layer, so application logs may contain little evidence. Possible indicators include memory-corruption crashes and unexpected commands executed by PHP worker processes. hunt reads your logs (never changes anything) and reports both:

sudo ./php2shell-mitigate.sh hunt

It looks for crashed PHP-FPM and Apache workers (segfault, abort, bus error), allocator panics, and crash backtraces that name the Zend allocator (attempting double-free, heap-use-after-free, the __zend_free and _efree frames in zend_alloc.c). It also looks for crashes in PHP's built-in CLI server, bursts of memory_limit fatals (php2shell's out-of-memory trigger), the PHP_SESSION_UPLOAD_PROGRESS field, PHP processes that ran a shell or download tool, and, if a profile from this script is loaded, any exec attempt it logged. A double free detected message can also come from a separate unpatched PHP bug. Where log lines are dated it prints a rough before/after split around the disclosure date, so a spike stands out.

It also covers the serialized-object variant of the upload-progress attack seen on 2026-10-21, where a PHP gadget chain is sent in the upload-progress field, lands in the session file, and fires on the next page load. The object travels in the POST body and sits in the session file, and a log tailer can see neither. So hunt and php2shell-watch.sh catch this one only when it surfaces an error. That can be the p2s:s campaign marker reaching a log (the broken probe command echoes it under bash), a shell spawned by a worker that then fails, or the object failing to deserialize (unserialize() errors, incomplete-class and session-decode failures). A silent, successful chain leaves nothing in the logs. The payload in the session file is the ground truth, so check session.save_path directly if you suspect this.

A separate crash signature turned up on 2026-10-22: the same allocator double-free (free -> __zend_free -> _efree in zend_alloc.c), reported by AddressSanitizer and reproduced through PHP's built-in CLI server (php -S) with a crafted POST body. Two things make it easy to miss. First, a sanitizer or debug build phrases the double-free differently from the double free detected line a release build prints, so the older allocator pattern didn't catch it. hunt now matches the ASan verdict lines and the Zend allocator frames as well. Second, the CLI server writes its crash backtrace to the process's stderr rather than a logfile, so neither hunt nor the watcher sees it unless you redirect php -S (or FPM) stderr into one of the scanned files. Most important, it reproduced on 8.5.12, the release this script otherwise treats as fixed. It is unconfirmed on a stock release build and may be a distinct bug, so the script's patched baseline is unchanged. If you want your hosts to stop reading 8.5.12 as safe, set PATCHED_8_5=8.5.13 (or the real fixed release once one ships) and the mitigations stay applied. The AppArmor layer confines PHP-FPM only. It does not wrap the php CLI binary, so on the dev server the only real defenses are patching PHP and not exposing php -S to untrusted traffic.

That crash can also come from a PHP bug that has nothing to do with php2shell. php-src issue 24488 describes a double free in the garbage collector involving SimpleXML objects. A third party's release notes put it on 8.4.27 and 8.5.12, the two releases that carry the 21 October fixes, call it introduced there, and say PHP has approved a fix. It is not established that it is the same defect as the double free above. So a double-free crash on 8.4.27 or 8.5.12 deserves a look but is not by itself evidence of exploitation, and it is not a reason to go back to an unfixed PHP. If a hunt hit names SimpleXML, start with that issue, and check it for its current status. The same notes mention a use-after-free in unserialize() when a destructor calls unserialize() or session_decode() (php-src issue 24481) on current releases; its status is not established.

A match tells you exactly where to look. Buggy extensions crash workers too, and the upload-progress field appears in legitimate forms. Compare the counts against a quiet period, correlate crashes with memory-limit fatals and multipart POSTs from the same client, and check the web root for new files. Applying this script's AppArmor profile in complain mode first makes hunt more useful, because it turns any FPM attempt to run a binary into a logged signal.

hunt also checks the indicators from a 24 October 2026 honeypot report by the phpot project (the phpot report) that tied 35 Apache-parent /bin/sh starts to complete HTTP requests on single-worker Apache mod_php running PHP 8.4.26, which is below the 8.4.27 fix. Read them as leads ordered by strength. The strongest are process-level: a shell or fetch tool running now under a web or PHP worker (with its TCP peers), repeated pre-script out-of-memory errors from one client, and proof-style files (id output) in temp directories and web roots. Weaker signals are the request fingerprint, traffic from known source addresses, and leak-sized responses. The fingerprint is ?q0=v&q1=v&q2=v... in its short form or ?q0000000=v&q0000001=v... in its long form, and both are matched. The addresses are the two the phpot report confirmed (HUNT_IPS) and two that sent exploit-shaped requests with no shell seen (HUNT_ATTEMPT_IPS, attempt indicators only). A leak-sized response is a PHPSESSID value thousands of characters long (the disclosure stage returned one of 819,200 bytes decoded, about 2 MB on the wire) or a POST whose logged response is half a megabyte or more. The size lead works only in a log that counts header bytes (Apache %O, which is the Debian and Ubuntu default, or nginx $bytes_sent), and hunt never prints a cookie value, because it is leaked process memory. On nginx, whose default access log cannot show response header size, the matching lead is the upstream sent too big header error in its error log. If the auditd rule from php/detect/ is loaded, hunt also reads the program starts it recorded, the one way to see a shell that started and exited between process samples.

None of these is proof on its own. The phpot report's final request could fail to reach the access log because the worker died, so a missing line is not evidence that nothing ran, and an HTTP 200 on one stage is not evidence the attempt failed.

Watching live

hunt is a one-shot scan. To watch continuously, the standalone php/php2shell-watch.sh follows the same logs with tail -F and prints one line per hit, so a sysadmin can leave it running and watch the output:

sudo nohup ./php2shell-watch.sh >/var/log/php2shell-watch.log 2>&1 &
tail -f /var/log/php2shell-watch.log

It survives log rotation, prints a count summary on stop, and changes nothing. A systemd unit is included for running it as a managed service (journalctl -fu php2shell-watch). The same caveat applies: a hit is a lead to investigate.

Since 2026-10-23 it also carries two signals ReliaQuest reported from lab reproduction of CVE-2026-109618. First, a memory error that ends with in Unknown on line 0. During exploitation PHP reports the failure before an application file is in scope, so the error names no file. That is a stronger tell than an ordinary memory-limit fatal, and because the failed allocation can be small, the signal does not depend on its size. Second, a correlation: a POST that returns HTTP 500 followed within a few seconds by the worker that handled it crashing, tied together by the worker PID appearing in both logs. The watch arms a PID when an access-log line shows a POST with a 500 status, and raises an ALERT if a same-PID crash (zend_mm_heap corrupted, AH00052, an abort/segfault signal, or in Unknown on line 0) lands within CORRELATE_WINDOW seconds (default 10). This needs an access log that records the PID among the watched files. For Apache that means %P in the LogFormat. With no access log present the correlation stays off and the per-line signatures still work. The default log set now includes the common access-log paths for this reason.

The watcher also runs a process watch: it samples /proc once a second and reports a new shell or tool (sh, curl, wget, PHP CLI and similar) whose ancestry includes a web server or PHP worker, which was the phpot report's strongest signal. It is a poller, so it can miss a process that lives less than the interval, and a shell whose worker has already died is re-parented and no longer links to it (the phpot report's worker crashed about 0.7 seconds after starting the shell). The auditd rule in php/detect/ has no such gap. Set PROCS=0 to turn the watch off, or PROC_ALLOW to ignore an application that legitimately shells out. A flagged process shows its TCP peers, for example [tcp: SYN-SENT->203.0.113.9:8080]. Two more signals work from logs and need no access-log line: a pre-script out-of-memory error followed by a crash of the same worker (the shape of the confirmed chain), and repeated pre-script out-of-memory errors from one client. It also matches the short numbered request fingerprint, raises a LEAD for a POST with a very large response or a PHPSESSID value thousands of characters long, and flags the edge cookie guard's log line and nginx's upstream sent too big header error.

Two of those alerts, the per-client burst and the same-worker chain, need the error-log line to carry the worker PID and the client address. Apache's own error log does. PHP's own error_log file format ([23-Oct-2026 09:08:35 UTC] PHP Fatal error: ...) carries neither, so those two alerts cannot fire from it, although the single-line signature still does. Under mod_php, leave error_log unset so PHP errors go to Apache's error log. The watcher prints a one-time NOTE when it sees an out-of-memory line it cannot attribute, and hunt says how many such lines it could not count.

PHP2Shell configuration

Variable Default Purpose
WEB_ROOTS /var/www /srv/www Read-only document roots
WRITABLE_PATHS (empty) Paths your app writes to, such as uploads
PATCHED_X_Y (for example PATCHED_8_4) 8.2.35, 8.3.36, 8.4.27, 8.5.12; empty for 7.0-8.1 First fixed upstream release for each branch
PHP_VER (empty) The same as --php X.Y. An exported PHP_VER in your environment therefore limits the run to that version
GUARD_HINT 1 Set to 0 to stop the environment check pointing to the third-party extension for PHP this script can't cover
PATCHED_115560_X_Y (empty) First release fixing CVE-2026-115560 for branch X.Y. Until set, only a changelog naming the CVE counts
SUPPORTED_VERS 7.0 7.1 7.2 7.3 7.4 8.0 8.1 8.2 8.3 8.4 8.5 Versions the script will touch
FIXED_BUILDS (empty) Extra fixed distro builds, space-separated full versions, for example a Debian LTS 7.4 update
HUNT_IPS 15.204.59.70 162.35.99.94 Source addresses reported in confirmed exploitation. An aging indicator: a hit means the address touched you, not what it did. Empty turns it off
HUNT_ATTEMPT_IPS 104.28.208.84 144.48.4.242 Addresses that sent exploit-shaped requests with no shell seen: attempt indicators, not confirmed RCE, and shared addresses are possible. Empty turns it off
PROOF_SINCE 2026-10-01 hunt's proof-file scan ignores older files
HUNT_PROC_ALLOW rotatelogs|cronolog|logger Regex on the command line: hunt's live process check ignores matches
COOKIE_MAX 4096 Flag a PHPSESSID value this many characters long, in a log that records response cookies. 0 turns it off. Also read by php2shell-watch.sh
RESP_BYTES_MAX 500000 Flag a POST whose logged response is this many bytes. Needs a log that counts header bytes. 0 turns it off. Also read by php2shell-watch.sh

php2shell-watch.sh reads its own variables, each documented at the top of the script: LOGS, WATCH_IPS, PROCS, PROC_INTERVAL, PROC_CMD_MAX, PROC_ALLOW, CORRELATE_WINDOW, CHAIN_WINDOW, OOM_CLIENT_THRESHOLD, OOM_CLIENT_WINDOW, COOKIE_MAX and RESP_BYTES_MAX.

When no upstream fixed version is set for a branch, the script checks your distro's changelog for both CVEs instead. That catches backported fixes from Ubuntu, Debian, and the ondrej/sury PPAs, which often keep the old version number.

PHP2Shell on cPanel (EasyApache 4)

php2shell-mitigate.sh assumes Debian-family packages and does not touch a cPanel server's EasyApache 4 PHP (/opt/cpanel/ea-php74 to ea-php85). php2shell-cpanel-mitigate.sh covers it, under any handler (mod_php, PHP-FPM, CGI or LSAPI). It writes one drop-in per PHP version, named so that it loads after every other file in that version's php.d directory and therefore wins over php.ini and the MultiPHP INI Editor. It never edits php.ini.

  1. Download it. The repository is mirrored on GitHub and Codeberg; pick a base URL.

    # BASE from GitHub:   https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main
    # BASE from Codeberg: https://codeberg.org/PrivacySafe/security-patches/raw/branch/main
    # curl -fLO "$BASE/php/php2shell-cpanel-mitigate.sh"
    curl -fLO https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main/php/php2shell-cpanel-mitigate.sh
    
  2. Read it before running it. Always.

    less php2shell-cpanel-mitigate.sh
    
  3. Make it executable.

    chmod +x php2shell-cpanel-mitigate.sh
    
  4. See what you have. This changes nothing.

    sudo ./php2shell-cpanel-mitigate.sh check
    
  5. If a version is unpatched and you cannot update the ea-php packages today, apply the drop-in. This restarts Apache and any running ea-php FPM pools. Add --no-restart to do that yourself at a time you choose.

    sudo ./php2shell-cpanel-mitigate.sh apply
    
Command What it does
check (default) Read-only report, and the file apply would write for each version
apply Writes the drop-in for every version, confirms PHP reports the values, and restarts Apache and FPM
status What is in place now, and whether any web server or FPM process still runs with the old settings
restore Removes the drop-ins this script wrote
cleanup Once a version is patched, reduces its drop-in to the single line CVE-2026-115560 still needs (--force does this for an unpatched version too)

The options are --post-max SIZE, --upload-max SIZE, --memory-unlimited, --no-restart, --no-scan and --force; --help describes each.

What the drop-in sets:

Setting When Why
session.upload_progress.enabled = 0 Every version, patched or not CVE-2026-115560 is not fixed by the 21 October releases
fatal_error_backtraces = 0 PHP 8.5, unpatched builds only Removes the fatal-error-backtrace variant
memory_limit = -1 Only with --memory-unlimited, unpatched builds only PHP's own protective measure; it trades the remote code execution for denial of service, so pair it with a request body cap (see php/edge/)
post_max_size, upload_max_filesize Only with --post-max (and --upload-max) Capped to the smaller of the current value and your ceiling; never raised. A chunked request can pass post_max_size, so this narrows the exposure and does not close it

Besides the version table, check reports:

  • the PHP handler of each version, read from WHM when whmapi1 is present (a version it cannot place shows as unknown);
  • whether each version is patched, by version number or because its package changelog names both CVEs. An end-of-life branch counts as unpatched unless a changelog names both;
  • settings elsewhere that can undo the drop-in: FPM pool entries, and .htaccess php_value and php_flag lines and .user.ini files under the home directories (a php.ini in a home directory loads before the drop-in, so the drop-in wins over it);
  • Apache RLimitMEM and RLimitCPU. They limit processes Apache itself forks (CGI, suEXEC, suPHP), and PHP's advisory warns against an address-space limit as a memory cap, because a failed allocation sends PHP down the vulnerable path. WHM regenerates httpd.conf, so change them in WHM, not by hand.

check exits 0 when every version is patched and nothing needs review, 1 when an unpatched version is present, and 2 when everything is patched but something above is worth a look.

Limits:

  • check prints the exact plan before apply makes any change, and restore reverts a drop-in it wrote.
  • The drop-in loses to settings applied later: .htaccess and .user.ini values, and FPM pool values. check lists them. The script does not change them.
  • apply restarts Apache through cPanel's restartsrv_httpd, which interrupts connections briefly. LiteSpeed's own workers are not restarted: restart LiteSpeed yourself.
  • The Apache edge guard can be added through WHM's Apache include editor, so that cPanel keeps it when it regenerates httpd.conf.
  • A release candidate, beta or dev build is never treated as the fixed release. A build reporting 8.4.27RC1, 8.4.27beta2 or 8.4.27-dev comes before 8.4.27, so it reads as unpatched. Only a build above the fixed number, or one whose package changelog names both CVEs, reads as patched.

Edge guards and detection content

Two optional folders sit beside the scripts. Both are compensating controls, not fixes. php/edge/ has Apache (server configuration or .htaccess) and nginx guards, and a curl-based tester for them (php2shell-edge-test.sh). The guards reject a multipart POST to a path that takes no uploads (log-only until you enforce) and, on Apache, replace an oversized Set-Cookie, plus guidance on a request body size limit, measured against the phpot report's stage sizes. They were verified with real PHP handlers (Apache 2.4.58 with mod_php, nginx 1.24.0 with PHP-FPM). They matter most for Apache mod_php, which the AppArmor layer does not confine. php/detect/ has an auditd rule that records every program the web-server account starts, a Tetragon filter for the same signal, a log-only outbound-connection rule, and a table of how strong each signal is. The auditd rule and the nftables rule load on a live kernel.

How exploitable is it, and since when

PHP's advisory says complete remote code execution for CVE-2026-109618 was validated over HTTP against Apache mod_php with default PHP input and memory limits, and that PHP validated remote code execution against PHP-FPM for CVE-2026-109619 (as quoted by HeroDevs and Rescana). Wiz demonstrated chains against stock WordPress, Drupal and Joomla images (as summarized by Corgea), and the phpot honeypot report recorded 35 Apache-parent shell starts on single-worker mod_php running PHP 8.4.26. The phpot report found the observed chain consistent with CVE-2026-109618 and did not establish a CVE-2026-109619 path. Early claims that the public proofs of concept were fake or hard to use did not hold up.

Exploitation may have begun before the release. Amazon Threat Intelligence reported a PHP2Shell exploitation attempt in its MadPot honeypot telemetry dating to 17 October 2026, four days before the 21 October fix (AWS Security Blog). Treat 17 October as the start of exploitation and start log hunting there, not on 21 October. hunt's dated before and after split uses 17 October as its baseline (set HUNT_SINCE to change it), so a count from before that date is your quiet baseline.

PHP protective measures and what they cost

PHP's advisories name protective measures beyond the ones php2shell-mitigate.sh applies. PHP says they "reduce exposure to specific attack paths but do not make an installation safe", and its releases state that updating is the only complete fix. The table lists each measure, what it addresses, what it costs, and whether this script applies it. The details come from PHP's advisories (CVE-2026-109618 and CVE-2026-109619) as summarized by HeroDevs and Corgea. Check the advisories for their exact wording before relying on a setting.

Measure What it addresses Cost or risk Applied by this script?
memory_limit = -1, set administratively (php_admin_value or the main php.ini, not .user.ini), every worker restarted, and confirmed that no pool, directory or application setting restores a finite limit Removes the out-of-memory condition CVE-2026-109618 depends on Trades the RCE for denial of service: one request can consume all memory. Pair it with a body cap No. A host-wide memory decision is yours
session.upload_progress.enabled = 0 The upload-progress producer and consumer paths, and the cookie-disclosure path Breaks upload progress bars Yes (the drop-in)
fatal_error_backtraces = 0 (PHP 8.5) The fatal-error-backtrace variant Loses backtraces in fatal errors Yes, on 8.5
A conservative post_max_size, plus a cap on the decoded body at the web server A multipart request with no usable Content-Length can pass post_max_size Refuses large uploads Guidance in php/edge/
A lower max_input_nesting_level; bounds on max_input_vars, multipart parts and headers per part Limits the structures that consume memory cheaply. max_file_uploads does not count empty file controls May break unusual forms No
Block multipart/form-data where the application takes none Removes the request shape the out-of-memory producers use Breaks uploads The multipart guard in php/edge/, log-only first
Remove per-request INI overrides that emit diagnostics (.htaccess php_value, PHP_VALUE) A consumer of stale error state before request initialization Per-application changes No
pm.max_requests = 1 on FPM pools that can absorb it Limits stale state surviving across requests A large performance cost No

Do not use RLIMIT_AS, RLIMIT_DATA, ulimit -v or systemd LimitAS as a stand-in for memory_limit. Per the same summaries they make mmap() fail, which sends PHP back down the vulnerable longjmp() path. This project sets no such limit. The advisories do not address cgroup memory limits, so test one before relying on it.

Which server API is exposed: for CVE-2026-109618, PHP says complete remote code execution was validated over HTTP against Apache mod_php, and that source analysis supports PHP-FPM as susceptible, with no complete FPM exploit at publication. For CVE-2026-109619, PHP says it validated remote code execution against PHP-FPM as well (as quoted by Rescana). Treat FPM as exposed, and mod_php as the higher priority.

Before you run it

  • This is not a patch. PHP released fixes on 21 October 2026 in 8.5.12, 8.4.27, 8.3.36 and 8.2.35, and distributions are shipping backports under their own version numbers. Upgrade, then run cleanup.
  • The profile blocks mail() , because PHP runs sendmail through /bin/sh. It also blocks ImageMagick delegates. If your app sends mail, use SMTP from PHP.
  • PHP 7.0 through 8.1 are past upstream end of life. PHP fixed only 8.2 through 8.5. For older branches a fix can only come as a distro or PPA backport, such as Debian LTS, Ubuntu ESM or the sury PPA. Without one, this mitigation is permanent, so plan your upgrade.
  • PHP 5.x is reported affected but not covered. The environment check flags it, and on Ubuntu 14.04 tells you whether the ESM Legacy fix is installed.
  • Checking by hand, check the package your server actually runs. apt list '?installed ?name(^php[0-9.]+$)' lists the php8.3-style metapackages, which a server with only php8.3-fpm or libapache2-mod-php8.3 may not have installed. Query those packages directly: dpkg-query -W 'php*-fpm' 'libapache2-mod-php*'.
  • Debian, Ubuntu, Trisquel and their derivatives only , with systemd, sysvinit or OpenRC. RHEL, Remi, and other SELinux-based layouts need a different approach. The environment check refuses to run on them.
  • PHP-FPM only. Apache mod_php and LiteSpeed lsphp are reported but not mitigated. mod_php is a demonstrated exploitation surface for this bug, and the AppArmor profile cannot reach PHP running inside Apache, so on a mod_php host the fix is to upgrade libapache2-mod-php<version> and restart Apache, not this profile. On a cPanel server with EasyApache 4 PHP, use the cPanel script.
  • For PHP nothing here covers and no package will fix (an end-of-life branch with no distro backport, PHP 5, unpatched mod_php, PHP built outside dpkg), the environment check points to php_uploadprogress_guard, a third-party PHP extension (5.4 to 8.5, any SAPI) that removes the session upload-progress handler and carries a fixed multipart parser. It's not part of this project and we haven't reviewed it. It runs inside every PHP process, so read the source, build it yourself rather than installing the prebuilt binaries, and test on staging. Upgrading remains the fix. GUARD_HINT=0 turns the pointer off.
  • Every server API that serves web requests is in scope , not only PHP-FPM and mod_php: PHP's advisory names php-cgi, FrankenPHP and LiteSpeed as well. What this project covers is limited to what its scripts and guards say they cover.
  • PHP inside other products (control panels, network and security appliances, management consoles, container images) can bundle its own copy. Updating the system PHP package does not touch it, and php2shell-mitigate.sh only reads packages. The fix there is the vendor's update or a rebuilt image, so check the vendor's advisory, and look for PHP binaries outside the package manager.
  • Rely on the crash and error-log signatures hunt looks for, and on patching. The demonstrated exploits run against stock production settings with display_errors off, so a successful attempt need not surface as a visible PHP error in a response or an application log.
  • Backups of everything the script touches go to /var/backups/php2shell/<timestamp>-<pid>/<version>/. The process id keeps two runs in the same second from sharing a directory and overwriting the first backup. remove keeps the profile, your local override and any disable link under separate names (usr.sbin.php-fpmX.Y, local.usr.sbin.php-fpmX.Y, disable.usr.sbin.php-fpmX.Y), so the site-specific rules you added to the local override survive the removal.
  • If the AppArmor profile fails to parse during apply, no profile is installed or changed, but the session.upload_progress drop-in written in step 1 stays in place: it only turns a feature off, so it is left on purpose.

Why cleanup leaves the AppArmor profile loaded

That's deliberate. The patch fixes these bugs. The profile confines PHP-FPM against the next one, which no patch does. After cleanup you choose: enforce to start blocking (review status first), or remove to unload the profile and delete its files. Until you pick one, it stays in complain mode and only logs.

php -v and backported fixes

php -v reports PHP's upstream version, never the distro build, so Ubuntu's 8.3.6-0ubuntu0.24.04.13 shows as plain 8.3.6. The script reads the package build with dpkg and prints it in every patch verdict. To check by hand: dpkg-query -W -f='${Version}\n' php8.3-fpm (or libapache2-mod-php8.3 for mod_php).

PHP Session Audit (CVE-2026-115560)

Script: php/php-session-audit.sh

CVE-2026-115560 needs three things at once: session.serialize_handler set to php_binary or php_serialize in config, application code switching it to php at runtime before session_start(), and session upload progress left on. Turning upload progress off closes it, and php2shell-mitigate.sh does that.

Status as of PHP's 23 October advisory, as reported by Cybernews: the PHP Foundation considers the setup unlikely in practice, so it is handling this CVE in the regular release cycle and will publish a full advisory with the next release. It has not yet named the affected versions or given a severity score, and it has reported no exploitation of this CVE. Its advice is to keep session.upload_progress.enabled = 0 until that release, and an application that already has it off needs nothing more for this issue. This is why PATCHED_115560_X_Y has no default: set it yourself once PHP names the fixed release for your branch. Check php.net for the current position.

This read-only script tells you whether the other two conditions exist, so you know whether you were exposed:

  • the configured handler and upload-progress setting for every PHP-FPM version and pool, and for mod_php;
  • per-directory overrides in Apache config, .htaccess and .user.ini;
  • code under WEB_ROOTS that switches the serializer through ini_set() or session_start([...]);
  • a session.save_path shared by pools with different serializers.
WEB_ROOTS="/var/www /srv" ./php-session-audit.sh

Exits 0 if nothing matched, 1 if all three conditions are present, 2 for a partial match worth reviewing. The code scan is a grep: it finds literal switches, and flags non-literal ones for you to check.

Red Hat advisories

Red Hat's advisory list (security advisories released 2026-04-01 to 2026-11-03) names the CVEs below. A Red Hat build is judged by its RPM release, not by the upstream version number, because Red Hat backports fixes: the advisory names the release that fixes it. A CVE with no advisory in the list has none for that period, which is not the same as not affected. The current state is on Red Hat's CVE database.

Advisory Released Severity Package and stream CVEs on this page
RHSA-2026:87070 2026-10-21 Important php-8.3.36-1.el10_2 (RHEL 10.2) CVE-2026-109618, CVE-2026-109619
RHSA-2026:87071 2026-10-21 Important php8.4-8.4.27-1.el10_2 (RHEL 10.2) CVE-2026-109618, CVE-2026-109619
RHSA-2026:87072 2026-10-21 Important php-8.2.35-1.module+el9.8.0+25111+a1450321 (RHEL 9.8); php-pecl-apcu-5.1.23-1.module+el9.7.0+24111+199d2cc4 (RHEL 9.7); php-pecl-rrd-2.0.3-4.module+el9.7.0+24111+199d2cc4 (RHEL 9.7); and 2 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:87073 2026-10-21 Important libzip-1.7.3-1.module+el8.10.0+20770+a5eca186 (RHEL 8.10); php-8.2.35-1.module+el8.10.0+25110+f67151d2 (RHEL 8.10); php-pear-1.10.14-1.module+el8.10.0+20770+a5eca186 (RHEL 8.10); and 4 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:87079 2026-10-21 Important php-8.3.36-1.module+el9.8.0+25109+6886e21a (RHEL 9.8); php-pecl-apcu-5.1.23-1.module+el9.8.0+24343+756b4fcd (RHEL 9.8); php-pecl-redis6-6.1.0-3.module+el9.8.0+24343+756b4fcd (RHEL 9.8); and 3 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:87080 2026-10-21 Important libzip-1.6.1-1.module+el8.10.0+22485+a3539972 (RHEL 8.10); php-7.4.33-8.module+el8.10.0+25117+30337cca (RHEL 8.10); php-pear-1.10.13-1.module+el8.10.0+22485+a3539972 (RHEL 8.10); and 4 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:87151 2026-10-21 Important php-8.0.30-8.el9_8.1 (RHEL 9.8) CVE-2026-109618, CVE-2026-109619
RHSA-2026:87414 2026-10-21 Important php-8.5.12-1.hum1 (Hummingbird) CVE-2026-109618, CVE-2026-109619
RHSA-2026:88532 2026-10-23 Important libzip-1.6.1-1.module+el8.3.0+6678+b09f589e (RHEL 8.3); php-7.4.33-1.module+el8.8.0+25148+55cf9df9.2 (RHEL 8.8); php-pear-1.10.13-1.module+el8.7.0+15127+a450a8db (RHEL 8.7); and 4 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:88533 2026-10-23 Important php-pear-1.10.13-1.module+el8.6.0+16577+0788886f (RHEL 8.6); php-pecl-apcu-5.1.18-1.module+el8.3.0+6678+b09f589e (RHEL 8.3); php-pecl-rrd-2.0.1-1.module+el8.3.0+6678+b09f589e (RHEL 8.3); and 4 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:88561 2026-10-23 Important php-8.3.19-1.el10_0.2 (RHEL 10.0) CVE-2026-109618, CVE-2026-109619
RHSA-2026:88565 2026-10-23 Important libzip-1.6.1-1.module+el8.3.0+6678+b09f589e (RHEL 8.3); php-7.4.6-5.module+el8.4.0+25156+e0c98721.3 (RHEL 8.4); php-pear-1.10.13-1.module+el8.4.0+16578+ed65e99e (RHEL 8.4); and 4 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:88659 2026-10-23 Important php-8.0.30-1.el9_2.2 (RHEL 9.2) CVE-2026-109618, CVE-2026-109619
RHSA-2026:88693 2026-10-23 Important php-5.4.16-48.el7_9.1 (RHEL 7.9) CVE-2026-109619
RHSA-2026:88720 2026-10-23 Important php-8.0.30-3.el9_6.2 (RHEL 9.6) CVE-2026-109618, CVE-2026-109619
RHSA-2026:88725 2026-10-23 Important php-8.0.30-1.el9_4.2 (RHEL 9.4) CVE-2026-109618, CVE-2026-109619
RHSA-2026:88834 2026-10-23 Important php-8.2.28-1.module+el9.6.0+25176+d8415d54.4 (RHEL 9.6); php-pecl-apcu-5.1.23-1.module+el9.4.0+20748+b46899d2 (RHEL 9.4); php-pecl-rrd-2.0.3-4.module+el9.4.0+20748+b46899d2 (RHEL 9.4); and 2 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:88835 2026-10-23 Important php-8.2.13-1.module+el9.4.0+25177+88c847cf.4 (RHEL 9.4); php-pecl-apcu-5.1.23-1.module+el9.4.0+20748+b46899d2 (RHEL 9.4); php-pecl-rrd-2.0.3-4.module+el9.4.0+20748+b46899d2 (RHEL 9.4); and 2 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:89111 2026-10-26 Important php-8.3.19-1.module+el9.6.0+25160+f8b750a4.3 (RHEL 9.6); php-pecl-apcu-5.1.23-1.module+el9.6.0+22647+1741ae35 (RHEL 9.6); php-pecl-redis6-6.1.0-2.module+el9.6.0+22647+1741ae35 (RHEL 9.6); and 3 more CVE-2026-109618, CVE-2026-109619
RHSA-2026:90321 2026-10-26 Critical devspaces/code-rhel9 (container image); devspaces/code-sshd-rhel9 (container image); devspaces/configbump-rhel9 (container image); and 13 more CVE-2026-109618, CVE-2026-109619

The Security Patches software and this documentation are dedicated to the public domain under CC0 1.0 Universal. Other pages on privacysafe.app/docs are CC BY-SA 4.0 unless the page says otherwise.

Edit this page on Codeberg

The PrivacySafe Ecosystem

Shop at Ivy Cyber

Ivy Cyber offers PrivacySafe products and services in the USA and has partnerships with 3NSoft in Canada and Kuip Limited in the European Union. Enterprise services and consulting are available at ivycyber.com

PrivacySafe Foundation is a 501(c)(3) nonprofit public charity that supports the development of the PrivacySafe applications, along with the components they rely upon: 3NWeb protocols, filesystem, documentation, and reference implementations.

PrivacySafe Foundation

Donations to PrivacySafe Foundation support education, research, documentation, and public code. Purchases from Ivy Cyber support PrivacySafe products, services, hosting, and technical support. Together, we're building a better web.