Category Archives: Security

Home Router Vendors Should be Forced to Fix Their Certificate Warning

A social media post was asking about the hassle of a secure connection to an ISP router (using TLS), and it reminded me of the early 1990s when we used to hand-craft TCP/IP networks on 3Com, Cayman, etc. as if it was serious engineering. All that’s ancient history because everything has been made so easy that anyone can connect securely.

That being said, vendors today aren’t really doing their part to make the easy stuff easy. For example, the German company FRITZ! still tells its customers to click through a certificate warning on their own router login.

That’s just nuts.

Its help page walks users through clicking past the certificate warning in every major browser and assures them that from the home network “This does not pose a security risk.” The same page tells them to ignore the browser warnings about an unencrypted login as well, because the interface opens inside the home network and the password stays off the internet.

Matthew Wallace and I covered this attack in Securing the Virtual Environment: How to Defend the Enterprise Against Attack, published by Wiley in 2012. That’s 15 years ago! Certificate warnings and operator fatigue were among the easiest attack vectors in the book. An operator who clicks through the same warning on every login will click through an attacker’s certificate the same way, because the two warnings look identical.

Come on vendors, what are you doing?

The certificate authorities way back in November 2011 (CA/Browser Forum) adopted Baseline Requirements that phased out publicly trusted certificates for internal names and reserved IP addresses, with November 1, 2015 as the latest permitted expiry. That’s a decade gone already since everyone was supposed to be doing internal networking the right way. Look at how Let’s Encrypt now issues IP address certificates, limited to public addresses it can reach for validation. A router at 192.168.178.1 can present only a certificate its owner chose to trust, so the vendor has to give the router a public name.

All of the pieces of this puzzle have been public for years.

Year What happened
2011 CA/Browser Forum phases out public certificates for internal names and reserved IPs
2012 Our book Securing the Virtual Environment shows how certificate warnings and operator fatigue are among the easiest attack vectors
2015 Plex issues every home media server its own trusted certificate
2020 Netgear’s shared router certificates are revoked because the private key is found in public firmware
2024 An attacker registers fritz.box
2026 Let’s Encrypt issues IP address certificates, and FRITZ! still tells users to ignore the warning

Using bland generic marketing for internal name resolution has drawbacks. FRITZ! routers answer fritz.box from their own resolver, and .box is a public top-level domain. At the end of January 2024 an attacker registered fritz.box, and heise reported that the domain redirected to various websites and intercepted SSH connection attempts. AVM, now FRITZ!, won control of the domain through WIPO arbitration in May 2024. Because, duh. But also, why didn’t they think of this before? The name the vendor thought of as private had a public registration sitting open.

Netgear, by comparison, tried to shortcut the safety of real public names with a certificate. It procured a certificate for routerlogin.com and shipped the private key in its router firmware, where anyone could extract it from a router or from the firmware downloads on Netgear’s own site. The certificates were revoked after the key was exposed for being in the public. A shared key makes every router the same router, and anyone holding the firmware can impersonate it.

Plex solved the same problem in 2015 for home media servers. Plex bought DigiCert certificates for every server. Each server holds a wildcard for its own hash-named subdomain of plex.direct, and a name such as 192-168-1-7.hash.plex.direct resolves to the LAN address written into it. The key is only on a single device and the operator’s browser can verify the certificate against the name.

A router has the easier version of this, so it makes no sense why router vendors are delaying the proper solution. Plex had to tell users to switch off DNS rebinding protection, because resolvers filter public names that point at private addresses. The router is the actual resolver that DHCP assigns to every client on the network, so it can answer its own name with its LAN address directly.

Any vendor could ship this today, and regulators should demand it already.

  1. The router generates its own key pair on first boot and keeps the private key on the device.
  2. The vendor assigns each router a random, unique hostname under a domain the vendor owns and answers the DNS-01 challenge in its own zone, so the router stays closed to inbound traffic.
  3. The router’s resolver answers that hostname with the router’s LAN address.
  4. The friendly name and the bare IP serve a plain HTTP redirect to the unique HTTPS name, and the login form appears only there.
  5. The router renews through the vendor automatically and falls back to its own individual self-signed certificate if renewal fails.

FRITZ! is sitting on all the pieces and just needs to put them together. Each box that registers with MyFRITZ! receives a random, unique address ending in .myfritz.net. Since FRITZ!OS 7 the box can obtain a Let’s Encrypt certificate for that address and renew it on its own. Each box also carries an individual self-signed certificate, so FRITZ! avoided Netgear’s shared key.

