Book a call
← All posts

PHP2Shell: The Calm Runbook

Title card reading PHP2Shell: the calm runbook

On October 21 the PHP project shipped emergency releases for two bugs in the PHP runtime itself, CVE-2026-109618 and CVE-2026-109619, now going by the name PHP2Shell. Both allow an unauthenticated attacker to run code on the server with the web worker's privileges. No application bug is needed; a one-line PHP file is enough. Both are rated 9.2 on CVSS v4.

If you run anything on PHP that faces the internet, this is the week's job. The fixed versions are 8.5.12, 8.4.27, 8.3.36 and 8.2.35. PHP 8.1 and older get no official release, only source backports and whatever your distribution or host ships.

It is being exploited. Amazon's honeypots saw the first attempt on October 17, four days before anyone outside the coordination group was supposed to know. Cloudflare's firewall went from thousands of attempts to millions within hours of the patch going public, with attackers tuning payloads against its rules in real time.

If you have one hour today: find every server that runs PHP for a browser, patch it to a fixed version, restart every PHP worker, and then read the section on hunting, because the patch does not undo what happened before it.

If you have ten minutes: turn off session.upload_progress in php.ini, restart PHP, and book the hour for tomorrow morning. That setting closes one of the two known doors. It is not a fix.

What the two bugs actually are

Most PHP vulnerabilities you have read about live in an application: a plugin that trusts a query string, a form that writes to the wrong table. These two live under the application, in the part of PHP that sets up and tears down every request. That is why the usual question ("do we use that feature?") does not help. You use request handling by definition.

CVE-2026-109618 is about running out of memory at the wrong moment. PHP allocates memory while it is still reading the incoming request. If an allocation fails partway through, request processing stops abruptly and leaves half-built data structures behind. The cleanup code that runs at the end of the request treats those structures as valid. A large or oddly shaped request can make the allocation fail on purpose. Rapid7 confirmed remote code execution through this path on Apache mod_php with default memory and input limits, including the normal multi-worker prefork setup. On PHP-FPM, a worker that fails during startup is killed rather than reused, which blocks this particular sequence; nobody has published a complete FPM exploit for it yet.

CVE-2026-109619 is about memory that outlives the request. Some internal state is kept between requests on the same worker. One request frees a piece of memory; the state still points at it; the next request on that worker reads or writes through the dangling pointer. The confirmed paths are the session upload-progress feature (which is why session.upload_progress.enabled=0 matters) and, on PHP 8.5, the new fatal-error backtrace feature, which is on by default. The session path works on both mod_php and FPM; on FPM it needs two requests to land on the same worker and some application conditions, which is harder to arrange but not hard enough to relax.

Why mod_php is the worst seat. With mod_php, PHP runs inside the Apache process and starts handling the request before any of your code runs. The exploit does not need a login page, a plugin, or a known URL; any request that reaches PHP will do. FPM separates the workers from the web server and recycles them on failure, which removes some of the paths. It does not remove the session one.

Why "we only have a Hello World page" is not a defence. The bugs trigger in request setup and teardown. Wiz's write-up makes the point directly: a PHP installation serving a page that prints "Hello World" is in scope. The content of your application is irrelevant to whether the runtime can be made to fail in the right order.

How it was found. Wiz Research found both in August, with heavy use of AI models for the discovery, exploit work and patch validation. The first was found on August 19; by August 21 they had unauthenticated RCE on stock WordPress and Drupal; the reports went to PHP on August 24; PHP acknowledged them on September 21 and shipped fixes on October 21. Every PHP release of the last decade has at least one of the affected code paths, and the older path goes back to PHP 5.4.

Are you affected?

Short answer: somewhere, yes.

