
By Eliot Rabaud
October’s critical flaws, an early Halloween: how not to panic?
In late October my phone wouldn’t stop ringing. Clients, peers, operations teams: everyone wanted to know what to patch, and in which order. To be fair, it was a busy month.
On 12 October, GitLab fixed a flaw rated 10 out of 10 (CVE-2026-97229). On the 21st, PHP rushed out the fix for PHP2Shell, two flaws rated 9.2 that affect every PHP installation serving web pages. The same day, GitLab fixed a second flaw rated 10 (CVE-2026-110264). And on the 27th, Anthropic published the details of an exploit for a Linux kernel flaw rated 9.8 (CVE-2026-98432): taking over a server as root, remotely, with no credentials at all.
Faced with a run like that, the temptation is to treat everything as an emergency, or to start with whichever flaw makes the most noise. The CVSS score doesn’t help much with triage: the four are all between 9.2 and 10, so all critical.
Learning to triage matters all the more for what comes next. The kernel exploit was written by one of Anthropic’s AI models, from a bug that the same research effort had surfaced. With AI now working on offensive research, for defenders and attackers alike, months like October will very likely happen again, and more often. Patching everything in a rush every time won’t hold for long.
We’re going to look at three of October’s critical flaws, the Linux kernel, PHP2Shell and the first GitLab flaw, to understand how to respond constructively using other indicators, starting with EPSS. You’ll see that for comparable severity, their exploitation probabilities range from under 1% to almost 100%.
Contents
- The three flaws in detail
- What CVSS says, and what it doesn’t
- EPSS, or the question of probability
- The three flaws side by side
- What EPSS doesn’t do
- In practice, what to do
- To wrap up
The three flaws in detail
The Linux kernel (CVE-2026-98432)
The bug is fixed by this commit from Eric Dumazet, reported by Xinyang Ge, a researcher at Anthropic. The fix is two lines: two kfree() calls replaced with kfree_rcu(). In other words, a piece of memory is no longer freed straight away, only once no CPU is still reading it. What hides behind this is a race condition between two CPUs.
The scenario, step by step:
- A client opens an IPv4 TCP connection with IP options in its SYN packet (Record Route or Timestamp, options that are rarely used). The kernel stores those options in memory. Their content is chosen by the client, so by the attacker.
- When the connection is established, the kernel hands those options to the new socket without copying them. Two structures now point to the same memory: if one frees it, the other keeps using it without knowing.
- If the client resends the same SYN at that moment, another CPU may still be reading those options to reply while the first one has just freed them.
That’s a use-after-free: the kernel reads, and sometimes writes, memory it no longer owns. If the attacker manages to put their own data there in the meantime, the kernel treats it as legitimate. Depending on timing, that lets them read kernel memory, or change it. The bug has been there since Linux 4.4, so for about ten years.
Because it’s a race condition, the attacker can’t control the exact timing and has to try a large number of times. A failed attempt usually just fails the connection without crashing the machine, so they can retry as often as they like. That explains the timings below.
On 27 October Anthropic published how its exploit works, without the code. It runs in two stages:
- Defeat KASLR. KASLR loads the kernel at a random address on boot, so an attacker doesn’t know where to aim. The exploit wins the race condition so that the kernel sends back a few extra bytes of its own memory in its SYN-ACK. As soon as one of those bytes contains a kernel pointer, the attacker knows the memory layout. In Anthropic’s demonstration, this first stage takes 48 minutes, the time needed to win the race condition enough times to pull out a useful pointer. Without this step they would be aiming blind, and would crash the server far more often than take it over.
- Take control. The same bug then overwrites a function pointer, an address the kernel will call later. The kernel then runs code chosen by the attacker, which launches a command as root. Five more minutes. As root, the attacker has every right on the machine: read the data, install a backdoor, move on to the rest of the network.
There’s one condition that many headlines left out. Anthropic’s exploit targets servers with an MPTCP listener (Multipath TCP, an extension that lets one connection use several network paths). Anthropic doesn’t explain why MPTCP is needed. The bug itself is in ordinary IPv4 TCP, and nobody has publicly shown code execution without MPTCP yet. In practice, a server with no MPTCP listener isn’t affected by this particular exploit. It’s still vulnerable to the bug, and nothing guarantees another exploitation path won’t be found.
This is where Go comes in. Since Go 1.24, a Go program listening on a port enables MPTCP by default on Linux. The Go security team pointed this out on 29 October. Caddy, Traefik and plenty of recent infrastructure tools are written in Go. So you can have an MPTCP listener on a server without ever configuring one, just because one of those tools listens there. It’s not the only case: any program can open one, and the Go team itself warns that future exploits may not all depend on MPTCP.
To be precise about the score: the kernel CVE team rates the flaw 9.8, and Red Hat moved to 9.8 in early November after a lower provisional score.
As of 3 November: no public exploit, no observed exploitation. Marcus Hutchins reported scans looking for MPTCP servers on port 443 and counted around 760,000 machines accepting a TLS connection over MPTCP (The Stack). That says nothing about how many are vulnerable.
PHP2Shell (CVE-2026-109618 and CVE-2026-109619)
Two flaws in PHP itself, fixed on 21 October in versions 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Both concern how PHP manages memory from one request to the next.
The first, CVE-2026-109618 (RCE via OOM corruption), abuses out-of-memory handling. When a request exceeds the memory limit, PHP aborts it abruptly (with a longjmp()) and leaves half-initialised memory behind. The end-of-request cleanup then reads that memory as if it were valid. In practice, an attacker who can trigger an out-of-memory, for instance with a multipart request built for the purpose, gets PHP to handle data they partly control, all the way to running code.
The second, CVE-2026-109619 (RCE via stale global state), leaves global variables pointing at the memory of a finished request. The next request handled by the same PHP process reuses them. In practice, one of the attacker’s requests can booby-trap the next one: it’s another use-after-free, but across HTTP requests.
The painful part: according to PHP’s official advisory, on Apache with mod_php the attack goes through automatic request body parsing, before a single line of application code runs. Authentication, framework checks and code quality never come into play. If PHP serves pages, it’s exposed. Only the update really fixes it; in the meantime PHP suggests, for example, blocking multipart requests upstream if the site doesn’t need them.
Exploitation moved very fast. Exploit code was circulating on 21 October. watchTowr’s honeypots saw attacks the same day. Japan’s CERT reported on 26 October that “many PoCs” had been published and attack traffic kept coming. CISA added both flaws to its Known Exploited Vulnerabilities catalogue (KEV) on 26 October, giving US federal agencies three days to fix them (CISA alert). A KEV entry means exploitation is no longer a hypothesis: it has been observed.
One nuance: not every PoC works everywhere. Success depends on the version, the environment and the memory layout. But it only takes one that works on your server.
GitLab (CVE-2026-97229)
Rated 10 out of 10, and the most interesting to pull apart. Nothing is vulnerable “by default”: the attacker brings the booby-trapped regex. They have to be able to drop a .gitlab-ci.yml file (the one that describes a project’s CI/CD pipelines) into a project, with a regular expression of their choosing, for instance under a rules:exists:regexp rule. Everything that follows comes from Rapid7’s technical analysis.
The chain runs in five steps:
- GitLab validates the regex. Before using it, GitLab screens it: source length, repeat bounds, compiled program size capped at 1 MB. For those checks it compiles the regex in a given encoding. The booby-trapped regex passes every test.
- Then it recompiles it for UTF-8. Later, GitLab evaluates the regex against the repository’s file paths. If a path contains characters encoded on four bytes in UTF-8, Ruby’s regex engine (Onigmo) recompiles the regex for that encoding. That’s the heart of it: what was checked isn’t what runs. This second compilation escapes the first one’s checks.
- An integer overflows. To decide whether to “unroll” a repeat (copy the pattern ahead of time as many times as it repeats), Onigmo works out how much space that would take: roughly
pattern size × number of repeats. That calculation happens in a signed 32-bit integer, which can’t go above 2,147,483,647 (INT_MAX). The attacker picks values so the product goes past that ceiling. The integer has no room to hold a number that big in memory: it “overflows” and comes back round as a negative. In Rapid7’s example the calculation gives 2,147,513,555, only 29,908 too many, and that’s enough. - The check gives way. Because the result has turned negative, the “is this small enough to unroll?” test wrongly says yes. Onigmo then unrolls a huge program. Inside that oversized program, the internal jump addresses are themselves stored in integers that are too small, which overflow in turn. Some of the engine’s jumps no longer point to legitimate code, but to data the attacker controls.
- The regex becomes a command. From there, Rapid7 showed you can redirect the engine’s control flow all the way to a system call and get an operating-system command to run. I’m not reproducing the payload construction; it’s in their analysis. The point to keep: the command runs as the server’s
gituser, the one that owns the repositories. So the attacker reaches the hosted source code, and potentially the pipelines’ secrets.
One detail Rapid7 stresses: the evaluation doesn’t even go through creating a pipeline. It’s triggered by the CI config’s check (“lint”) API, reachable without authentication on a public project. Fixed on 12 October (19.2.8, 19.3.5, 19.4.2).
So it still comes down to being able to push that file and that booby-trapped path into a reachable project. On an instance where anyone can sign up and create a project, that barrier is gone. It also falls as soon as a legitimate account is compromised: a contractor, an intern, a former employee whose account was never disabled. CISA rated the flaw “automatable”, exploitable at scale by a script, and added it to KEV on 29 October.
What CVSS says, and what it doesn’t
CVSS measures the severity of a flaw in the abstract: reachable over the network or not, account needed or not, impact on confidentiality, integrity and availability. It describes the flaw. It says nothing about what attackers are doing.
All three flaws tick the same boxes: network, no account, no victim action, full impact. So they all land between 9.2 and 10.
PHP2Shell is almost ironic. Its CVSS 4.0 vector says attack complexity is high (AC:H) with attack requirements present (AT:P). On paper it’s the hardest of the three. Yet it was exploited on the day the patch came out. The kernel, on the other hand, is rated low complexity (AC:L), even though you have to win a race condition, uncover the memory layout and wait 48 minutes. Red Hat first went with AC:H, hence its lower provisional score. The gap between “low” and “high” comes down to interpretation.
CVSS is still useful: it tells you what happens if the flaw is exploited. It doesn’t tell you whether it will be.
EPSS, or the question of probability
EPSS (Exploit Prediction Scoring System) answers a different question: how likely is it that this flaw will be exploited somewhere in the world in the next 30 days? It’s published daily, for free, by FIRST, the organisation that also maintains CVSS.
Under the hood it’s a machine learning model (gradient-boosted trees, XGBoost) fed with around 2,850 features per flaw. Those include public exploit code, Metasploit modules or scanner templates, mentions on social media and forums, the vendor, the type of flaw, the age of the CVE and the number of references. It’s trained on what actually happened: attack detections from the honeypots, intrusion detection sensors and endpoint telemetry of a dozen partners (how it works).
It gives two numbers:
- the probability, between 0 and 1: 0.05 means a 5% chance of exploitation within 30 days;
- the percentile: where the flaw ranks among all CVEs. 98% means it looks more threatening than 98% of the others.
For scale, only 1.5 to 3% of published flaws are exploited in any 30-day window, and the median score is around 0.7% (user guide). A 25% score is huge.
The three flaws side by side
Scores pulled from the FIRST API on 3 November 2026:
| Linux kernel | PHP2Shell | GitLab | |
|---|---|---|---|
| CVE | 2026-98432 | 2026-109618 | 2026-97229 |
| CVSS | 9.8 | 9.2 (v4) | 10 |
| EPSS | 0.76% | 99.999% | 27.7% |
| EPSS percentile | 55% | 99.998% | 98% |
| Public exploit | No | Yes, several | Yes (Rapid7) |
| KEV catalogue | No | Yes, 26 Oct | Yes, 29 Oct |
Same severity rating, and the probability of exploitation varies by a factor of 130. The kernel flaw that sent everyone into a panic sits barely above the median CVE. PHP2Shell is at the very top of the scale.
The history is even more telling:
| Date | Kernel | PHP2Shell | GitLab |
|---|---|---|---|
| 22 Oct | 0.17% | 5.3% | 0.55% |
| 24 Oct | 0.17% | 11.8% | 0.55% |
| 25 Oct | 0.39% | 99.98% | 18.4% |
| 3 Nov | 0.76% | 99.999% | 27.7% |
The kernel score has still more than quadrupled since 24 October. Keep an eye on it. But it’s a long way from the other two.
What EPSS doesn’t do
I’m not going to sell you EPSS as a crystal ball. It has limits, and the table above shows one of them very well.
It lags. PHP2Shell was being exploited on 21 October. On the 22nd its EPSS was 5%, and it only jumped on the 25th. The model needs signals to pile up. In the first days of a flaw, a low EPSS doesn’t mean much.
It doesn’t know your estate. EPSS is a global number. It doesn’t know whether your GitLab is open to the internet or tucked behind a VPN, or whether your servers run a Go service. A 0.76% flaw on an exposed server that runs your whole business deserves more attention than the same flaw on an isolated test box.
A low score isn’t a green light. 0.76% is low for a single flaw. Across a hundred exposed servers it’s no longer negligible.
KEV comes first. GitLab sits at 27.7% even though CISA has seen it exploited. FIRST explains that this isn’t a contradiction: KEV looks back (it has been exploited), EPSS looks at the next 30 days. A flaw in KEV gets handled first, whatever its EPSS.
One last trap: don’t multiply EPSS by CVSS to get a “risk score”. FIRST advises against it in black and white; the result means nothing.
In practice, what to do
You don’t need a paid tool to start. Here’s the order I recommend, inspired by what CISA now does with its BOD 26-04 directive, which no longer sorts by CVSS but by exposure, observed exploitation and automation.
- In KEV and exposed to the internet: now. Hours, not days. That was PHP2Shell and GitLab on 26 October. And if a PHP 8.1 or older can’t be upgraded, CISA’s advice is to take it offline.
- High EPSS and exposed: this week. FIRST notes that a threshold around 0.04 (4%) selects roughly as many flaws as the CVSS “critical” category, but flaws with a real chance of being exploited. Adjust the threshold to how much you can patch.
- Everything else: in your normal cycle, starting with what’s exposed and what your business depends on. Check EPSS weekly; it moves.
The Linux kernel fell into the third category, with temporary measures that are easy to apply while you wait for the reboot. To check whether you have MPTCP listeners:
ss -M -l -n -p -eTo switch MPTCP off until the new kernel is in, as suggested by Akamai:
sysctl -w net.mptcp.enabled=0
sysctl -w net.ipv4.tcp_migrate_req=0This doesn’t affect sockets that are already open, so restart the services involved. On the Go side, the GODEBUG=multipathtcp=0 environment variable does the same thing one program at a time. It lowers the risk without removing it; the only real fix is an updated kernel.
And to look up the EPSS of your CVEs, one line is enough:
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2026-98432,CVE-2026-109618,CVE-2026-97229"The KEV catalogue can be downloaded as JSON from CISA’s website, and EPSS has a free API. Cross-referencing the two fits in about thirty lines of Python:
import json, urllib.request
# Your current CVEs (the ones affecting versions you actually run).
cves = ["CVE-2026-98432", "CVE-2026-109618", "CVE-2026-97229", "CVE-2026-110264"]
# EPSS: all the CVEs in a single call.
u = "https://api.first.org/data/v1/epss?cve=" + ",".join(cves)
epss = {d["cve"]: float(d["epss"]) for d in json.load(urllib.request.urlopen(u))["data"]}
# CISA's catalogue of already-exploited vulnerabilities (KEV).
k = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
kev = {v["cveID"] for v in json.load(urllib.request.urlopen(k))["vulnerabilities"]}
# Sort: what's in KEV first, then by descending EPSS.
for cve in sorted(cves, key=lambda c: (c not in kev, -epss.get(c, 0))):
flag = "KEV" if cve in kev else " "
print(f"{flag} EPSS {epss.get(cve, 0):6.1%} {cve}")Feed it the CVEs that concern you and it ranks them: first the ones CISA has seen exploited, then by descending probability. On our four examples it puts PHP2Shell then the first GitLab flaw at the top (both in KEV), then the second GitLab flaw, and the Linux kernel last. It’s the skeleton of what vulnerability-management tools sell, and it already changes how you triage.
To wrap up
I’m not saying the kernel shouldn’t have been patched. It should, within days. But an admin who spent the evening on kernels while their GitLab, open to sign-ups, was still on 19.4.1 got the order wrong. CVSS told them everything was burning at the same level. EPSS and KEV would have told them where the fire actually was.
There’s one condition for all of this to work: knowing what you run. Which PHP version is behind that old brochure site? Is that GitLab reachable from the internet? Is that reverse proxy written in Go? Without an inventory, EPSS is useless, because you don’t know what to apply it to. That’s exactly what the Discovery external audit does: a review of everything your company exposes on the internet, versions included. And if you want to set up a patching routine that holds up, we can talk it through in a consultation.
Related articles
- Pentest

Intrusion Audit
Intrusion audit, or pentest: simulating real attacks on internal, external, cloud or web systems to find and fix the flaws before attackers do.
- Cybersecurity

An inventory of successful hacks: what if it was not that complicated?
Five famous hacks (Colonial Pipeline, Equifax, Change Healthcare, France Travail, supermarket fridges): mundane ways in, and what each one cost.
- Pentest

GDPR and penetration testing
How penetration testing helps meet GDPR security obligations: the key articles (25, 32, 33, 35), the sanctions, and why a pentest proves due diligence.