FRITZ! currently points at the Internet. The Let’s Encrypt certificate requires a MyFRITZ! account, and the box opens a port to the internet during validation. AVM’s release notes state that HTTPS to fritz.box or the local IP keeps the self-signed certificate.

The FRITZ! example is useful because the change is so trivial for them and so powerful for their customers. AVM issues the myfritz.net names under its own domain, so it can answer DNS-01 for every box in its own zone. The certificate belongs in the default setup, separate from remote access, with the box answering its myfritz.net name with its LAN address and fritz.box serving a redirect to it.

First setup remains the one exception. A new router reaches setup before its WAN connection is up, and a certificate installed at the factory would expire (public certificates are already capped at 200 days and drop to 47 days in March 2029). So the first session would be on the box’s own self-signed certificate, and the vendor can print its fingerprint on the label next to the Wi-Fi key it already prints.

Speaking of printing keys, here’s a fun history fact: when I met with 3Com in 1999 to evaluate their very first ever WEP-capable access point, the plastic case had the passwords embossed into it (AirConnect at Networld+Interop). Literally four hard-printed passwords in the white plastic. “What were you thinking” I asked with my mouth open in shock. “Nobody will ever use WEP if it’s not easy enough” they said. I’m not sure they were right, but I remember one of the worst concepts of secret keys ever. See also CVE-2001-0352.

SNMP agents in 3Com AirConnect AP-4111 and Symbol 41X1 Access Point allow remote attackers to obtain the WEP encryption key by reading it from a MIB when the value should be write-only…

Like, nobody at 3Com said “maybe a sticker would be better than designing the hardcase mold for the same four WEP keys to be fixed forever for everyone”. Talk about planned obsolescence. Sad history fact: 3Com is no more.

FRITZ! gets the Internet side right but still hasn’t closed the gap on the home network. They have had the solution since FRITZ!OS 7 and it’s long past time to get it done right, the easy way.

Wiz at it Again: Low Risk, High FUD of PHP2Shell

Public evidence for PHP2Shell has turned out to be far thinner than its headlines. It’s a bad bug, yes, but who caused so much attention to it instead of other bugs?

Wiz frames the reach of the flaw as 70% of the internet, and its write-up says the impact “cannot be overstated.” OpenAI called it one of the most impactful security issues in recent memory, a line that Fortune carried.

This is a pattern. In 2023 I noted Wiz’s Microsoft AI leak report was technically sound and overstated the risk to sell the product. OpenAI has become synonymous with overstatement and bellicose self-promotion. Weren’t they recently warning us the world already would be ended?

Attempts at compromise, versus actual compromises, are separate numbers. No vendor has published a confirmed compromise, and the exploit experts I have spoken to have seen none. TechTarget reports Cloudflare seeing attempts in the thousands to millions, and Amazon honeypots logging probes from 17 October. Recorded Future says the public evidence leaves the full extent of successful exploitation and confirmed compromises open.

The public proof-of-concept code is weaker.

SC World quotes a developer who tested the three most-starred GitHub repositories against a vanilla PHP server. Once again proving, as an aside, that stars are a worthless measure. Each “starred” code failed to reach code execution. Chainguard’s CEO told TechTarget that many of the public exploits were fake or were removed before they worked. Recorded Future adds that the author of the bundle later invalidated his own theory.

The leaked bundle that set off the panic was, in Kevin Beaumont’s reading, machine-generated and only half right, and the copies on GitHub that built on it inherited those mistakes.

The advisories explain part of this. Both CVE-2026-109618 and CVE-2026-109619 rate attack complexity as high (AC:H). Calif’s write-up shows why. The chain needs control over memory layout and, on PHP-FPM, cooperation from the application.

Compare that with a bug I reported to Qdrant that was just disclosed this week.

GHSA-frc7-44gm-99qq carries a CVSS score of 9.8, with attack complexity low, no privileges required, and no memory tricks. I reported it to the vendor in February and then again formally through the German BSI on 9 July, and it was published on 22 October.

I should point out PHP’s advisories use CVSS v4.0 and Qdrant used the old v3.1, which means the comparison depends on an attack complexity field. I mean, aside from the fact that the Swastikas run on Qdrant and the whole world runs on PHP.

Patch history

The history of patching is always a great story.

In January 2003, Slammer was a single UDP packet carrying 376 bytes against a Microsoft SQL Server bug patched six months earlier (MS02-039, 24 July 2002). It reached 90% of vulnerable hosts in ten minutes, doubling every 8.5 seconds. There were no funny memory tricks and no second request. The attack complexity was zero, so the whole story was six months of customers left vulnerable by (failing to patch from) the richest company in the world.