The product may be Node, Python or .NET and still have a PHP surface: the marketing site on WordPress, phpMyAdmin on a database box, Matomo, a legacy admin panel someone built in 2019. That box usually shares a network, a credential or a deploy key with the product. A shell on the WordPress server is the way in.

  • EXPOSED. Product on PHP (Laravel, Symfony, custom), self-managed servers. Direct. You own the runtime and the restart. Patch today, restart all workers, hunt from Oct 17.
  • EXPOSED, NEXT DOOR. Product on anything else, WordPress or Drupal marketing site. The product is not vulnerable; the box beside it is. Patch or confirm the host did. Check what that server can reach.
  • COVERED, VERIFY. WordPress on a managed host (Kinsta, WP Engine, Pressable, Pantheon, SiteGround, Flywheel). Platform-level per each host's notice; several patched before disclosure. Read the host's notice, confirm the PHP version in Site Health, nothing else.
  • ASK YOUR HOST. WordPress or Laravel on shared cPanel hosting. cPanel shipped fixed EasyApache packages; CloudLinux alt-php lagged on many hosts. Ask in writing whether the Oct 21 release is applied. Apply the ini mitigation yourself if the panel lets you.
  • REBUILD. Docker or Kubernetes with the official php: images. Fixed tags exist (8.3.36-fpm etc). A running container does not pick them up. Rebuild with the new tag or digest, roll the deployment.
  • NO OFFICIAL FIX. PHP 8.1, 8.0 or 7.4 anywhere. Ubuntu 22.04/24.04 and Debian 12/13 patched their packages; Ubuntu 20.04 only under Ubuntu Pro; Debian 11 has no free fix. Take the distribution package if one exists, otherwise build the source backport or move the workload.
  • PATCH ANYWAY. PHP CLI only (cron jobs, queue workers, no web traffic). Not the web-facing path, but the same binary tends to serve web traffic somewhere. Patch with everything else.
  • ASK THE VENDOR. An appliance or vendor product with PHP inside (NAS admin UIs, some firewalls, helpdesk software). These are the ones that get forgotten. List them, check each vendor's advisory, isolate what cannot be patched.

Two things people get wrong. First, a Node or Python product is not affected by this bug; it is affected by what sits next to it. Second, moving a PHP application to another language is not the response to a runtime CVE. Every runtime has had one of these. The response is knowing what you run and being able to patch it in an afternoon, which is the subject of the rest of this post.

How this went for one founder this week

A worked example, with the names and details invented. The estate is the one we see most often, and the steps are the runbook we use.

Maya runs a B2B SaaS. The product is Node and Postgres on AWS, built by her own two engineers. The marketing site is WordPress, on a shared cPanel account at a regional host, built by an agency two years ago. Nobody on her team has logged into it since the agency handed over the password.

Tuesday, 4:05pm. Her lead investor sends a WhatsApp: "Are you affected by this PHP thing?" She has read the headline, not the advisory. She forwards it to us with one word: "Help?"

4:10pm, a ten-minute call. Three questions. What runs PHP anywhere in the company: the WordPress site, and she thinks that's all. Who can log into it: the agency, and a cPanel password in a shared Google Doc. Does it share anything with the product: a SendGrid API key, pasted into the contact-form plugin so the site could send mail. The product is not in scope. The site is, and so is that key.

4:25pm, the panel. cPanel, MultiPHP Manager: the domain is on PHP 8.1, handler lsphp, so LiteSpeed. The host's status page says nothing about October 21. We open a ticket asking, in writing, whether the fixed ea-php81 build is on the server. While that waits, MultiPHP INI Editor: session.upload_progress.enabled set to Off, saved. A one-line PHP file in the web root confirms it (PHP 8.1.33 litespeed upload_progress=off), then gets deleted. Twenty minutes in, the known session path is closed.

4:50pm, something in front of it. The site has no CDN. Cloudflare's free plan has the managed rules for both CVEs on by default, so the nameservers move. It takes the rest of the hour to propagate. Not a fix; a second lock on the door while the host wakes up.

