Let's Talk About Black October
Table of Contents
Introduction
For four days in October, our security channels read like the montage at the start of a disaster movie. Between October 19 and 22, 2026, so many serious disclosures went public that people started calling the whole month Black October.
Black October is a media label, and a slightly misleading one, because it suggests a single attack or a single bug. It was a pile-up of unrelated vulnerabilities, several of which had been sitting in code for months or years, that happened to land in the same week. Most of it had nothing to do with Gradle, but one piece affects any build that runs on a JVM, which is every Gradle build ever.
This post covers what happened, what it looked like from inside Gradle (including the places where we were exposed), and what we’d check before the next pile-up arrives.
What actually happened during Black October? #
Here’s the short tour:
| Disclosure | What it was | Why build engineers should care |
|---|---|---|
| PHP2Shell | Two remote code execution flaws in the PHP runtime (CVE-2026-109618 and CVE-2026-109619). Amazon’s honeypots saw the first exploit attempt on October 17, and PHP shipped fixes on October 21. | Not a JVM problem, but the cleanest example of the patch race. |
| Node2Shell | An out-of-bounds write in the Brotli C decoder (CVE-2026-111497), affecting Brotli 1.0.3 through 1.2.0. Node.js bundles it, and so do Java wrappers like brotli4j. | Vulnerable native code can ride in on a dependency you never picked. |
| eMudhra CA compromise | Eight intermediate CA certificates whose private keys were generated so badly that anyone can recover them from the certificates. One of the keys was literally the number 4. | They chain to roots your JDK trusts. |
| Linux kernel list leak | Unpatched kernel bugs posted to the public kernel mailing list instead of the private security list. | Disclosure itself became a point of failure. |
| LiteLLM on PyPI | A malicious release (1.107.0) published through the project’s own release pipeline with a credential stealer. It was live for about two hours. | The build provenance looked legitimate and faithfully recorded a malicious fork as the source. |
PHP2Shell made the most noise. The embargo leaked a day early, CERT-EU told anyone who couldn’t patch to pull their PHP apps offline, and dozens of public websites, government sites among them, went dark for hours. We checked our own exposure fast. We don’t run PHP anymore, and we’ve never been happier about it.
As for why it all happened at once, most of the explanations we read point at AI-assisted vulnerability discovery. The team that found PHP2Shell used AI-assisted tooling, and Chris Krebs (the former director of the U.S. Cybersecurity and Infrastructure Security Agency) told CNBC that about 3% of all critical vulnerabilities ever discovered had turned up in the past month.
Is finding bugs still the hard part? #
No, finding bugs got cheap. Patching, rebuilding, and redeploying run at the same speed they did last year, because people and release processes do that work.
On October 22, OpenAI’s Sam Altman said he no longer thinks the industry can patch every bug. The disclosure system is already falling behind. As of October 30, the NVD dashboard counted 91,687 new CVEs in 2026, of which NVD had analyzed 42,858. GitHub told the community its CVE assignment queue was running at about five weeks. We can confirm that one personally: our own advisories from earlier this month are still waiting for CVE IDs.
With fixes as the bottleneck, the useful question for your team is how fast a fix reaches the builds you actually run.
Why does Black October hit your build? #
Of the five disclosures, the eMudhra compromise matters most for JVM builds. Chrome and Firefox blocked the compromised intermediates on October 17, using revocation lists that only browsers ship. Most other TLS clients have nothing like that. According to our infrastructure team’s testing, JVMs and curl with OpenSSL both kept trusting the forged chain.
The JDK makes this worse because it carries its own trust store in cacerts.
Updating your operating system’s CA bundle does nothing for it.
Endor Labs put it well: “Patching the system does not patch the application.”
A Gradle build starts a JVM, and that JVM downloads plugins, dependencies, and sometimes a Gradle distribution over HTTPS. An attacker holding a recovered key and sitting in the middle of that connection could present a valid-looking certificate for a repository host and serve a tampered artifact.
Our team judged an attack on the Gradle Plugin Portal possible but a stretch, because the attacker needs a man-in-the-middle position on your connection first. We knew of no exploitation in the wild. But “unlikely” is a weak answer to “could someone have swapped a plugin in our build?”, and you deserve a better one.

