If your company has a website or an app, this probably concerns you. Talk to your technical team, or whoever maintains your website, today and ask whether they are aware of these vulnerabilities. Better a phone call today than a scare tomorrow.

It looks like we are in what some are already calling security’s black October. And honestly, with everything that has happened these past few days, that is no exaggeration. It also looks like this is going to be the norm in the age of AI.

On Tuesday afternoon we learned from a tweet that PHP, the language that WordPress and a good part of the world’s websites run on, was going to release a patch for two serious vulnerabilities the next day. No details, just a score of 9.2 out of 10 and some bad news: older versions would not get a fix. We look after quite a few WordPress sites, so we went on alert (and started sweating).

That was the first warning, but not the last. On Wednesday it was Node.js’s turn, the engine behind a huge number of applications and APIs. Almost at the same time, research came out explaining how to take down servers that use HTTP/3, the most modern version of the protocol websites are served with. And, as if that were not enough, it emerged that eMudhra, one of the organizations that guarantee a website is who it says it is, had keys so weak that anyone could forge certificates for any domain.

Four fronts in two days. In this post we tell you how we handled such a serious crisis with the help of AI agents, which decisions we made ourselves and what we learned. The first part is for everyone. The second, for the technical folks.

What is at stake

Without getting technical: in the worst case, the PHP and Node.js flaws let someone from outside run code on a server without needing a username or password. In other words, take it over. The HTTP/3 one can be used to drain a server’s memory and so knock it out of service. And the eMudhra one makes it possible to impersonate legitimate websites, for example to sneak fake software packages in when installing Python programs.

The trickiest part has not been the up-to-date servers, which get fixed by updating, but the old ones. When a version stops being supported, it no longer receives patches. And then there are only two ways out: migrate or switch off.

Agents that know the house

We have been working for a while with agents that know the inventory of our infrastructure. They have limited access and explicit permissions for specific tasks, nothing like an open bar.

And there is another piece that makes this possible: our infrastructure is written as code. How each server is configured, which versions it runs or how each application is deployed does not depend on someone remembering what they changed years ago. Everything is in files, versioned and reviewed like any other code. So changing something means editing a few lines, testing them and applying them. And for an agent, reading and changing code is exactly what it does best.

The division of work has been the same in every case. The agents did the heavy lifting: reviewing every server, reading the advisories and comparing them with our code, taking backups, checking them, applying whatever we approved and documenting everything. We decided what got touched, what got switched off, what got published and in what order.

And the order was the usual one. Backups first, verified one by one. Then test locally, then in a staging environment and finally in production. One server at a time, checking before and after.

Half an hour after the patch

We spent Wednesday with one eye on the news and the other on Twitter, cross-checking what PHP, the vendors and the official agencies, both international and Spanish, were publishing. When the patch came out, shortly before four in the afternoon, we had everything ready.

Half an hour after it was released, all our production servers were updated. There was only one hiccup: one of the staging servers saw so much activity from us that it decided we were an attack and locked us out for more than thirty minutes. The security measures we have configured, working wonderfully against ourselves.

Node.js and HTTP/3 went much the same way. Before rushing, the agents checked how the problem could get into each application. Today we updated what was needed, each project with its own way of deploying.

With eMudhra, in our case the risk lies mostly on the development side, which is where Python packages get installed. Together with an agent we prepared a set of instructions that review a computer without touching anything and, once you give the go-ahead, fix it in the right order. We have published them so anyone can use them, and we have also passed them on to some clients.

Where the mud is

The hard part was the old machines, running PHP versions that were no longer supported. We backed them up and switched them off.

One of the websites they hosted we migrated that same day to a new server, updating the design, the plugins and everything that had gone untouched for years along the way. The agents carried out the migration autonomously while we reviewed and made the decisions. The rest are following their own process with each client.

Teatros Ejemplares: from WordPress to HTML

The most interesting case is Teatros Ejemplares. It is the digital publication of the tribute to Cervantes and his Exemplary Novels made by contemporary playwrights from Spain and Latin America.

The content had not changed since 2016. But there it was, on a WordPress with 21 plugins, and every new vulnerability forced us to patch something nobody was editing. Honestly, it meant keeping a website alive just so it could act as an archive.

So we took the chance to cut our losses and turn it into a completely static website: good old HTML pages. No PHP, no database and nothing to patch.

An agent went through the whole website and saved every page exactly as it looks, with the same addresses, so as not to break links or lose search rankings. Then we cleaned it up: out went the Google Analytics that had not worked for years, and out went the cookie banner, which was only there because of it. In their place, analytics that use no cookies. We checked there was not a single broken link and published it on the usual domain.

From designing the migration to having the website ready to publish took about two hours. And we have not deleted the WordPress: it is still saved as a way back, just in case.