5:30pm, the logs. cPanel's Raw Access Logs for the domain, downloaded, back to October 15. We're looking for multipart POSTs followed by 500s, a burst of 500s from one address, and the session upload-progress field name in anything. Result: zero 500s in the window; two multipart POSTs on the 21st from a scanner IP, both to the contact form, both answered 200; no sign of the upload-progress field. On disk, through the file manager: no PHP files modified under wp-content/uploads since July; wp-config.php untouched; one administrator account, hers; no cron jobs that weren't there at handover. We write all of it down, with timestamps, in a shared doc.

5:50pm, the key. The SendGrid key is rotated anyway. It is the one thing on that server that reaches the product, and rotating it costs fifteen minutes. The contact-form plugin gets the new one; the old one is revoked.

Wednesday, 9:40am. The host replies: the server moved to the fixed build overnight, lsphp restarted. On shared hosting you can't read the package changelog yourself, so the proof is the host's written confirmation plus our own check that the ini setting survived the restart. It did. The site goes on the list to move to a managed WordPress host next month, where this is somebody else's Tuesday.

Wednesday, 10:00am, the reply to the investor. Four lines. The product doesn't run PHP. The marketing site did, on 8.1; the known attack path was closed Tuesday at 4:30pm and the host applied the patch overnight. Logs from October 15 show no exploitation attempts; the one shared credential was rotated as a precaution. Evidence attached.

About three hours of our time across two days, most of it reading logs. The part that made the difference was not the patch. It was knowing within ten minutes what ran PHP, who could reach it, and what it could reach.

First thirty minutes: find every PHP you own

The version you see from php -v on a server is the CLI binary. It is often not the one serving web traffic, and on a box with cPanel, Docker or two PHP branches it is almost never the only one. Inventory first, then patch.

On a Linux server you can SSH into

# every php binary on the box, and what version each one is
for p in $(which -a php php-fpm php8.1 php8.2 php8.3 php8.4 2>/dev/null; ls /opt/cpanel/ea-php*/root/usr/bin/php /opt/alt/php*/usr/bin/php /usr/local/lsws/lsphp*/bin/lsphp 2>/dev/null); do echo "$p: $($p -v 2>/dev/null | head -1)"; done

# what is actually serving requests right now
ps -eo pid,user,comm,args | grep -Ei 'php-fpm|lsphp|httpd|apache2' | grep -v grep
apachectl -M 2>/dev/null | grep -i php        # mod_php loaded = highest risk
systemctl list-units --type=service | grep -i php

# the installed package and whether the fix is in it (Debian/Ubuntu keep the old version string)
apt-cache policy php8.3-fpm 2>/dev/null | head -3
zgrep -E 'CVE-2026-10961[89]' /usr/share/doc/php8.3-fpm/changelog.Debian.gz 2>/dev/null
rpm -q --changelog php-fpm 2>/dev/null | grep -E 'CVE-2026-10961[89]'

The version that matters is the one the browser hits. Drop a one-line file in the web root, request it, delete it:

<?php echo PHP_VERSION, ' ', PHP_SAPI, ' ', ini_get('session.upload_progress.enabled') ? 'upload_progress ON' : 'upload_progress off';

PHP_SAPI tells you apache2handler (mod_php), fpm-fcgi, litespeed or cgi-fcgi. On Ubuntu and Debian the version string can stay at the old number after a backported fix; the package changelog is the proof, not the number.

Containers. The version is baked into the image. docker ps --format '{{.Image}}' and kubectl get pods -A -o jsonpath='{..image}' | tr ' ' '\n' | grep -i php | sort -u list the tags in use. Anything older than 8.2.35, 8.3.36, 8.4.27 or 8.5.12, or on a floating tag like php:8.3-fpm pulled before October 21, needs a rebuild.

cPanel/WHM. WHM, then MultiPHP Manager, shows the version per domain; MultiPHP INI Editor shows the handler. The packages behind them are ea-php* and, on CloudLinux, alt-php*. The panel assigns versions; it does not update the packages.

