PHP, Node, eMudhra: Rebuild the Base, Not the App
Between 17 and 22 October 2026 PHP shipped 8.5.12, 8.4.27, 8.3.36 and 8.2.35 for two unauthenticated remote code execution bugs, Node.js patched the Brotli decoder it bundles and three TLS hostname checks, browsers blocked eight eMudhra intermediate CAs whose private keys fit in a single byte, and GitLab published its second CVSS 10.0 in nine days. None of it was application code. The DevSecOps test is two numbers: how fast you can see what sits under each image, and how fast you can rebuild the base and roll every one of them.
“These flaws are in the PHP runtime, so updating website code or a plugin(s) alone does not fix these vulnerabilities.” Cloudflare wrote that on 21 October 2026, announcing WAF rules it had switched on at 03:02 UTC, by its own count about nine hours ahead of public disclosure and nearly eleven before the release itself. Change the nouns and the sentence covers the whole week. On 17 October at 18:05 UTC a security researcher reported to Google's Chrome Root Program that eight eMudhra intermediate CA certificates had weak private keys; Chrome had a CRLSet block live at 19:45 UTC and Firefox finished its OneCRL push at 22:59 UTC. On 21 October at 14:00 UTC PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fixed CVE-2026-109618 and CVE-2026-109619, a day early after the ids leaked. The same day Node.js v26.12.1, v24.22.1 and v22.23.4 fixed ten CVEs including a Brotli decoder out-of-bounds write, and GitLab 19.5.1 fixed CVE-2026-110264, its second CVSS 10.0 in nine days. On 22 October eMudhra said it was leaving the global TLS certificate business.
Four runtimes, one shape: the fix sat in the interpreter, in a decoder compiled into the runtime, in the CA bundle baked into the base image, or in the CI server, and a dependency scan of the application repository saw none of it. That leaves two numbers to measure. Time to inventory: how long until you can list every running image by PHP version and SAPI, by the Brotli inside its Node, by the age of its trust bundle. Time to roll: how long from the vendor's release until the last image on the old base is gone. Amazon's threat intelligence team gave the second one its unit on 21 October: “For critical vulnerabilities in internet-facing systems, measure patch and mitigation timelines in hours.”
The PHP fix lives under the framework, and each flag closes one path
Both bugs live in how the interpreter sets up and tears down a request, and neither needs a flaw in the application. The PHP project scores both CVSS v4 9.2, Red Hat rates them Important at CVSS 3.1 8.1, and NVD's v3.1 scores are 9.8 for CVE-2026-109618 and 8.1 for CVE-2026-109619. The 10.0 that circulated on 20 October was, in the PHP Foundation's words, “a wrong CVSS score of 10.0 (the actual score is 9.2)”. In CVE-2026-109618, PHP abandons a request on an out-of-memory error with longjmp(), and a failed allocation during request setup leaves state that shutdown later trusts. In CVE-2026-109619, persistent worker globals outlive the request memory they point into, and one request plants state for the next one on the same worker. One confirmed consumer is session upload progress, which the advisory says “is enabled by default, and no application call to session_start() is required.”
- Which SAPI you run changes the risk. php.net: “We consider the risk highest for mod_php, with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs.” No complete FPM exploit has been demonstrated for CVE-2026-109618, and Red Hat explains why: “PHP-FPM workers exit on early initialization failures before reaching the corrupted structures”.
- Each flag closes one path.
session.upload_progress.enabled=0blocks the confirmed session paths of CVE-2026-109619 and nothing in the other CVE; a post written from inside the PHP security team, by its own account, advises you to “ask which code path a flag closes before you switch it off”. One hardening habit backfires: “Do not substituteRLIMIT_ASorRLIMIT_DATA, as these can cause the vulnerable allocation failure.” - 8.1 and older get no release; fixes exist anyway. PHP's security team provided minimal source backports for 7.4, 8.0 and 8.1 for people building from source, Ubuntu's USN-8948 notices cover jammy's php8.1 as a standard update with focal and older behind Ubuntu Pro, and Red Hat's own fixes stop at RHEL 8 (its bulletin lists RHEL 7 as affected by CVE-2026-109619 only, with no advisory for it). The backport you depend on, and the paid tier it sits behind, belong in the inventory.
Attempts at scale are real, and no compromise is confirmed
The sources disagree; repeat them as they stand. Amazon's MadPot honeypots logged an attempt on 17 October, four days before disclosure, and proof of concept traffic from about 02:00 UTC on 21 October. The post adds that “it’s unclear if any of these attempts would have succeeded in real-world systems”. watchTowr “has observed exploitation of both CVE-2026-109618 and CVE-2026-109619 in the wild” from its honeypots, the one standing claim of its kind. CISA “has not observed exploitation of these vulnerabilities as of the time of publication”. Rapid7 “has not observed exploitation in the wild, though there are multiple reports of exploitation attempts”. Sansec: “The public exploits do not work against a default PHP installation.” Sophos reported one successful exploitation at 16:18 UTC on 22 October and corrected itself at 21:21 UTC: “no evidence of a successful compromise due to these vulnerabilities”.
The disclosure frayed at the end. The PHP Foundation's timeline has the CVE ids circulating from about 15:00 UTC on 20 October and a proof of concept video published by mistake that evening. Then, “On October 21st at 07:41 UTC, more than six hours before the coordinated release”, Chainguard “broke the embargo and published built images containing the fix.” Chainguard's chief executive, as The Register reports him, does not dispute the timeline and says the company shipped because people were being hit before the disclosure; Techzine reports him denying a leak. Debian's advisory went out at 13:45 UTC, fifteen minutes before the release, and the Docker library's php Dockerfile commit was authored at 13:44 UTC, with the official-images merge that builds the Hub images following at 15:16 UTC: coordinated, and proof that a distribution package and an image definition can be ready the hour the fix lands.
Node shipped a decoder that was on nobody's dependency list
Node labels CVE-2026-111497, “Node.js Brotli decoder out-of-bounds write permits remote code execution”, High. Google, the numbering authority for Brotli, published the record on 22 October at CVSS v4 9.2 Critical for Brotli 1.0.3 before 1.0.10, 1.1.0 before 1.1.1 and 1.2.0 before 1.2.1. The decoder is compiled into Node; a package level SBOM that records node 24.22.0 has no line for it. Aikido's write-up of 22 October lists the reach: untrusted input to node:zlib's Brotli functions, fetch(), which decompresses br responses on its own, and Express 5 or body-parser 2.x decoding a body sent with Content-Encoding: br. Node took the fix as a cherry-pick of one upstream commit dated 27 February 2026 with no security wording in its message; the bug was reported to Node by OpenAI Codex Security, and Google's CVE record followed on 22 October, a day after Node's release.
- Three hostname checks were wrong. CVE-2026-105483 (High, 26.x and 24.x) is a bypass when an IP is written in fullwidth digits, CVE-2026-106069 (High) when a host loosely resolves to an IP address, and CVE-2026-106072 (Medium) lets DNS SANs authenticate non-canonical IPv4 endpoints.
- End of life lines got nothing. Node's schedule ended v20 on 30 April 2026 and v25 on 1 June 2026. Vercel: “Node.js 20 reached end of life and will not receive this fix”; projects on it “must change their Node.js version to 22.x or 24.x and redeploy”. Its WAF rules went in front of every project the same day, with the limit stated: “It does not cover responses your functions fetch from other servers.”
A trust store is a dependency with a version and an owner
eMudhra's incident statement says that in a key generation ceremony on 30 April 2025 “a key wrap/unwrap step produced a second, weak set of keys alongside the original strong keys”, and that “The issue went undetected for about 18 months, through two independent audit cycles.” Scott Helme recovered all eight P-384 private keys on a laptop in under a second by computing 1G, 2G, 3G until the point matched: the key for EM OV TLS CA - G3C is 4. eMudhra revoked them on 18 October and says no end entity certificates were ever issued from them.
Chrome and Firefox were safe within three hours of their reports because both push blocklists to running clients. Helme presented a forged api.stripe.com chain and found that curl, Python and Node accepted it with default settings on every Linux and macOS machine he tested; for those clients the revocation “doesn’t help them at all”. Go's CVE-2026-111943 record explains that “the system trust stores may not be updated without further action”, hence the crypto/x509 denylist in 1.27.3 and 1.26.10. Node's v24.22.1 ships “tls: reject chains containing blocked public keys” and roots from NSS 3.131.1. Dennis Jackson's tracker lists the fixed versions across languages and distributions, and notes that macOS's /etc/ssl/cert.pem, which command line tools read, is unchanged. Cloudflare, which updated “shared trust stores, base images, and other shared foundations” across its own services, drew the rule: “a trust store can’t be a static list of root certificate authorities”.
- Test the service itself. “A successful browser test does not prove that a background service is protected”, Cloudflare writes, and its guidance is never to clear the error by disabling verification. Present a known bad chain to each client library in CI and fail the build when one accepts it.
- mTLS bundles and the root come next. CA bundles uploaded to load balancers, gateways and VPN concentrators are refreshed by no OS package, and a device that cannot take a new bundle trusts these intermediates until they expire, more than a decade away. Chrome removes emSign ECC Root CA - G3 on 27 October; anyone with a certificate under it needs a replacement from another hierarchy.
Inventory below the application takes minutes if the data exists
CISA's alert asks organisations to “Identify and inventory systems running any version of PHP” and warns that “PHP is often a component of other software and prevalence may not be directly observable.” Palo Alto Networks lists PAN-OS and Panorama as affected with no fix date yet and Cisco names three products with no workaround; neither is aware of malicious use. Your own images deserve the same scrutiny, one layer below the package name.
- PHP: version and SAPI. On Debian and Ubuntu the package name says both,
php8.3-fpmorlibapache2-mod-php8.3; in the official image the tag does,8.3-fpmor8.3-apache. A digest pinned before the official-images merge at 15:16 UTC on 21 October lacks the fix. - Node: the runtime version and what it bundles.
node -p process.versions.brotliprints the Brotli compiled into a binary, which package level SBOMs do not list; judge patched status by the Node version against the 21 October releases. - The trust bundle: a package with a release date.
ca-certificatesappears in every distribution SBOM, fixed from the 20 October releases on. Runtimes with their own bundle, certifi, Node's NSS roots, Java's cacerts, each need a line. - The CI server is a runtime too. GitLab fixed CVE-2026-97229 (CVSS 10.0, versions 19.2 to 19.4) in 19.4.2 on 12 October and CVE-2026-110264 (CVSS 10.0, every version from 7.0) in 19.5.1 on 21 October, with no exploitation statement about either.
The script below reads each image with syft instead of running it and asks any Node binary what it bundles. Producing the SBOMs is the subject of the shifting security left guide; this is what to ask them on a day like 21 October.
#!/usr/bin/env bash
set -euo pipefail
# INVENTORY: read the image itself.
# syft 1.x, one image per line.
pat='^(php|libapache2-mod-php'
pat+='|node|ca-certificates'
pat+='|libbrotli)'
while read -r img; do
echo "== $img"
syft scan "registry:$img" \
-q -o json \
| jq -r --arg p "$pat" '
.artifacts[]
| select(.name | test($p))
| "\(.name) \(.version)"' \
|| true
# SAPI: php8.3-fpm or
# libapache2-mod-php8.3 says which
# BROTLI: built into node, absent
# from the package SBOM. Ask node.
docker run --rm --entrypoint node \
"$img" -p \
process.versions.brotli \
2>/dev/null || true
done < images.txt
# Fixed: php 8.5.12 8.4.27 8.3.36
# 8.2.35; node 26.12.1 24.22.1
# 22.23.4; ca-certificates 20 Oct on
Version banners do not move when a distribution patches
Ubuntu's fix for noble is php8.3 8.3.6-0ubuntu0.24.04.13. The upstream part stays 8.3.6 before and after, as Ubuntu's stable update policy normally requires. A scanner that reads only the PHP version string sees the same number on a patched host and an unpatched one. Read the package release.
Watch a fleet leave the old base in hours
Red Hat's bulletin carries the sentence that turns the week into one task: “any customer containers should be rebuilt when the base images are updated.” Press Rebuild the base below. One service image is drawn as its layers, application code at the top and operating system at the bottom, with the PHP runtime and the trust bundle lit as faults; beside it sit the eighteen images built on that base, against a clock set to 21 October. Try Patch the app first and watch the faults stay lit, then bump the base, pick how the fleet rolls, and read off the hours spent on the old layers.
The gap between the two policies is pipeline design. An automated rebuild means the base bump is a commit that triggers every dependent image, each build reproducible from a pinned digest, a canary taking traffic first, and a redeploy that needs nobody awake. A maintenance window means the same commit waits for a calendar. Edge rules buy the hours in between: AWS wrote of its own rule set that “Patching is the only complete fix, and you should upgrade PHP as your first priority”, and listed what the rules miss, among them CVE-2026-109618 itself and headers inside multipart parts, which the firewall cannot parse.
Takeaways
- Every fix in the 17 to 22 October wave shipped as a new interpreter, a decoder inside Node, a trust bundle or a GitLab build; nothing committed to an application repository closed any of them.
- Each php.ini flag closes one confirmed path and
RLIMIT_ASopens one; the mitigations buy hours while 8.5.12, 8.4.27, 8.3.36 or 8.2.35 reaches every worker. - An SBOM that records
nodeand stops cannot see Brotli 1.0.3 to 1.2.0 compiled into it, and a banner reading 8.3.6 on Ubuntu noble cannot tell patched from unpatched; query package releases and runtime versions. - Chrome and Firefox were safe within three hours because a blocklist reached running clients; curl, Python, Node, Java and Go needed a new bundle, a denylist or a release, and a service has proved itself when it refuses a known bad chain in CI.
- Make the base image bump the unit of work, every dependent image rebuilt and rolled behind a canary from one commit, and report patch latency in hours.
How long would your fleet sit on the old base?
We wire base image bumps into automated rebuilds of every dependent image, put runtime, bundled library and trust store versions into an inventory you can query in minutes, and add a known bad chain test to CI so a background service proves itself the way a browser does.
Measure your patch latency