In May 2017, WannaCry ran on a Windows bug patched two months earlier (MS17-010, 14 March 2017) and shut down hospitals and factories that had not applied it. In June 2017, NotPetya ran on the same patched bug. That same spring, Equifax was breached through an Apache Struts bug whose fix (S2-045) had shipped in March; GAO put the exposure at least 145.5 million people.

In December 2021, Log4Shell was one line in a text field. The fix shipped in days, and the Cyber Safety Review Board called it an endemic vulnerability whose instances “will remain in systems for many years to come, perhaps a decade or longer.”

Every one of these had the kind of easy exploit that almost anyone could run. PHP2Shell does not rise to this level, yet. That is the difference between a bad week and a bad year, and so far PHP is getting a lot more attention than if AI hadn’t tried to convince everyone it was the only bug in town.

PHP history

PHP has been here twice before, three times if you know the secret.

In May 2012, CVE-2012-1823 leaked by accident from PHP’s own bug tracker. The rushed fix was broken within hours and the second fix followed a week later. Scanners still probe for it. In June 2024, CVE-2024-4577 was patched on a Thursday and in ransomware by the weekend. Both were argument-injection bugs, one request, again no memory layout to manage.

The secret is the lineage Calif lays out. Stefan Esser found the first PHP memory_limit remote bug in 2004 and withheld his exploit. In 2020 a report of long multipart variables causing out-of-memory conditions sat in the tracker as a minor bug. In 2024 an OSTIF and Quarkslab audit found a related issue, could not prove exploitation, and it went unfixed.

And so a very old bug was turned it into code execution in August 2026 with an AI assistant and a history that said where to look.

Twenty-two years from first sighting to patch. The hard part was never finding the bug. It was proving there is an exploit, and then getting one to work, which is where everyone except the discoverers still sits today.

Patching now

Patching is the gap that’s easy to measure. Censys counts 3,082,629 hosts and 56.7 million web properties running PHP. Its dashboard at php2shell.censys.com updates hourly. In the latest update, PHP 7.4, 8.0 and 8.1 received source-only backports with no compiled release. The older branches stand as follows.

PHP branch Patch status
7.4, 8.0, 8.1 Source-only backports, no compiled release
5.4 through 7.3 Nothing

Given all that, if you ask me, the risk is low. The potential is high, higher than before AI made a big deal about it, still not as high as other bugs getting no attention at all.

Who Put Ebury on the Tor Exits to Pop SSH?

An RSA key opened a Tor exit server at 04:38 UTC on 28 August 2026. It wasn’t in the authorized_keys file. Surprise?

Dan Goodin at Ars Technica ended his reporting of 333 hijacked exits on this silent key, calling it a last perplexing finding. The relay operators called it an open question on the tor-relays list.

But I looked at their own post and see an answer.

Ebury loads keys from memory. ESET documented the method in October 2017: on a backdoor connection the implant sets sshd’s AuthorizedKeysFile to /proc/self/environ and hooks open(), so the attackers’ RSA public key is read from the process environment and authorized.

The authorized_keys file is bypassed. The operators pulled the implant configuration from process memory on their own servers and printed it: AuthorizedKeysFile /proc/self/environ, UsePAM no, LogLevel QUIET, HISTFILE=/dev/null. A key that logs in with that configuration in place leaves exactly the trace we see here, an accepted login and an empty file.

So, it stands to reason, the key worked silently because Ebury was already inside sshd on Switzerland2 when the key arrived. The flow of events in UTC:

03:19 Attacker logs in to the management server with the operator password, correct first try
03:25 Ebury 1.8.3 installed on the management server
04:33 Two SSH connections to Switzerland2, dropped before authentication
04:38 Switzerland2 accepts RSA key SHA256:dvACokMYcXJ2om4wuS+TvdtjrAgPK7rC7ZQHDgvBHzk
04:38 +1s sudo decodes base64 into perl and installs the same Ebury build on Switzerland2

The install they can see came one second after the login. The login needed trust that was already in place, so whatever granted it was there first. On the backdoor reading, the sudo at 04:38:01 was a reinstall onto a host the attacker already owned, run with the operator’s sudo password because they had it.

The operators themselves seem to arrive here as well. They give two possibilities, a line in the main sshd_config or an earlier backdoor, and then report that the main sshd_config came from the hosting provider’s VPS image and that the attacker replaced it on 30 September. Both of their possibilities say trust was established on Switzerland2 before 28 August, by someone other than the operators.