Plesk. plesk bin php_handler --list shows every handler and its version. Plesk's own PHP builds update through the Plesk installer; OS-vendor handlers update through apt or dnf.

Managed WordPress hosts. Tools, then Site Health, then Info, then Server, in WP admin. If the host's notice says they patched platform-wide and Site Health shows a fixed version or the host's build number, you are done with that site.

The ones people forget. phpMyAdmin on a database server. Matomo. Roundcube. An old intranet. The admin interface of a NAS or a firewall. Anything a vendor shipped with PHP inside. Ask each vendor; isolate what cannot be patched.

Mitigate before you can patch

PHP's own line is that no configuration change fully mitigates these bugs. True, and also not a reason to do nothing for the hours or days between now and the patch landing. These reduce the surface. Every one of them has to be set at the server level (php.ini, the FPM pool, or the Apache/LiteSpeed config) followed by a restart of every worker. ini_set() in your application and .user.ini run too late; the vulnerable code has already executed by the time they apply.

; php.ini or the FPM pool. Then restart PHP (not reload).

; Closes the confirmed session path of CVE-2026-109619. WordPress, Laravel and Drupal do not use this feature.
session.upload_progress.enabled = 0

; PHP 8.5 only: closes the confirmed backtrace path of CVE-2026-109619.
fatal_error_backtraces = 0

; Makes the out-of-memory path of CVE-2026-109618 harder to reach on purpose.
; Yes, unlimited. The bug is in PHP's own OOM handler; letting PHP hit its limit is what triggers it.
; Bound memory at the container or cgroup level instead if you must. An OS-level allocation failure can still reach the path.
memory_limit = -1

; Shrink what one request can make PHP allocate while parsing input.
post_max_size = 8M          ; 32M if the app takes uploads
upload_max_filesize = 4M
max_input_vars = 1000
max_file_uploads = 5

For PHP-FPM there is one more, and it is the one I would actually reach for first on a busy site:

; FPM pool. Recycle the worker after every request so no state survives into the next one.
; This is a real performance cost (opcache survives, the process does not). Accept it this week.
pm.max_requests = 1

In front of PHP, cut off the request shapes the exploits need:

# nginx, in the server block. Multipart bodies are where the known vectors live.
client_max_body_size 8m;
client_header_buffer_size 4k;
large_client_header_buffers 4 8k;

# If the application has no file uploads at all, refuse multipart outright.
if ($content_type ~* "multipart/form-data") { return 413; }
# Apache, in the vhost or .htaccess where allowed.
LimitRequestBody 8388608
LimitRequestFieldSize 8190

If you are behind Cloudflare, the managed rules for these CVEs are on for every plan including Free; on paid plans check that the managed ruleset is in block mode, not log. Imunify360 blocks the public exploit on Apache but only logs it on LiteSpeed, which matters on a lot of shared hosts.

What looks like a mitigation and is not. Hiding errors (display_errors=0, log_errors=0, @) changes nothing; the stale-state access happens whether or not it is reported, and a quiet error log is not evidence of safety. RLIMIT_AS and RLIMIT_DATA make it worse, because an allocation failure is the trigger. Putting the site behind a login does nothing for mod_php, where the exploit runs before your login code does. Taking the site offline does work, and for a brochure site that nobody can patch today it is the right call for a weekend.

Patch, per environment

The rule across all of them: upgrade the package, then restart the process that serves requests. A reload is not enough for mod_php; running Apache children keep the old module. Queue workers and anything under Supervisor keep the old binary in memory until they are restarted too.

Ubuntu 22.04 / 24.04 / Debian 12 / 13

sudo apt-get update
apt list --upgradable 2>/dev/null | grep php
sudo apt-get install --only-upgrade 'php8.3*' libapache2-mod-php8.3   # your branch
sudo systemctl restart php8.3-fpm        # FPM
sudo systemctl restart apache2           # mod_php: restart, not reload
zgrep -E 'CVE-2026-10961[89]' /usr/share/doc/php8.3-fpm/changelog.Debian.gz   # the proof