For a website that does not change, the safest thing is to have nothing to run.

More than a week in two days

By hand, all of this would have taken us more than a week. With agents and human supervision we got it done in two days of intense work: PHP patched, the unsupported machines switched off, one website migrated, another turned into HTML, Node.js updated, HTTP/3 reviewed, the development machines protected against eMudhra and the servers up to date.

Not everything is closed. There are websites still to migrate, a service to move off an unsupported version and a handful of clean-up tasks that came up along the way while reviewing everything in depth. But the urgent part is solved.

What we take away

  • Coming prepared is everything. Having the infrastructure inventoried, the backups available and the agents’ permissions clearly scoped is what allowed us to decide and act fast when the warnings arrived. One after another.
  • Infrastructure is better as code. If every server is described in files, changing it is fast, it can be reviewed before it is applied and it leaves a record. And the agents work on something they understand.
  • What you think you have is not always what you have. A service showed up that was not on our list, and some automatic updates that were “enabled” had not updated anything in a long time. Check, do not assume.
  • What is not used is better removed. A website nobody edits does not need a WordPress behind it. Every spare piece is one more piece to patch.
  • Unsupported versions are a ticking time bomb. When a serious flaw arrives, there is no patch. Better to migrate calmly beforehand than in a hurry afterwards.

If the answer to “is your infrastructure inventoried?” is “more or less”, today is a good day to start. And if you want help, write to us.

For the technical folks: how we did it

From here on, the details: identifiers, versions and what we learned along the way.

Infrastructure as code

Everything that follows relies on the infrastructure living in git repositories:

  • Trellis (Ansible) for the WordPress servers: packages, PHP version, PHP-FPM, Nginx, fail2ban, firewall and automatic updates. Changing the PHP source or enabling updates properly was a change in the repository and a provision.
  • Dockerfiles and docker compose for the Node.js applications, with the base image declared in the repository itself. Upgrading Node meant changing a tag or a digest and redeploying.
  • Caddy configuration versioned alongside each project.

Every change is a commit: it is reviewed before being applied, tested first on a local VM and in staging, and if something fails it gets rolled back. The agents read those repositories to learn how each server is set up and propose changes as diffs that we review.

PHP: CVE-2026-109618 and CVE-2026-109619

Two unauthenticated remote code execution flaws, with a CVSS 4.0 score of 9.2.

  • CVE-2026-109618 (GHSA-w8hw-w9fx-j79w): memory corruption when PHP runs out of memory with the request data only half initialized.
  • CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp): part of a request’s state survives freed memory and the next request on the same worker reuses it.

Fixed in 8.5.12, 8.4.27, 8.3.36 and 8.2.35. The 7.4, 8.0 and 8.1 branches only have backports in the source code. Details in the official php.net announcement and, if you use Trellis, in the Roots thread.

How we did it:

  • The Roots mitigation. Roots published a temporary mitigation (session.upload_progress.enabled = Off). The agents reviewed our code and no project used that feature. We kept it ready, but decided to wait for the official release. The PHP advisory later confirmed the mitigation was real, though partial. It also suggested changing memory_limit and post_max_size; we did not apply that because it could break large uploads.
  • Sury instead of the PPA. When the release came out, Ondřej’s PPA, where our servers installed PHP from, was still on 8.4.26 and 8.3.35, but packages.sury.org, from the same maintainer, already had the fixed versions. We switched the source, something Trellis has had on main since August. Watch out: the new role does not remove the PPA file if Ansible created it under another name, and you end up with both repositories at once.
  • Trellis installs PHP but does not update it. With apt_package_state: present, our servers were three versions behind. To patch, you have to provision with apt_package_state=latest.
  • Timeline. First staging at 15:58, after validating on a local VM. Production between 16:02 and 16:16, one server at a time, checking the version, the package source, the PHP-FPM restart and that the websites returned the same HTTP codes. The last staging, at 16:49.
  • The lockout. Trellis rate-limits SSH connections and uses fail2ban. After so many backups and checks, a server locked us out for more than half an hour. We solved it with a persistent SSH connection shared between Ansible and ssh, with no retries.
  • PHP 7.4. PHP released minimal patches in source code only, and the repository for those machines announced there would be no packages that day. That is why we switched them off.
  • The risk is higher with mod_php. We use PHP-FPM, but that was no excuse to sit still.
  • A link to a supposed “PHP 8.4.27 released” went around on the php-net.pro domain, which is not php.net. The reliable sources are php.net, the php-src advisories on GitHub and your distribution’s package repositories.

Node.js: CVE-2026-111497 and friends

The Node.js October security release fixes an out-of-bounds write in the Brotli decoder (CVE-2026-111497, high severity) that can lead to remote code execution, other high-severity hostname validation flaws in TLS and dns.resolve, and several minor ones depending on the branch.