The hosting layer is where Ebury has lived for a decade, and in this case it has a name. Switzerland2 was a FranTech VPS. An analysis of public directory data posted to tor-relays counts the relays of the 58 other affected operators: 71 of 101 on FranTech switched to the attacker’s policy, against 13 of 9,054 hosted everywhere else, and each of those operators had their affected relays all on FranTech or all off it. Kaba counted 26 of the 27 compromised servers outside the Quetzalcoatl family at FranTech and asked the list whether they were compromised at the hoster level. ESET’s May 2024 paper documents hosting providers whose infrastructure was compromised so that Ebury landed on the servers they rented out. ESET rented one and had Ebury within seven days. One provider lost 70,000 servers in 2023. Tor exit nodes are on ESET’s victim list.

Kaba, who found the 333 exits by scanning every exit with exitmap, names the hosting company as the open theory.

Marc-Etienne Léveillé, who investigated Ebury at ESET for more than a decade before leaving the company, told the list that version 1.8.3 has circulated since at least June 2026, that ESET has seen providers whose management servers ran Ebury and whose customers’ virtual servers ended up infected, and that with 400,000 servers compromised since 2009 there are most likely other compromised relays still running.

Every check the operators ran on the host passed. dpkg -V passed because the packaged library stays intact and only a symlink moves. ldd on sshd passed because the implant loads through libkrb5 into sshd-session. A shell reported a clean /proc/self/maps because the implant hides from the processes it lives in. One of their review passes declared the server clean on those results. The true findings came from outside the host: the public CollecTor archive showed the exit policy had changed, process memory showed the implant, and the Tor network health team saw the policy change in the directory data before the operators saw anything on their own machines.

The password reuse is the stated root cause. It explains 03:19 and it explains 04:38:01. It has nothing to do with 04:38:00. That second belongs to a backdoor that predates the password, and the burning question now is who put it there.

PHP2Shell: ini won’t save you

Update, 16:00 CEST

PHP published both advisories at 14:00 UTC. Both are rated 9.2 with high attack complexity. CVE-2026-109619 needs a persistent worker (mod_php or PHP-FPM) that serves more than one request.

Fixed versions are 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Censys later reported source-only backports for 7.4, 8.0 and 8.1 with no compiled release, and nothing for 5.4 through 7.3.

CVE-2026-109618

Memory corruption in how PHP recovers from running out of memory. A multipart request can push a worker past memory_limit during request startup, before any script runs, and the recovery path leaves freed memory in use.

PHP confirmed unauthenticated code execution against mod_php with default settings. The configuration controls PHP lists are the ones in Step 3 below: block multipart at the front end if the application does not use it, enforce the decoded body size there rather than in PHP, which checks post_max_size only against the declared length, and limit input variables, parts and headers.

One is new and counterintuitive: setting memory_limit=-1 removes the out-of-memory event the bug depends on, and PHP calls it the primary configuration mitigation while saying it is incomplete.

CVE-2026-109619

Stale global state surviving a fatal interruption. One request dies on an out-of-memory or fatal error and leaves pointers into a freed heap; the next request on the same worker touches them before initialization. PHP names session upload progress as one of the paths in. PHP’s own advisory recommends session.upload_progress.enabled=0 and suggests leaving it off after patching.

Patching is the only complete mitigation.


PHP ships fixes for two critical bugs at 16:00 CEST today, 21 October, and the only thing really confirmed about them so far is the high score.

The short version

The people with the least PHP expertise sit most exposed in the gap between a vulnerability leak and patch. The controls below are free and won’t take long.

Details

Most of the web runs on PHP and the first bug sits in the multipart upload parser, a front door that PHP opens before any site code runs. An attacker does not need to know which site they are hitting or even the actual software it runs. They send crafted uploads to any server they can find. The second bug needs two requests to the same worker, one to leave corrupt state behind and one to use it. Anything still on old, unsupported versions of PHP, which is a lot of small businesses and forgotten boxes, has no fix being offered. This is how to lose control to attackers. So let’s talk about what to do.

Known knowns

This much has been confirmed by PHP: the notice on php.net announces two critical vulnerabilities, both rated CVSS v4.0 9.2, and a request that everyone prepare to upgrade immediately after a 16:00 CEST announcement. PHP reserves the right to release earlier if details leak or exploitation starts. Their notice gives no CVE IDs, no component, and no affected versions.