Ubuntu 20.04 gets the fix only with Ubuntu Pro (sudo pro fix CVE-2026-109618). Debian 11 has no free fix; that server moves or gets a source build. If your PHP comes from ppa:ondrej/php or packages.sury.org, the two were updated at different times on October 21; check which one the server uses before trusting apt list.

RHEL / AlmaLinux / Rocky

dnf module list --enabled php
sudo dnf upgrade 'php*'
dnf updateinfo list --security | grep -i php
rpm -q --changelog php-fpm | grep -E 'CVE-2026-10961[89]'
sudo systemctl restart php-fpm httpd

Red Hat's own packages were still pending on day one. Remi's repository had no build on October 21 either; when it lands, sudo dnf upgrade 'php84-*' and systemctl restart php84-php-fpm, with your version in place of 84. Until then, the mitigations above and pm.max_requests=1.

Docker and Kubernetes

# pin to the fixed patch version, never to a floating tag
FROM php:8.3.36-fpm-alpine

docker build --pull --no-cache -t myapp:2026-10-23 .
docker compose pull && docker compose up -d --force-recreate
docker exec <container> php -v

# Kubernetes: change the image tag or digest in the Deployment, then
kubectl rollout status deployment/<name>

A running container never picks up a new image. kubectl rollout restart only helps if the tag is floating and imagePullPolicy: Always; otherwise it restarts the old image.

cPanel / WHM (EasyApache 4)

sudo dnf clean all
sudo dnf update 'ea-php*' 'ea-apache24*'
/scripts/restartsrv_httpd
/scripts/restartsrv_apache_php_fpm
for p in /opt/cpanel/ea-php*/root/usr/bin/php; do "$p" -v | head -1; done

cPanel shipped fixed packages, including builds for 8.1, 8.0 and 7.4, which matters because a lot of shared hosting still runs them. On CloudLinux, sudo dnf update 'alt-php*'. If you are a customer on shared hosting rather than the owner of the server, you cannot run any of this: ask the host in writing whether the October 21 release is applied, and set session.upload_progress.enabled=0 in MultiPHP INI Editor yourself while you wait.

Plesk

plesk bin php_handler --list
plesk installer --select-release-current --upgrade-installed-components
systemctl restart plesk-php83-fpm
plesk bin php_handler --reread

Laravel Forge, Vapor, Laravel Cloud. Cloud patches for you. Forge updates PHP from the server's PHP tab; after it, php artisan queue:restart and php artisan octane:reload if you run Octane, because those processes hold the old binary.

Managed WordPress. Kinsta, WP Engine, Flywheel, Pressable, Pantheon and SiteGround all published notices and patched or shielded at the platform level. No WordPress core update exists or is planned for this; WordPress.org's statement is that the PHP update alone is the fix.

PHP 8.1, 8.0, 7.4 with no vendor package. The PHP security team published source-only branches named PHP-7.4-CVE-2026-109618-109619, PHP-8.0-... and PHP-8.1-... containing only the two fixes. Clone with git clone --depth 1 --branch PHP-8.1-CVE-2026-109618-109619 https://github.com/php/php-src.git, build with your original configure flags, and know that php -v will still print the old number and nobody maintains the branch after this. It is a bridge to an upgrade, not a destination.

Verify the patch took

Three separate things have to be true, and people check one.

1. The package on disk is fixed. apt-cache policy or rpm -q shows the new version, or the changelog names the CVEs. On Ubuntu and Debian the version number may not change; the changelog line is the evidence.

2. The process serving requests is running the new code. Patching a file does not change what a running process has in memory.

# any process still holding a deleted (replaced) library or binary
sudo grep -lE ' /(usr|lib|opt).*\(deleted\)$' /proc/[0-9]*/maps 2>/dev/null | cut -d/ -f3 | sort -u | xargs -r ps -o pid,lstart,comm -p