Fixed in 22.23.4, 24.22.1 and 26.12.1. Branch 20 is out of support and gets no patch.

How we did it:

  • Read the code before rushing. The Brotli RCE did not come in through requests: Fastify does not decompress bodies without @fastify/compress, and Express 4 only accepts gzip and deflate. Watch out for Express 5 (body-parser 2), which does decompress incoming Brotli. The real path was the opposite one: fetch/undici decompressing br responses from external services.
  • Check what is actually running. With node -v inside each container. None had been updated, and a third-party tool that was not on the list showed up, with its own Node inside.
  • Each project, its own way. An image without a pinned version: rebuild with --pull. Another with the version pinned in the Dockerfile: change the tag and redeploy. And one with the image pinned by digest and production frozen: a hotfix from the exact commit that was running, changing only the Node digests and checking the diff carried nothing else.
  • Having the version in the Dockerfile is not enough. An unpinned FROM keeps the cached base image unless you rebuild with --pull.
  • Agents as a team. The inventory agent coordinated and split the work among each project’s agents, which know how their project deploys.
  • Still pending. The third-party tool does not yet have a release with a fixed Node, and one service is still on branch 20. A patch will not do for that one: it needs migrating to 24.

HTTP/3: QPACK amplification

FoxIO’s research, dubbed header accordions, shows how with a single unauthenticated HTTP/3 connection a client can force the server to reserve tens to thousands of times more memory than it sends. The problem lies in QPACK, HTTP/3’s header compression over QUIC.

  • CVE-2025-64702: quic-go, the library used by Caddy, Traefik and cloudflared. Fixed in 0.57.0.
  • CVE-2026-12523: Cloudflare quiche. Fixed in 0.29.3.
  • lsquic was fixed in 4.9.0 without a CVE. Bun, the h3 crate and aioquic were still unpatched at the time of publication. IIS, Envoy, ngtcp2/nghttp3 and Google quiche are not affected. nginx does not appear in the research.

How we did it:

  • See what actually serves HTTP/3, that is, what listens on UDP 443.
  • In Caddy, caddy build-info tells you which quic-go version each binary carries. Almost all of them were already fixed.
  • Advertising is not serving. One Caddy carried a vulnerable quic-go and advertised HTTP/3 in the alt-svc header, but the container did not publish UDP 443 and the firewall dropped it. Vulnerable on paper, unreachable in practice. Even so, we upgraded it to Caddy 2.11 and turned off the HTTP/3 advertisement, which was not being used.

Python: eMudhra CA compromised

The eight eMudhra/emSign intermediate CAs had private keys with d < 256, so anyone could issue valid certificates for any domain, pypi.org included. The emSign root is being removed from certifi. Details in the Python Software Foundation advisory.

The catch: pip before 26.2.2 keeps trusting its vendored certifi even when the system is already patched, so updating the system or the external certifi is not enough. Minimums: pip 26.2.2, uv 0.13.4, certifi 2026.10.18 (2026.10.19 recommended), pixi 0.82.1 and sigstore 4.5.1.

How we did it:

  • A prompt to review each machine. We have published it in a gist. First it takes inventory without touching anything: interpreters, venvs, ensurepip, pipx, uv, conda, TLS overrides, Docker, CI and lockfiles. Then, with approval, it updates in a safe order: pip from the wheel with its SHA256 verified and installed with --no-index, and then pipx, uv and certifi.
  • A second opinion. We ran the prompt past another model, GPT-6 Astra with Codex, which confirmed the versions, the wheel’s URL and hash, and suggested corrections that are already included.
  • Watch out for .pyc files. In one Python, pip still reported 26.2.1 after installing the new wheel: the old .pyc files had the same size and date as the new .py files and Python took them as valid. It was fixed by deleting pip’s __pycache__. Check pip.__version__, the code that actually loads, and not just the dist-info.
  • The .trellis/virtualenv folders count too. Each Trellis project has its own virtualenv for Ansible, with its own pip.
  • Still pending. Some old ensurepip versions still ship a vulnerable pip, so a new venv created with them is born unpatched. And Node ships its own certificate store, which gets fixed by updating Node.

Automatic updates that do not update

  • Automatic security updates were “enabled” on some servers, but they were missing the directive that applies them. More than a hundred security updates had piled up per machine.
  • We applied them with a Trellis playbook, tested first on a local VM, with no reboot and no downtime, and left the automatic updates properly configured. Kernel reboots are scheduled for low-traffic hours, and a major MariaDB upgrade we have deliberately kept separate.
  • So that deploys would not fail when cloning the repository, the persistent SSH connection also needed agent forwarding.

And that is it for the technical part. If you want us to review your website or your infrastructure before the next warning arrives, write to us and we will look at it together.