Reported but unconfirmed by PHP: the identifiers CVE-2026-109618 and CVE-2026-109619, and a link to session.upload_progress handling in ext/session. CERT-EU’s advisory says explicitly that it rests on leaks. GMO Flatt Security says its own research overlaps with the reports and warns the write-up may be inaccurate. Huntress reports that watchTowr reproduced CVE-2026-109618. A public exploit exists, and its author claims a second path through the multipart parser that no ini setting closes.

Attacks: Cloudflare’s CTO reported early attacks at 07:43 UTC and shipped stopgap WAF rules to all plans. Huntress had seen none as of its 20 October post. By the afternoon, watchTowr reported active exploitation attempts of both CVEs. Tweakers, the Dutch House of Representatives, the Dutch NCSC and Security.NL took their sites offline at 10:35 UTC as that has been the growing “best practice” in the wait.

Disputed: a GitHub repository posted this morning claims the use-after-free described in the public bundle does not exist in released PHP. PHP’s advisory for CVE-2026-109619 and Calif’s write-up settle this. Calif places the second bug in php_session_rfc1867_callback, the function the public bundle named, so the audit’s claim that no such bug exists does not hold. The bundle’s claim of a second path through the multipart parser also matches Calif’s first bug. Alexandre Daubois of the PHP security team adds that the upload-progress flag belongs to the second bug, and that the CVE-2026-109618 advisory never mentions sessions.

The server API is what seems to decide. On Apache mod_php both bugs give code execution against arbitrary PHP scripts. On PHP-FPM a startup failure kills the worker, so the first bug does not carry over, and the second needs an application that decodes serialized data after startup, such as WordPress through maybe_unserialize() or phpMyAdmin through session_start(). Calif confirmed pre-auth code execution against WordPress, Drupal, Joomla, Magento, Nextcloud, phpMyAdmin and others.

Its partial mitigations:

  • PHP-FPM, disable session upload progress and set memory_limit=-1
  • mod_php, set memory_limit=-1

Removing the memory cap of course raises the risk of memory exhaustion, so patch.

What now?

Step 1: Inventory

We patch what we know needs it. List every host, container image, and shared-hosting account running PHP, with the version and SAPI (mod_php, PHP-FPM, or CGI) for each. Include the places people forget: CI runners, staging boxes, old WordPress and Joomla installs, vendor appliances with embedded PHP, Windows software bundling its own PHP, and base images in your registry.

PHP supports only 8.2 and later. Huntress names PHP 5, 7, 8.0 and 8.1 as the versions that need upgrading or isolation. CERT-EU advises isolating internet-facing applications from public networks where patching is delayed.

Step 2: Be skeptical about the ini setting

Two people inside the PHP project have said ini mitigation advice being spread is NOT a mitigation.

Core developer Gina Peter Banyard, replying to the suggestion at 02:13 UTC, wrote that the only mitigation is patching or upgrading PHP, and added that taking a server offline for a day is the most sensible option for anyone who can afford it. Release manager Joe Ferguson put it shorter:

ini settings won’t save you

Set session.upload_progress.enabled = 0 anyway. It’s easy and closes the second bug’s consumer path on PHP-FPM. Upload progress tracking only drives progress bars, which WordPress, Magento and most applications never use anyway. The PHP manual lists the setting as on by default and changeable only per directory, so ini_set() in application code has no effect on it. Put the line in a drop-in under the web SAPI’s conf.d (for example /etc/php/8.3/fpm/conf.d/99-no-upload-progress.ini), restart PHP-FPM or Apache, and verify from a web request. php -i shows the CLI configuration, which is separate. On shared hosting use the control panel’s php.ini editor or a .user.ini, which should take just minutes to apply.

Step 3: Keep multipart away from PHP

The multipart parser runs on every multipart/form-data POST before any application code, so the one configuration change that keeps PHP out of that code entirely is refusing those requests at the web server.

  • For nginx, in every server block that passes to PHP:

    if ($http_content_type ~* “multipart/form-data”) { return 403; }

  • For Apache: a RewriteCond on %{HTTP:Content-Type} matching multipart with a [F] rule.
  • For Fastly, a simple recv VCL snippet:

    if (req.method == “POST” && req.http.Content-Type ~ “(?i)multipart/form-data”) {
    error 403 “Forbidden”;
    }

    An edge requires an origin to refuse connections from everywhere else: lock the origin firewall to your CDN’s published IP ranges.

This breaks every file upload on the site, including the WordPress media library, but it buys time until the patch lands.