# Debian/Ubuntu shortcut
sudo needrestart -b -r l

# start times: anything older than the patch time is suspect
ps -eo pid,lstart,comm | grep -Ei 'php-fpm|lsphp|httpd|apache2'

If a PHP worker's start time is before you patched, it is unpatched, whatever the package says. systemctl restart php8.3-fpm, systemctl restart apache2 (not reload), pkill lsphp for LiteSpeed, supervisorctl restart all for queue workers, php artisan queue:restart for Laravel.

3. The thing the browser hits agrees. The same throwaway file from the inventory step:

<?php echo PHP_VERSION, ' ', PHP_SAPI, ' upload_progress=', ini_get('session.upload_progress.enabled') ? 'on' : 'off', ' memory_limit=', ini_get('memory_limit');

Request it over HTTPS through whatever is in front (CDN, load balancer) so you test the real path, then delete it. If X-Powered-By is in your response headers, expose_php=Off is a free improvement while you are in php.ini.

Do this on every server, including the one that "only runs cron." If you have more than five servers, write the three checks into one script and run it from a central box; the output is your evidence for the board, the client or the auditor.

Assume breach from October 17

Amazon's advice is to investigate from October 17, and that is the right starting line. The patch stops new exploitation. It does nothing about a web shell dropped on the 19th. No public list of indicators has been released; what follows is what the exploit paths imply and what the hosts and CDNs have described seeing.

In the web server logs. The known vectors ride on multipart POST bodies, the session upload-progress field, and requests built to make PHP run out of memory. Look for:

# multipart POSTs from an IP that then got 500s (memory exhaustion looks like a 500)
awk '$6 ~ /POST/ && $9 == 500' /var/log/apache2/access.log* /var/log/nginx/access.log* 2>/dev/null | awk '{print $1}' | sort | uniq -c | sort -rn | head

