The PHP security team did something it rarely does: it gave advance warning of a patch. On 20 October a message appeared on the php.announce mailing list and on the php.net home page: on 21 October at 14:00 UTC an unscheduled update would be released, fixing two critical vulnerabilities. Both were rated 9.2 under CVSS version 4.0. The announcement contained no description, no CVE numbers and no list of affected versions: all of that was promised together with the patch.
The patch even came out earlier than promised — the vulnerability advisories appeared at 13:34 UTC, twenty-odd minutes ahead of schedule.
The short answer, if that’s all you need: update to 8.2.35, 8.3.36, 8.4.27 or 8.5.12, depending on your branch. Both holes are unauthenticated remote code execution, and your CMS makes no difference. Below are the details, and what to do if “just update” doesn’t work in your case.
If you’re short of time, here is the fork in the road. If your site is static, with no server-side code, this doesn’t concern you. If it’s on ordinary shared hosting, your host will do the update; your job is to find out the version and keep an eye on it. If it’s your own server or containers, the update is on you, and the commands are below. If you don’t know which of the three is yours, start with step one.
What PHP is and why this concerns you
A short introduction for those who have heard of PHP but never worked with it. It’s the language behind many popular CMSs — WordPress, Joomla, Drupal, Bitrix, OpenCart — and most custom-built sites of small companies. It doesn’t run in the browser but on the server: when someone opens a page, the server runs the PHP code and returns ready-made HTML.
Two things follow from this. First, a hole in PHP itself is not a hole in one plugin or one theme but in the foundation the whole site stands on. Second, if a site only serves ready-made files — a static site on Astro, Hugo, Tilda or Webflow, a plain HTML landing page — there is no PHP in the picture. That said, a storefront may have a neighbouring service or a separate admin panel in PHP, so if in doubt it’s better to ask your developer or host.
Why the patch was announced in advance
Normally a vulnerability and its patch are published at the same time: until the hole is closed, any detail about it is a gift to an attacker. Advance notice is the exception.
Judging by the wording of the announcement, the logic is to give administrators time to prepare. When an update lands out of the blue, large infrastructure needs a whole day just for the organisational side — agreeing a window, warning clients, building images. With a day’s warning, the plan is ready by the time of release. The price is that both sides read the warning, attackers included.
The announcement made a point of saying that if the details became public or there were signs of real attacks, the release would come out ahead of the scheduled time. In effect, an insurance policy against its own warning.
What 9.2 means and why it’s a lot
CVSS is the industry scale of technical severity, from 0 to 10. The 9.0–10.0 range is called critical, and 9.2 falls into it.
But it’s important to understand what the number doesn’t tell you. It doesn’t tell you how the vulnerability is exploited, which versions are affected, whether a special configuration is needed or whether the vulnerable module is enabled on your server. Sometimes a vulnerability that is critical on the scale lives in an extension that most servers simply don’t have. Nor does it tell you how likely it is that your particular site will be broken into. Without a description, 9.2 is a reason to take things seriously, not a reason to assume you’ve already been hacked. In this case the description arrived with the patch, and it turned out to be unpleasant — more on that in the next section.
A second detail: this is a score under version 4.0, a fairly new edition of the methodology. Under version 3 the same holes might have got a different number, so when publications disagree it’s worth comparing not only the number but the methodology.
The vulnerabilities: CVE-2026-109618 and CVE-2026-109619
The numbers that circulated online before the release turned out to be real: they are CVE-2026-109618 and CVE-2026-109619, both marked critical. Fixed versions by branch:
| PHP branch | Vulnerable versions | Update to |
|---|---|---|
| 8.2 | below 8.2.35 | 8.2.35 |
| 8.3 | below 8.3.36 | 8.3.36 |
| 8.4 | below 8.4.27 | 8.4.27 |
| 8.5 | below 8.5.12 | 8.5.12 |
| 7.4, 8.0, 8.1 | all builds without the patch | a package from your vendor or the PHP team’s source backport |
| 7.3 and older | not listed in the advisories | your package vendor or a move to a supported branch |
Both are unauthenticated remote code execution, with the privileges of the user the web server runs as. And both are about the same thing: memory that survives the boundary of a request.
The first, “RCE via OOM corruption”, is about running out of memory. When a request hits the limit, PHP bails out of the current context by a shortcut. At certain points the request’s memory is already visible but not yet fully set up, and the emergency exit leaves it in an invalid state; the request shutdown procedure then looks into that state. In its advisory the PHP team writes that full code execution has been confirmed over HTTP against mod_php with default settings.
The second, “RCE via stale global state”, is about state that a worker keeps between requests: errors, sessions, objects, extensions. Some operations run before the next request’s normal initialisation and manage to touch data left over from the previous one and already freed. That’s a use-after-free, and with a well-arranged heap, code execution too. Here both mod_php and FPM are confirmed. An important detail: the request that leaves the garbage and the request that stumbles on it have to land on the same worker — multiple workers and process restarts lower the odds but don’t remove the problem.
What this means in practice. Neither case requires an account on the site or any action from a visitor. Neither depends on the CMS: the hole isn’t in WordPress or a plugin but in PHP itself, so “my site is simple, there’s nothing to break” doesn’t work here.
One more detail from the advisory for the second hole — it answers the objection “my site doesn’t even use sessions”. One of the links in the confirmed chain is upload progress tracking, session.upload_progress. It’s enabled by default and fires without any session_start() in the site’s code. In other words, the site doesn’t need to work with sessions itself for that link to be present.
About the rumours — and why you still shouldn’t have relied on them
It’s worth recording what this story looked like before the release, because the next one will look the same.
Before the patch came out, the PHP team confirmed exactly four things: an unscheduled release, the date and time, two vulnerabilities and a score of 9.2 under CVSS v4.0. The notice contained no CVE numbers, no description and no list of versions. Everything else — the numbers, the word RCE, “all versions since PHP 5 are affected”, a “CVSS 10.0” score — went round social media from an anonymous retelling citing “a reliable source”.
The numbers and RCE were eventually confirmed. But that doesn’t mean they deserved belief in advance: at that moment there was nothing to check them against, and rumours that don’t pan out look exactly the same. Not all of it was confirmed, mind you: the “since PHP 5” scope has not been officially confirmed — the advisories list the supported branches, and the team released backports for 7.4, 8.0 and 8.1, nothing older; the score stayed at 9.2, not 10.0.
Next came a “working exploit” doing the rounds — an archive with a proof-of-concept script. Ondřej Surý, maintainer of the most popular repository of PHP packages for Debian and Ubuntu, warned that nobody had vetted the archive, that it looked AI-generated, that with the wrong parameters it simply crashes the process, and that it should be treated as untrusted. The takeaway for a site owner is simple: no “vulnerability checks” on your server with scripts from social media. It’s a classic way to bring in something worse than the hole itself.
Why delaying the update is more dangerous than it used to be
I don’t have a direct measurement of “how much time passes between a PHP patch and the first attacks”, and it’s more honest to say so up front. But the overall economics of attacks have changed over the past year, and that has been measured.
In my write-up of the NorForge research into open Cairn panels I cited figures from a campaign against online shops: the average cost of processing one target was about 25 dollars, victims were selected automatically by traffic, the success rate was around ten per cent, and it took less than a day from launch to result. The same mechanics, seen from the scam-service side, come up in the talk on AI-powered phishing.
The takeaway for a site owner is simple. When an attempt costs that little, you can go through everyone, including small sites with “nothing to take”: the “who’d bother with us” argument rested on the attacker’s time being expensive. And the time your site goes unpatched is exactly what they’re counting on.
How to update PHP: a step-by-step plan for site owners
Start with the diagnosis — everyone needs it and it takes minutes. Then the steps for your setup.
Step 1. How to find out the PHP version on your site
The easiest place to check the PHP version is your hosting control panel. In cPanel it’s MultiPHP Manager, in FASTPANEL it’s the site card and its PHP settings, in ispmanager it’s the domain settings. Bear two things in mind: a setting may apply to several sites at once, and the command-line version is chosen separately from the version the site runs on.
If the site is on WordPress, the version is visible in the admin area: Tools > Site Health > the “Info” tab > the “Server” section. Section names vary slightly from version to version. WordPress also warns you there about an outdated branch — but the absence of a warning doesn’t mean a specific patch is installed.
On your own server there’s a trap people fall into regularly. The php -v command shows the version of command-line PHP, but the site runs through a separate process — PHP-FPM — and its version may be different. What’s more, there may be several FPM versions on the machine, and different sites may be served by different ones. What exactly serves a given site is visible in the web server configuration, by the socket or port in fastcgi_pass:
grep -rn fastcgi_pass /etc/nginx/
systemctl list-units 'php*-fpm.service'
The first command shows which socket the site talks to, the second shows which FPM services are running on the machine at all. Match them up and you have the answer to “which PHP do I have” — the one that actually matters.
Step 2. Who updates PHP: your host, your own server or Docker
Ordinary or managed hosting. Your host will update PHP; you can’t do it yourself. Your job is not to wait in silence. Check your provider’s news or status page, or better, write to support with a direct question: which PHP version serves my domain, is the 21 October security update installed (8.2.35, 8.3.36, 8.4.27 or 8.5.12), and if not, when will it be, and is anything needed on my side. Timelines vary radically between hosts, from a few hours to the next scheduled maintenance, and the answer to that question is the only way to find out yours.
A common question from WordPress owners is how to update PHP from the admin area. You can’t: WordPress only shows the version in “Site Health”, and it’s changed in the hosting panel. Within one branch — from 8.3.35 to 8.3.36 — the host does this, and such an update usually goes unnoticed. But “usually” is not a guarantee: afterwards it’s worth opening the site and checking forms and payments, as in step five. If you need to move to a different branch, for example from 7.4 or 8.1 to a supported one, update WordPress, the theme and plugins first: old plugins are what most often break on a new PHP version.
Your own VPS or dedicated server. First of all, find out where your PHP packages come from at all: that determines when the fix will arrive.
If they come from Ondřej Surý’s third-party repository — packages.sury.org for Debian or ppa:ondrej/php for Ubuntu, and on servers with several PHP versions it’s almost always this one — fixes are built there on release day. The maintainer promised them for all branches in the repository and for all supported Debian and Ubuntu releases: first for ordinary amd64 servers, ARM builds later. He gave no timeline; the build server was overloaded that day.
If PHP comes from your distribution’s standard repositories, the fix will arrive through the usual security updates — but not straight away. On the evening of 21 October, all affected Ubuntu packages had the status “needs evaluation”, and the Debian tracker had no entries for these CVEs yet. In that case the temporary measures in the section below are not optional; they’re what you need to do until the package is out.
These commands show what’s installed and what’s available:
sudo apt update
apt list --upgradable
apt-cache policy php8.3-fpm # substitute your version
apt-mark showhold
In the apt-cache policy output, the first line is the installed version, the second is the upgrade candidate, and below you can see which repository it will come from. apt-mark showhold checks for a trap: packages that someone once put “on hold” won’t update either in bulk or individually, and it will look as though there’s nothing to update. If PHP is on that list, release the hold with sudo apt-mark unhold followed by the package names.
Take a backup before you run the next command, not after: backups are covered in detail in step four, but this is where you do it. If the update clashes with an extension, you’ll have nothing to roll back to.
If the list contains only PHP packages, you can update everything at once (sudo apt upgrade). If half the system you weren’t planning to touch today is there too, update selectively, listing your PHP packages together with their extensions:
sudo apt install --only-upgrade php8.3-fpm php8.3-cli php8.3-common
Next you need to restart the FPM services that serve your sites — all of them, if there are several versions:
sudo systemctl restart php8.3-fpm
There’s no need to touch Nginx: it talks to FPM over a socket, and updating PHP doesn’t change its configuration. Reload the configuration only if you’ve edited it, and in that case run sudo nginx -t first. If PHP is loaded into Apache as a module (mod_php) rather than through FPM, you need to restart Apache itself — the module lives inside its processes.
Separately, there are long-running PHP processes outside FPM: Laravel Octane, Swoole, RoadRunner, queue workers and daemons under supervisor. They keep in memory the PHP version they started with, and an updated package on disk doesn’t affect them. Restart them too — above all the ones that accept HTTP requests themselves.
For RHEL, AlmaLinux and Rocky the procedure is the same with dnf, usually with the Remi repository enabled.
PHP in Docker. A base image update doesn’t arrive on its own: the container will keep running on the version it was built with. Backups — same rule as above: before, not after.
First, look in your Dockerfile and compose.yaml to see how the version is written. If it says FROM php:8.3-fpm, a fresh image will be pulled. But if the version is nailed down — php:8.3.35-fpm or a digest reference — no command will move you off it: the tag points to a specific build, and --pull will dutifully check for updates to exactly that build. You need to change that line by hand to the fixed version, and only then update.
What comes next depends on how the service is built. If you have your own Dockerfile with FROM php:...:
docker compose build --pull php
docker compose up -d php
If compose.yaml specifies a ready-made image via image::
docker compose pull php
docker compose up -d php
Here php is the service name from your Compose file; substitute your own. The --pull flag matters: without it, the build may take the old base image from the local cache, and --no-cache doesn’t solve this — it rebuilds the layers but doesn’t update the base. And if your images are built in CI, remember that the build is only half the job: the new image still has to be published and rolled out; a running container won’t update itself.
And a separate trap that can take the site down after a successful update. If Nginx runs in a neighbouring container and reaches PHP by service name (fastcgi_pass php:9000), the recreated container gets a new address while Nginx keeps knocking on the old one — and instead of the site you’ll see a 502. The fix is to restart the front-end container straight after:
docker compose restart nginx
To check the result inside the container:
docker compose exec php php -v
Step 3. Which PHP version is current, and is yours still supported
This is a separate point and, in my view, the most important one for small sites.
The current PHP versions are the ones the project itself supports: right now 8.2, 8.3, 8.4 and 8.5. The latest stable is 8.5, while 8.6 is still at the release candidate stage and not fit for a production site. Branch 8.1 and everything older have been declared end of life: the PHP team no longer issues regular releases for them. This time it made an exception — more on that shortly.
An important caveat, without which this point sounds scarier than it is. Distributions and commercial vendors often maintain old branches themselves: Debian, Ubuntu and RHEL backport security fixes into their packages over the lifecycle of the OS release. So a site on PHP 7.4 from the system repository may get a fix from the OS vendor — or it may not. The only way to find out is to ask whoever supplies those packages to you: your host, your OS vendor or your contractor.
The PHP team itself also made an exception. Because of the severity of the vulnerabilities, it published minimal source backports of the fix for three branches that are out of support: PHP 7.4, PHP 8.0 and PHP 8.1 in the php-src repository. It’s important to understand what these are. They are not releases: there will be no new version numbers and no ready-made packages; you have to build the patch yourself. They’re aimed at people who build PHP from source, or who have no vendor maintaining the old branch. They won’t help an owner of a site on ordinary hosting — there the question is still for the host. And they don’t cover PHP 7.3 and older.
A good example is that same sury repository. A fix for these two CVEs was promised there for every branch it carries, not only those supported by the PHP team. According to user reports, on release day updates arrived for 7.4, 8.0 and 8.1, and even for 5.6. That’s no reason to stay on 7.4: the next hole in an old branch may never get patched. But for the moment it answers the question “what do I do right now”.
It’s also worth looking at the support calendar. Security fixes for branch 8.2 end on 31 December 2026 — in two months it will be where 8.1 is now. Branch 8.3 is covered until the end of 2027, 8.4 until the end of 2028, 8.5 until the end of 2029. If you’re on 8.2, it’s worth preparing the migration now: choose a branch compatible with your CMS, plugins and extensions, and test the move before the end of the year, not in the last week of December.
For a site on an EOL branch the order is: first a backup and a clear way back, then a test on a copy of the site, and only then the switch on the live one. Switching the version in the panel is not in itself the same as installing the patch — make sure you’re moving to a fresh build of a supported branch, not last year’s.
Step 4. A backup before the update — and a check that it restores
A truism that saves the day. Updates within one branch usually go more smoothly than a move to a new one, but that’s no guarantee: the site may have an extension compiled for a specific build.
The copy should include both files and database, be stored somewhere other than the same server — and actually restore. A copy that has never been restored is not a backup but a hope. For a shop, add one more question: what to do with orders that come in between taking the copy and rolling back — restoring yesterday’s database will wipe them.
And one point people remember too late: the backup must be protected from deletion by the same account the site runs under. In the write-up of the campaign against online shops, the automated attack also wiped the backup tables on one victim’s site.
Step 5. How to check the update was installed
After updating, check the version of the process serving the site and the full version of the installed package. A nuance: if the fix came from your OS vendor, the PHP version number may not change at all — the package version changes instead. In that case the confirmation is the vendor’s bulletin, not the output of php -v.
Then open the site — and not just the home page. The admin area, the payment form, file uploads, outgoing email: these are the places where an incompatible extension shows up. And look at the web server’s error log: it will show what isn’t visible on the page.
What to do if you can’t update PHP right now
It happens: the contractor is unavailable, the site is ancient, the maintenance window is only at the weekend. Then you need measures that reduce the risk but don’t replace the patch. The PHP team says this in plain words in both advisories and flags it as a warning: the only complete solution is to update. Everything else breaks individual links in the chain. But these measures are concrete, and in places not obvious — here is what the PHP team itself recommends to those who can’t update yet.
For the first hole, the memory exhaustion one, the main setting is to administratively set memory_limit=-1 and restart all workers. Yes, remove the limit rather than tighten it: it’s precisely a predictable per-request limit that lets an attacker control the confirmed scenario. Before doing this, it’s worth honestly assessing whether the server can cope with requests that have no memory ceiling, and checking that no pool, directory or application setting puts a finite limit back. The advisory also carries a separate warning: don’t limit memory via RLIMIT_AS and RLIMIT_DATA — they lead to exactly the memory allocation failure that triggers the vulnerable path. A container or cgroup memory limit is a possible substitute, but with caveats and not complete either.
Some things reduce the risk even after the patch: keep post_max_size modest, check the real size of the request body at the front end, reduce input nesting and the number of input variables, and if the application doesn’t accept multipart forms at all, cut such requests off before they reach PHP. In Nginx that’s a few lines inside the server block:
if ($http_content_type ~* "multipart") {
return 403;
}
The price of this measure is that any file upload and any form that sends data as multipart will stop working. Before switching it on, go through the site’s forms.
For the second hole, the one about state between requests, there is no general setting at all — that’s also stated plainly. Specific measures: on PHP 8.5, turn off fatal_error_backtraces; for everyone, turn off session.upload_progress.enabled if you don’t need it (the PHP team advises keeping it off even after updating: the feature is rarely used and poorly battle-tested), and restart the workers. If performance allows, pm.max_requests=1 helps — a worker lives for one request and has no chance to carry anything into the next. And it’s worth dealing with fatal errors and warnings on paths a visitor can reach: they are what leave that very garbage behind.
“Administratively” is a key word in these recommendations. The settings are set where the application can’t override them: in php.ini or in the FPM pool. For example:
; /etc/php/8.3/fpm/pool.d/www.conf — substitute your version and pool
php_admin_flag[session.upload_progress.enabled] = off
php_admin_value[memory_limit] = -1
; PHP 8.5 only
php_admin_flag[fatal_error_backtraces] = off
After editing, restart FPM. But .htaccess with php_value and php_flag, and the PHP_VALUE parameter that Nginx passes to FastCGI, won’t do for this. The PHP team explicitly advises preferring settings applied at startup: values that arrive with each request are processed before its initialisation, and a deprecated directive that raises a warning there is one of the links in the second chain. Old and invalid directives are best cleaned out of there altogether.
And what not to do even in a panic: “switch off PHP” in the web server. If you remove the handler, Nginx or Apache will start serving .php files as plain text, along with the database passwords from the config files. If you need to take the site down for the duration of the update, have the web server respond with a 503.
After that comes ordinary hygiene, which was relevant before this story too.
Reduce what’s exposed. An admin area reachable from anywhere, phpMyAdmin at its default address, a forgotten test subdomain with a copy of the site — all of this is extra attack surface. I wrote in detail about ports, the firewall and what to close by default in the article on a safe start for a service in Docker. When restricting access, don’t cut off yourself and the integrations you need: allow for mobile internet with a changing address and payment notifications that come from outside.
Look at a WAF if your hosting plan includes one or you use Cloudflare: the details have been published, and security vendors respond to this kind of thing — it’s worth checking whether a rule has appeared and whether it’s enabled. But bear in mind that both holes are triggered by ordinary-looking requests, so don’t expect much from a filter. And a WAF only works if the server can’t be reached around it: if the site’s IP is known and open to everyone, the Cloudflare filter will simply be bypassed.
Set up file monitoring. An unexpectedly changed file in the site root or a new script from an unfamiliar domain is something worth noticing automatically, not the next time you happen to log in to the admin area.
And set yourself a deadline. “We’ll update when we have time” is the worst option: it turns temporary measures into permanent ones.
How to check whether you’ve been hacked
Details of the holes leaked a day before the patch, and the PHP team itself confirmed code execution over HTTP. Even so, as of publication there were no confirmed reports of mass attacks: claims were going around, but nobody showed public data behind them. It’s too early to panic, but it’s exactly the right time to take a look, especially if the site was open to everyone before the update.
What to check first:
- web server processes: php-fpm or Apache launching a command shell is a bad sign;
- new and modified
.phpfiles in the site root and especially in upload directories; - scheduler jobs —
crontab -las the web server user and the/etc/cron.*directories: gaining a foothold there is a standard trick; - the access log: spikes of
POSTrequests withmultipart/form-datato pages that don’t accept uploads at all.
You can find PHP files changed in the last three days with one command (use your own path):
find /var/www -name '*.php' -mtime -3 -ls
If something turns up, updating alone isn’t enough: a patch doesn’t revoke access that has already been gained. You’ll need to change passwords and keys, audit the files and, most likely, restore from a copy taken before the problems.
Checklist: updating PHP in ten steps
Briefly, in order:
- Find out which PHP serves the site: the hosting panel, “Site Health” in WordPress, or
fastcgi_passplus the list of FPM services. - If the site only serves ready-made files and has no server-side code, stop reading here.
- Check the branch: you need 8.2.35, 8.3.36, 8.4.27 or 8.5.12 — anything below is vulnerable. On 7.4–8.1, a vendor package or the PHP team’s backport for building from source; on anything older, only your vendor or a move.
- Work out who does the update: the host or you.
- On hosting, ask support about your domain, the patch and anything needed on your side.
- On your own server, find out where the packages come from (sury or the distribution), check
apt-mark showhold, take a backup, update the PHP packages, restart all FPM services and long-running PHP processes. - In Docker, check whether the version is pinned in the tag, rebuild with
--pullor pull the ready-made image, redeploy, restart the front end, check the version inside the container. - After the update: version, package version, the whole site (admin area, payments, email), the error log.
- If you can’t update: the temporary measures from the PHP advisories in
php.inior the FPM pool, reduce the exposed surface, set a date. Don’t switch off PHP in the web server — serve a 503. - If the site was open before the update, check processes, recent files, cron jobs and the access log.
What will remain
These two specific vulnerabilities will be closed, and in a week they’ll be forgotten. Something else will remain.
First: inventory. The most expensive answer to “have you updated?” is “where do we even have PHP?”. A list of where your server-side code runs, with versions and owners, is put together once and then maintained — and on days like this it saves hours.
Second: dead branches. PHP 7.4 has long been unsupported by the PHP team, and without separate maintenance from a vendor it still has unfixed holes. An unscheduled patch is a good reason to look at the version number and put the move in the plan before it becomes urgent.
And third, less obvious. The day’s advance warning is itself an admission that the speed of patching has become more important than silence around a vulnerability. For a site owner, the right response is not panic on release day but a procedure known in advance: who updates, in which window, with which backup and how the result is checked. Writing it down once is cheaper than improvising every time.
This article is based on the PHP security team’s notice of 20 October 2026, the vulnerability advisories GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp in the php-src repository, the PHP 8 changelog, replies from the maintainer of the deb.sury.org repository and Ubuntu Security data.





