Java CVE-2026-91403: Compromised cacerts Roots Allow Silent MITM on All LTS Versions
Oracle disclosed CVE-2026-91403 (CVSS 9.1) where compromised eMudhra intermediate CA private keys in the JDK cacerts trust store allow attackers to forge valid certificates for any domain, enabling undetectable man-in-the-middle attacks on all Java LTS versions; the article details detection via keytool, removal steps, container image fixes, and interim mitigations like revocation checking and custom trust managers.
Vulnerability Overview
Oracle released a security alert on October 21, 2026 for CVE-2026-91403 with a CVSS 3.1 score of 9.1 (Critical). The vector is AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N, meaning network-accessible, low complexity, no privileges or user interaction required, with high confidentiality and integrity impact but no availability impact. The flaw is not in application code but in the JDK's bundled cacerts trust store: several root certificates have associated intermediate CA private keys that were compromised in the public eMudhra incident.
Root Cause: Trust Anchor Compromise
In the eMudhra incident, private keys for eight intermediate CAs were exfiltrated. Researchers demonstrated possession by signing a fixed message with each key. These intermediate CAs chain to roots trusted by Mozilla, Microsoft, Apple, and Chrome root programs. The JDK cacerts file includes four of these root certificates:
emsignrootcag1 [jdk];
emsignrootcag2 [jdk];
emsigneccrootcag3 [jdk];
comodoaaaca [jdk];Possession of an intermediate CA private key allows an attacker to issue a fully valid certificate chain for any domain. The JVM's certificate validation passes, the TLS handshake succeeds, and no exception appears in logs — making the attack completely silent. Affected scenarios include outbound HTTPS calls, JDBC over TLS, Kafka/RabbitMQ TLS channels, OIDC JWKS fetching, mTLS client certificate validation, and signed JAR verification.
Affected Versions
All current LTS lines are impacted: Java SE 8u511, 8u511-perf, 11.0.33, 17.0.21, 21.0.13, 25.0.5, 27.0.1; GraalVM for JDK 17.0.21, 21.0.13; GraalVM Enterprise Edition 21.3.20. No LTS version is immune.
Detection: Verify Whether Your Runtime Is Affected
A single keytool command checks for the suspicious aliases (JDK 9+):
# JDK 9 and later default cacerts password is changeit
keytool -list -cacerts -storepass changeit | grep -Ei "emsign|comodoaaaca"For richer detail (alias, subject DN, SHA-256 fingerprint), a Java program can scan the trust store. The same code works on JDK 8 and JDK 21 because java.home points to the JRE directory in JDK 8 and the JDK root in JDK 9+, and appending /lib/security/cacerts resolves correctly in both cases.
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import java.util.HexFormat;
import java.util.Set;
public class ScanCacerts {
// The four aliases involved in this incident, with the [jdk] suffix used by the bundled trust store
private static final Set<String> SUSPECT = Set.of(
"emsignrootcag1 [jdk]",
"emsignrootcag2 [jdk]",
"emsigneccrootcag3 [jdk]",
"comodoaaaca [jdk]"
);
public static void main(String[] args) throws Exception {
// JDK 8: java.home points to jre; JDK 9+: points to JDK root. Both work with /lib/security/cacerts
String path = System.getProperty("java.home") + "/lib/security/cacerts";
KeyStore ks = KeyStore.getInstance("JKS");
try (InputStream in = new FileInputStream(path)) {
ks.load(in, "changeit".toCharArray());
}
for (String alias : SUSPECT) {
if (!ks.containsAlias(alias)) {
continue; // already removed — this runtime is clean
}
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
String fp = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(cert.getEncoded())
);
System.out.printf(
"%s%n subject = %s%n sha256 = %s%n",
alias,
cert.getSubjectX500Principal(),
fp
);
}
}
}Remediation: Remove the Compromised Roots and Restart
The official fix is delivered via CPU (Critical Patch Update) 426; each distributor will release updated runtimes. However, version numbers alone cannot guarantee the fix because vendors sync at different speeds. After upgrading, re-run the detection command — if the aliases remain, the fix has not been applied.
If a vendor update is not yet available, manually delete the four entries from cacerts and restart the JVM. The trust store is loaded once at startup, so running processes are unaffected until restarted.
JDK 9 and later:
# Backup first, so you can roll back if needed
cp "$JAVA_HOME/lib/security/cacerts" "$JAVA_HOME/lib/security/cacerts.bak"
keytool -delete -cacerts -storepass changeit -alias "emsignrootcag1 [jdk]"
keytool -delete -cacerts -storepass changeit -alias "emsignrootcag2 [jdk]"
keytool -delete -cacerts -storepass changeit -alias "emsigneccrootcag3 [jdk]"
keytool -delete -cacerts -storepass changeit -alias "comodoaaaca [jdk]"JDK 8: requires explicit keystore path because java.home points to the JRE directory.
cp "$JAVA_HOME/jre/lib/security/cacerts" "$JAVA_HOME/jre/lib/security/cacerts.bak"
keytool -delete -keystore "$JAVA_HOME/jre/lib/security/cacerts" \
-storepass changeit -alias "emsignrootcag1 [jdk]"
# repeat for the other three aliasesSide effect: The first three roots are eMudhra's own and have limited usage. The fourth, comodoaaaca (AAA Certificate Services), is widely used. Oracle recommends testing critical outbound connections (payments, object storage, SMS, authentication) in a staging environment before deleting, to avoid breaking legitimate TLS handshakes.
Container Images: The Most Overlooked Vector
If the base image tag hasn't changed, Docker's build cache retains the old cacerts layer; a simple rebuild does not help. Options:
Pin the base image by digest (e.g., eclipse-temurin:21-jre@sha256:REPLACE_ME).
Force a clean rebuild.
Add a build-time assertion that fails the build if any compromised root remains.
# Lock digest to prevent tag drift to a stale trust store
FROM eclipse-temurin:21-jre@sha256:REPLACE_ME
# Assertion: the trust store must not contain the incident roots; if found, the build fails
RUN ! keytool -list -cacerts -storepass changeit | grep -Ei "emsign|comodoaaaca"Batch-scan local images (only works for images that include keytool):
docker images --format '{{.Repository}}:{{.Tag}}' | \
while read -r img; do
docker run --rm --entrypoint sh "$img" -c \
'keytool -list -cacerts -storepass changeit 2>/dev/null | grep -Ei "emsign"' \
>/dev/null 2>&1 && echo "受影响: $img"
doneDebian/Ubuntu: ca-certificates-java Bug
OpenJDK on Debian/Ubuntu uses the system ca-certificates package, converted to a Java keystore via ca-certificates-java. A long-standing bug (Debian #1069251) prevents apt upgrade from refreshing the already-generated keystore. To force regeneration:
# Run as root; force rebuild of the Java trust store
rm -f /etc/ssl/certs/java/cacerts
update-ca-certificates --freshArch, Fedora, Gentoo, Oracle Linux, and RHEL use the system certificate bundle directly — updating the system certificates suffices. FreeBSD and Homebrew OpenJDK do not follow the system bundle; upgrade the package instead.
Interim Mitigations During the Upgrade Window
Enable revocation checking at the JVM level so each handshake queries CRL/OCSP:
java -Dcom.sun.net.ssl.checkRevocation=true \
-Dcom.sun.security.enableCRLDP=true \
-jar app.jarLimitation: the default is soft-fail — if CRL fetching fails (network outage, CDN hiccup), the connection is allowed, effectively losing protection. For fail-closed behavior, implement a custom X509TrustManager wrapper that throws on any CertPathValidatorException.
Application-level guard: reject any certificate chain containing an eMudhra-issued certificate. This is a temporary band-aid only for this specific incident; the real fix is cleaning the trust store.
// Stop-gap: reject any chain where a certificate's subject DN contains eMudhra identifiers
// Only protects against this incident's chains; not a general solution.
private static void rejectEmudhraChain(X509Certificate[] chain) throws CertificateException {
for (X509Certificate cert : chain) {
String dn = cert.getSubjectX500Principal().getName();
if (dn.contains("eMudhra") || dn.contains("EMSIGN") || dn.contains("EM-DV")) {
throw new CertificateException("untrusted eMudhra chain: " + dn);
}
}
}Hook this method into checkServerTrusted and checkClientTrusted.
Signed JAR verification: enable revocation checking when verifying signatures:
jarsigner -verify -revcheck -strict app.jarRecommended Handling Order
Scan all runtimes to confirm impact scope.
Upgrade to patched runtimes and re-scan to verify removal.
If upgrade is not immediately possible, delete the four roots from cacerts and restart the JVM.
Enable revocation checking on critical services.
Prioritize services with heavy outbound HTTPS traffic, mTLS endpoints, and platforms that fetch remote metadata or plugin packages.
No public proof-of-concept or in-the-wild exploitation has been reported, but absence of a PoC does not imply absence of capability. The 9.1 score reflects that the attacker needs nothing — no credentials, no user interaction. The exploitation window for this class of vulnerability is typically shorter than most assume.
References: Oracle Security Alerts, OpenJDK Risk Matrix, OpenJDK eMudhra Disposal Notice.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