# the session upload-progress field name in request bodies or query strings
grep -h 'PHP_SESSION_UPLOAD_PROGRESS' /var/log/nginx/*.log /var/log/apache2/*.log 2>/dev/null | head

# memory exhaustion in the PHP or FPM error log, with timestamps, since the 17th
grep -h 'Allowed memory size' /var/log/php*-fpm.log /var/log/apache2/error.log /var/log/php_errors.log 2>/dev/null | awk '$0 >= "[17-Oct-2026"' | head

# FPM worker churn: children dying and respawning in bursts
grep -hE 'child [0-9]+ exited on signal|SIGSEGV|SIGBUS' /var/log/php*-fpm.log 2>/dev/null | head

Unusually long or malformed PHPSESSID cookies in access logs (if you log cookies) are another signal Wiz called out. A burst of 500s from one IP on a site that normally never returns 500 is the single most useful thing to look for.

On disk. The payload is almost always a file.

# PHP files changed since Oct 17 anywhere under the web root, especially in upload directories
find /var/www -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \) -newermt '2026-10-17' -printf '%TY-%Tm-%Td %TH:%TM %u %p\n' | sort

# PHP files where they should never be
find /var/www -path '*/wp-content/uploads/*' -name '*.php'
find /var/www -path '*/storage/*' -name '*.php'

# the usual web shell fingerprints
grep -rlE 'eval\s*\(\s*(base64_decode|gzinflate|str_rot13)|passthru\s*\(|shell_exec\s*\(|\$_(POST|GET|REQUEST)\[[^]]+\]\s*\(' /var/www --include='*.php' 2>/dev/null | head

# for WordPress: a core and plugin checksum pass
wp core verify-checksums; wp plugin verify-checksums --all

Persistence. New cron entries (crontab -l for the web user and root, /etc/cron.d), new systemd units or timers, new SSH keys in ~/.ssh/authorized_keys for every user, new admin users in the application (WordPress: wp user list --role=administrator), new outbound connections from the web server (ss -tnp | grep -E 'php|apache|lsphp').

If you find anything. Treat the server as compromised, not as cleaned. Rebuild from a known image, restore content from a backup taken before October 17 if the content allows it, and rotate every secret the server could read: database passwords, API keys in .env or wp-config.php, the deploy key, SMTP credentials, any cloud role attached to the instance. A web shell with the worker's privileges has read all of them.

If you find nothing. Write down what you checked and when. "We looked and found nothing" is a sentence you will be asked to defend; the log excerpts and the file listing are what make it defensible.

What not to do

Do not start a rewrite. The question "should we get off PHP" has come up in three conversations this week. The answer this week is no. Node had a critical in its HTTP parser, Python had one in its tarfile module, Java had Log4Shell; every runtime has one of these every few years. What protected the companies that came through those quickly was not the language. It was knowing every box they ran, being able to patch in an afternoon, and having logs to look at afterwards. If your estate is hard to patch, that is the project. A rewrite would take a year and leave you with a new runtime and the same problem.

Do not hide the errors. Turning off display_errors and log_errors or sprinkling @ on calls makes the log quieter and the server no safer. The stale-state access happens either way. You also lose the one signal that would have told you something was being tried.

Do not reach for process limits. RLIMIT_AS, RLIMIT_DATA and a tight memory_limit all make allocation failures more likely. Allocation failure is the trigger for CVE-2026-109618. Bound memory at the container or cgroup level if you must bound it.

Do not trust php -v. It reports the CLI binary. The web SAPI can be a different build, and on Ubuntu and Debian the fixed package keeps the old version string.

Do not reload when you mean restart. A graceful Apache reload keeps the old mod_php in the children that are already serving. FPM reload is fine for config, not for a new binary. Restart.

Do not patch and stop. The window opened on October 17 at the latest. A patched server with a web shell from the 19th is a compromised server.

Do not take a working site down in a panic if you can patch it in the same hour. Taking it down is the right call for a site nobody can patch this week. For everything else, the hour spent patching beats the hour spent explaining the outage.

FAQ

Our product is on Node. Are we done?

Your product is. Your WordPress site, your phpMyAdmin and anything else with PHP in it are not. List them, patch them, and check what credentials and network paths those servers have to the product.

Our host says they patched. Do we need to do anything?

Confirm it: Site Health in WordPress, or the throwaway PHP_VERSION file, through the real public URL. Then do the hunting section anyway if the site was reachable between October 17 and the host's patch time. The host patched the runtime; they did not look at your uploads folder.

We are on PHP 7.4 and the host will not move us. What now?

cPanel shipped fixed 7.4 packages, so ask the host specifically for those. If they will not, set session.upload_progress.enabled=0, put the site behind Cloudflare today, and plan the move; a runtime nobody will patch is a liability you are choosing to keep.

Does a WAF make this safe?

No. Cloudflare's own write-up says attackers were mutating payloads against its rules within hours. A WAF buys time and visibility. Patch.

Do we need to update WordPress, Laravel or Drupal?

No. WordPress.org's statement is that no WordPress update is required or planned; the fix is PHP itself. The same holds for the frameworks.

Should we rotate secrets even if we found nothing?

If the server was exposed and unpatched for any part of October 17 to 21 and you cannot prove from logs that nothing reached it, rotating the database password, the API keys in .env or wp-config.php and the deploy key costs an hour and removes the question.

How do we stop being in this position next time?

An inventory that lists every PHP binary and what serves it, one script that reports version and worker start time across the estate, log retention of at least 30 days, and a habit of restarting workers as part of every patch. None of that is a project; it is an afternoon.

If you want a second pair of eyes on an estate this week, the contact page is at decipheringlogic.com/contact. It costs nothing to ask.

Have a feature stuck at the demo stage?

The $5,000 Decipher Blueprint de-risks it in two weeks — architecture, scope, and an honest quote. Credited if you build with us.

How the Blueprint works