If you can go further, allow only GET and HEAD, which leaves the site readable and closes every write path. If you can go further still, stop PHP-FPM and serve a static maintenance page. The maintenance modes inside WordPress and Magento do not help, because PHP parses the request before the application sees the flag.

If uploads must stay open, narrow them: reject multipart on every path except the upload endpoints, set a conservative body cap (for nginx, client_max_body_size) everywhere else, and put admin and internal applications behind a VPN or an authenticating proxy. One rule costs nothing: block any multipart POST whose body contains the field name PHP_SESSION_UPLOAD_PROGRESS, which legitimate traffic never carries. Treat it as a tripwire for one reported path, since a WAF parses multipart bodies differently from PHP.

Signature-based WAF rules for this bug are guesses. Cloudflare only covers traffic that reaches the origin through Cloudflare. Use edge rules to shrink the attack surface until the patch lands.

Step 4: Depth to defense

A memory corruption bug simply opens the door, and so there is also defense work inside to be done.

Disable the functions you leave unused via disable_functions (system, exec, passthru, shell_exec, proc_open, popen). A memory-corruption exploit can sometimes bypass this, so it raises the cost without guaranteeing safety. Run each PHP pool as its own unprivileged user, keep web roots read-only for that user, and restrict outbound network access from web servers so a shell has nowhere to call home. Confirm PHP workers can only read the credentials they need.

Step 5: Watch for exploitation

Evidence of attempts is visible without knowing the exact bug.

Crashes and fatals: repeated worker segfaults in the PHP-FPM log (child exited on signal 11) or Apache error logs, and clusters of Allowed memory size exhausted or Maximum execution time exceeded fatals from one source. Both bugs start from a request that dies mid-flight, and the second needs a follow-up request on the same worker.

Request patterns: bursts of multipart POSTs to endpoints that take no uploads, followed by a second quick request from the same source. The upload-progress field lives in the POST body and never reaches the access log, so look at who is POSTing to what.

Process behavior: the PHP or web-server user spawning a shell, sh or bash as a child of php-fpm or apache, or making unexpected outbound connections. On Linux, an auditd or EDR rule on execve from those parents is cheap and high-signal. Huntress points to Florian Roth’s process-watcher as a free option.

Persistence: new or recently modified .php files in upload and cache directories, new cron entries for the web user, and unfamiliar SSH keys. Compare against a known-good deployment hash if you have one.

Keep the logs. If you find something after the patch, you need to look before the patch.

Step 6: Patch when the release lands

The fixed version numbers arrive with the 16:00 CEST announcement. Version numbers in pre-release analysis were unconfirmed until php.net published them, and the ones circulating turned out right.

Your distribution’s tracker matters more than the php.net version number. Debian, Ubuntu, Red Hat and the official Docker images backport fixes under their own package versions, and none of them had a page for either CVE as of 20 October. If your PHP comes from Remi’s repository, the PHP Foundation says it will not update today; plan on distribution packages or a source build.

When the release ships: read the official advisory for the real affected component, upgrade, fully restart PHP-FPM or Apache, and confirm the running version from a web-served request rather than the package manager. For containers, rebuild images and redeploy rather than patching in place. Managed hosts will lag. Ask your provider for a date today.

After patching, remove the multipart block and re-enable upload progress only if something needs it.

PHP has been here before

PHP has twice seen this before. In 2012 a request-handling bug (CVE-2012-1823) leaked by accident from PHP’s own bug tracker, the rushed fix was broken within hours, and attackers have hunted it ever since. In 2024 a cousin of that bug on Windows (CVE-2024-4577) was patched on a Thursday and in ransomware by that weekend. Both were simpler than this week’s bug. The pattern is still that a fix arrives and attackers move faster than the people who run the servers, and the conversation is all about widespread infrastructure code nobody maintains.

The ancestor is older. By Calif’s account, Stefan Esser found the first PHP memory_limit remote vulnerability in 2004 and withheld his exploit. In 2020 a researcher reported bug #78876, long multipart variables causing out-of-memory conditions and uncleaned temporary files, and little came of it. In 2024 an OSTIF and Quarkslab audit rediscovered a related out-of-memory issue, exploitation was not proven, and it went unfixed. In August 2026 Calif found the paths that turn it into code execution, reported the first bug on 11 August and the second on 21 August, and PHP shipped fixes on 21 October. The earliest affected path Calif found dates to PHP 5.4.0, released in 2012.

Updated 16:00 CEST with the published advisories, above.