Brotli is a different kind of exposure. Versions of the Java wrapper brotli4j before 1.24.0 bundle the vulnerable C decoder in platform-specific native artifacts. Whether you’re exposed depends on whether one of those native JARs sits on your runtime classpath, and you may have never asked for brotli4j in the first place. If it’s there, upgrade to brotli4j 1.24.0, which bundles the fixed Brotli 1.2.1.
The fastest way to find out whether brotli4j is in your build is a Build Scan.
Search for brotli4j in the dependencies view and it shows every project and configuration that resolves it.
Without a Build Scan, run ./gradlew :app:dependencyInsight --dependency brotli4j in each project.
To check build dependencies such as plugins, run ./gradlew buildEnvironment.
Since it doesn’t cover buildSrc (gradle/gradle#8301), also run ./gradlew :buildSrc:dependencyInsight --dependency brotli4j --configuration runtimeClasspath for each buildSrc project.
Our docs on reporting dependencies cover this.
The fix depends on where brotli4j shows up. In the simplest case, it arrives transitively on a project’s runtime classpath, and you can enforce the fixed version with a strict constraint in that project:
// build.gradle.kts
dependencies {
constraints {
implementation("com.aayushatharva.brotli4j:brotli4j") {
version { strictly("1.24.0") }
}
}
}
Add the same constraint for each brotli4j native artifact on your classpath (such as native-linux-x86_64), because the natives are where the vulnerable decoder lives.
If it comes in through a plugin instead, upgrade that plugin or ask its maintainers for a release with brotli4j 1.24.0.
What did Black October look like inside Gradle? #
Like every other engineering team that week, we started by patching our own machines. On October 20, after the eMudhra keys went public, one of our engineers asked everyone at Gradle to install every available update, because a third-party certificate authority’s leaked keys let attackers impersonate any website. Then we looked at what that same leak meant for our CI and our official Docker images.
Did Gradle Build Tool need a fix? #
No. Gradle doesn’t ship its own trust store and relies on the JDK for TLS, so the product itself needed no patch.
That answer is technically correct and not very comforting. Every Gradle build runs on a JDK, and every JDK we tested trusted the forged chain.
Was our CI exposed? #
Yes. Our infrastructure team verified that Temurin 11, 17, 21, and 25 and macOS’s built-in curl all accepted the forged chain, while browsers rejected it. That meant every Gradle build on our CI agents, across Linux, macOS, and Windows, was exposed.
Containment took most of the week:
- Blocking the compromised certificates in every JDK on our agents, including JDKs provisioned on demand for builds
- Blocklisting the certificates in OS bundles where the vendor hadn’t shipped a fix
- Patching the embedded JDK in our internal artifact repository instead of waiting for its vendor
- Rebuilding our machine images
Getting the blocklist right took us three tries, because each certificate has several valid encodings with different fingerprints (Sonatype explains why). If you blocked these certificates yourself, make sure your list has all 48 entries, the same number the JDK vendors ship.
We also tested GitHub-hosted runners. Linux and macOS runners trusted the forged chain, and Windows rejected it. When we published this, GitHub’s runner images didn’t include the fix yet, so any job on a Linux or macOS runner that builds with Gradle was exposed unless the workflow patched its own trust store.
Not everyone agreed on how far to go. At one point, someone asked whether we were overreacting on CI. We settled on a simple rule: patch now where the fix is easy, and wait for vendor JDK releases where it isn’t.
What about the official Gradle Docker images? #
This one is a good story, and we wish it were a better one.
Every variant of the official gradle image trusted the forged chain, each for a different reason:
| Variant | Why it was exposed | Status on November 4, 2026 |
|---|---|---|
| Corretto | The JDK trusted the emSign root, and we expected Amazon to be the slowest to fix it. We were wrong. | Fixed. Amazon shipped fixed builds on October 23 with all 48 entries in blocked.certs, and our images rebuilt the same day. |
| Temurin | The JDK carries its own trust store, so OS certificate fixes never reached Java. | Waiting on Adoptium’s fixed release. Our images rebuild automatically once the new base images land. |
| GraalVM | Pinned tarballs with their own trust store. Rebuilding the Ubuntu base changed nothing for Java, and the old GraalVM CE 17 and 21 lines will never get upstream fixes. | Moved to Liberica NIK, which still maintains 17 and 21. We’ll bump it manually once Liberica ships a fixed release. |
One of our engineers proposed patching blocked.certs directly in every image.
The image maintainers chose to keep the same security posture as the upstream JDK images instead of layering on mitigations that would later need unwinding.
That’s a defensible trade-off, and it’s also why some of our variants were still waiting at publication.
Two takeaways if you build your own images:
- “Rebuild when the base image changes” doesn’t help when the JDK carries its own trust store.
- GraalVM
native-imagebinaries keep the trust store they were built with, so you need to rebuild them after your GraalVM distribution is fixed.
Did we publish advisories of our own? #
Yes, earlier this month we published three high-severity Gradle advisories:
We’ll cover them in an upcoming blog post.
What should you check this week? #
Here’s our list, in the order we’d do it:
- Update your JDKs, then prove the fix is there. OpenJDK and Oracle shipped theirs as CVE-2026-91403, but don’t trust the version number alone. The disclosure site lists test hosts, and a patched JVM should refuse to connect to them.
- Find out which JDK and trust store each build really uses. Check the Gradle daemon JVM, your toolchains, your Docker images, your CI runners, and any GraalVM
native-imagebinaries. - Turn on dependency verification. It checks checksums and signatures of every artifact, so a tampered plugin fails the build even if TLS was fooled.
- Turn on dependency locking. Lock files list every resolved dependency, transitives included, so a new brotli4j in your graph shows up in the code review diff.
- Stay on a supported Gradle version. Under our open source policy, that means the latest minor of the current and previous major releases.
- Inventory the native code you ship. Look for platform-specific artifacts like brotli4j’s natives, and ask whether you need them.
Two commands to get started:
# See which JVM actually runs your Gradle daemon
./gradlew --version
# Bootstrap dependency verification metadata for your build
./gradlew --write-verification-metadata sha256 help
Review the generated gradle/verification-metadata.xml before you commit it, since bootstrapping trusts whatever your build downloads right now.
Why is staying patched the hard part? #
Black October exposed the gap between “a fix exists” and “your build is running it.” Teams pinned to older Gradle versions, or living with long upgrade cycles, sit right in that gap. Under our open source policy, security fixes go to the latest minor of the current and previous major releases, on a best-effort basis, and anything older is end of life.
The Gradle Security Subscription exists for teams that need more than best effort. It adds fixes under SLA (Critical in 7 days, High in 30), security patches for older Gradle versions, and advance notice of new vulnerabilities. Open source Gradle users keep receiving security fixes under the existing policy.
If your team runs Gradle versions you can’t upgrade quickly, talk to us.