10 月 21 日,Oracle 发布了一份计划外的安全警报,编号 CVE-2026-91403,CVSS 3.1 评分 9.1,影响 Oracle Java SE、Oracle GraalVM for JDK 与 GraalVM Enterprise Edition。IT之家在同一天报道了这条消息,标题落在「高危漏洞,用户应尽快升级」。
这条新闻本身没有错,但它把一个证书信任问题写成了「Java 出了个高危漏洞」的样子。主机吧读完原始公告后发现,实际情况值得多说几句——因为这个「漏洞」的成因,主机吧在一周前刚写过一篇:印度 CA 机构 eMudhra 的中间证书私钥可被直接算出来。CVE-2026-91403 就是那起事件的第三块多米诺骨牌。
更关键的是:浏览器早就封了,操作系统也封了,唯独默认配置的 Java 还在信任。我在本机做了实测,结果和厂商的说明完全对得上。下面先说结论,再说怎么处理。
这不是 Java 的代码缺陷,是 Java 的证书信任材料没人来更新。把一个「信任决策错误」按「远程代码执行」的口径去理解,后面的处置动作很容易做错。
一、先把这个漏洞的性质说清楚
Oracle 公告的描述只有一句话:
This Security Alert addresses CVE-2026-91403 by providing Java updates which distrusts the set of impacted intermediate CA certificates related to eMudhra.
没有内存越界,没有反序列化,没有可执行代码被注入。它做的是把一批证书加进黑名单。CVSS 9.1 的向量是 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N——网络可达、低复杂度、无需权限、无需交互,机密性与完整性双高,可用性无影响。这个打分在逻辑上没错:如果攻击者能签发任意域名的证书,你发给「银行」的请求可以被完整读取和篡改。
但这里有个容易踩的坑。OpenJDK 的公告写得比 Oracle 更明确:CVE-2026-91403 concerns eight compromised intermediate CA certificates.——它明确说这是一个证书信任问题。第三方安全分析也点出了同一件事:这条通告「并未确立一条 Java 远程代码执行的利用链」。
所以正确的理解是:
| 不是 | 而是 |
|---|---|
| JVM 有个洞,攻击者能塞代码进来 | JVM 会相信一张伪造的证书,因为它信任的中间 CA 已经不安全 |
| 服务器会被直接打穿 | 你的 Java 程序连接的服务,可能不是你以为的那个服务 |
| 打个补丁就完事 | 补丁改的是一个证书文件,怎么改取决于 Java 是怎么装的 |
换句话说,受害位置在 Java 作为 TLS 客户端的那一侧,不在服务器端。JSSE 这个组件名(Java Secure Socket Extension)已经说明了位置——就是 Java 里做 TLS 的那一层。
二、为什么浏览器拦住了,Java 没拦住
要理解这个 CVE,得先看清一条平时不会注意的分界线。

eMudhra 的八个中间 CA 出问题之后,各方反应其实很快:
- 10 月 17 日,报告者用每个私钥签名了「We have a problem here」并提交给浏览器根计划,Firefox 与 Chrome 当天封禁;
- 10 月 18 日至 19 日,eMudhra 以
CA_COMPROMISE为原因撤销了全部八个中间证书; - 10 月 19 日 Apple 封禁,10 月 20 日 Microsoft 封禁。
这些封禁通过 OneCRL、CRLite、CRLSets 等各自渠道下发,都是浏览器自己的机制。而 JVM 有两个地方和它们完全不相通:
第一,Java 自带一份独立的 cacerts。除非应用显式设置 javax.net.ssl.trustStore,JDK 只用 $JAVA_HOME/lib/security/cacerts(或同目录的 jssecacerts)里那份根证书列表。它不订阅浏览器的封禁列表,也不跟随操作系统的 CA 更新——除非这个 JDK 是发行版包管理器装的、且 cacerts 恰好是指向系统证书库的软链。
第二,Java 默认不检查证书撤销状态。com.sun.net.ssl.checkRevocation 默认是 false。也就是说,即使 eMudhra 在 10 月 18 日已经把这些证书标成 CA_COMPROMISE,一个默认配置的 JVM 在握手时根本不会去问「这张证书撤销了吗」。CA 的撤销对一个默认 JVM 是隐形的。
还有第三层:Java 标准 TLS 实现不强制证书透明度(CT)。按测试站的说法,这些中间证书在封禁前从未出现在 CT 日志里——这意味着强制 CT 的浏览器不可能被用于攻击,但对不做 CT 校验的客户端来说,这条线索同样不存在。
三、本机实测:6 条链,全部信任
与其看别人的截图,不如直接在自己的机器上跑。我用的是环境里现成的 OpenJDK 20(Zulu 20.28+85-CA),先用 keytool 确认两个关键根证书都在:
keytool -list -cacerts -storepass changeit | grep -iE "emsign|comodo"
# comodoaaaca [jdk], Jan 1, 2004, trustedCertEntry ← Sectigo AAA
# cert_94_emsign_ecc_root_ca___g394, Feb 27, 2023, trustedCertEntry ← emSign ECC Root CA - G3
两个根都在。注意 comodoaaaca 这一行——它就是 Sectigo 的 AAA Certificate Services。这一点很关键,后面会讲。
然后写一个最小程序,用 java.net.http.HttpClient 依次连接测试站提供的六个主机,覆盖 DV / OV / EV 三种证书等级 × emSign 与 Sectigo 两条签发路径:
var c = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
var r = c.send(HttpRequest.newBuilder(URI.create(url)).build(),
HttpResponse.BodyHandlers.ofString());
return "TRUSTED HTTP " + r.statusCode();
六个主机按 {dv,ov,ev}-{g3b,g3c}[-aaa]-<测试站域名> 的规律拼出,-aaa- 后缀表示走 Sectigo 的交叉签名路径。返回 200 就是「信任了」,抛异常才是「拒绝了」——这里要特别留神,默认配置下能连上才是坏结果。

结果很干净:六个主机,六个 HTTP 200。一条都没拒。
这里面最值得单独说的是 -aaa- 那三行。很多人会想「我用的不是 eMudhra 的根,跟我没关系」——但 Sectigo 当年给同样的公钥做了交叉签名,所以走 Sectigo 根也能链到同一批中间证书。密钥是同一把,换根躲不过。主机吧此前在 eMudhra 那篇文章里提过这个交叉签名结构,这里是它在 Java 侧的实际体现。
四、两种缓解措施,我都试了
Oracle 和 OpenJDK 给了两条不必升级的应急路径。两条我都跑了一遍,都能挡住。
路径一:开启撤销检查。加两个系统属性:
java -Dcom.sun.net.ssl.checkRevocation=true \
-Dcom.sun.security.enableCRLDP=true \
Check.java
加上之后,六个主机全部被拒。其中 Sectigo 那条链的报错信息很明确:
CertificateRevokedException: Certificate has been revoked,
reason: CA_COMPROMISE, revocation date: ...
但另外几条链报的是 CertPathValidatorException: Could not determine revocation status——拒绝的理由不是「确认撤销」,而是「没法确认是否撤销」。这个区别很重要,下面第五节会展开。
路径二:替换 blocked.certs。这是 OpenJDK 公告的首选建议。它就是一个文本文件,第一行写 Algorithm=SHA-256,之后每行一个证书的 SHA-256 指纹。测试站公布了 impacted-intermediates.json,里面正好是 16 条记录——8 个中间 CA,每个都有 emSign 签名和 Sectigo 交叉签名两个版本。
# 先生成 16 条指纹,再追加到 JDK 的 blocked.certs
python3 -c "import json; [print(x['sha256']) for x in json.load(open('impacted-intermediates.json'))]" \
>> "$JDK_HOME/lib/security/blocked.certs"
为了不动原环境,我拷了一份 JDK 出来单独改。改完之后六个主机全部 REJECTED,报错是:
CertPathValidatorException: Untrusted certificate: CN=EM DV TLS CA - G3B, O=eMudhra BV, C=NL
这个报错比撤销检查那条清楚得多——它直接点名了是哪张中间证书被拒。
然后是最重要的一步:验证没有误杀。我在两种配置下都跑了 https://www.zhujib.com/ 作为对照,都是 200。追加 16 条指纹只拉黑那八个中间 CA 本身,不影响其他证书——这也正是它比「直接删掉两个根」更精确的原因。
五、动手前,先把这三个坑填上
实测过程中遇到几个问题,都是照着文档做仍然会卡住的地方。
坑一:官方 blocked.certs 下载链接打不开
按 OpenJDK 公告给的地址取文件,Unix 和 Windows 两个版本我都试了,都返回 HTTP 403,页面是「Unexpected outage」。这不是我这边的问题——同一时段有研究者记录了完全一致的遭遇。所以现阶段更可靠的做法是从 impacted-intermediates.json 自己生成指纹追加,等官方链接恢复后再换回分发版本。
另外,Rev 1 的公告里还写错了一处:11.0.33.0.2 这个版本号后来在邮件列表里更正为 11.0.33。照着错版本号去找补丁会找不到。
坑二:撤销检查会让正常握手也失败
前面提到的那几条 Could not determine revocation status,就是这个问题。撤销检查会在每次连接时去拉 CRL,分发点不可达、超时或者返回异常,握手就直接失败——不是因为证书有问题,而是因为查不到状态。
实测里最直接的证据是:开启撤销检查之后,连对照组 www.zhujib.com 都报 ConnectException: HTTP connect timed out。同一台机器、同一个网络,不开撤销检查时它是 200。
这不是说撤销检查不该用,而是说它改变的是所有证书的校验行为,副作用范围远大于「只拉黑这八张证书」。生产环境里,如果你的服务依赖多个外部 HTTPS 接口,务必先在预发环境跑一轮,别直接上生产。
坑三:改错文件,改完等于没改

OpenJDK 公告把处置方式按安装来源分成三类,这个分类比「升级 Java」这句笼统建议有用得多:
| Java 怎么装的 | 用哪份证书 | 该做什么 |
|---|---|---|
| 独立安装(Oracle JDK、Temurin、Liberica、官网压缩包) | 自带的 cacerts |
升级到 .0.2 版本;做不到就换 blocked.certs 或开撤销检查 |
| 发行版包(Arch、Debian、Ubuntu、Fedora、RHEL、Oracle Linux 等) | 多数是系统 CA 库 | 更新系统 ca-certificates 即可,Java 本身不用动 |
| Homebrew、FreeBSD | 仍是 Java 自带 | 必须更新 Java 包本身,更新系统证书无效 |
Homebrew 和 FreeBSD 这两行是明确的例外。如果你在 macOS 上用 Homebrew 装的 JDK,看到「更新 ca-certificates」这类建议照做,等于什么都没做。想快速判断自己的环境属于哪一类,看一眼 cacerts 是不是软链就有答案:
ls -l "$(dirname $(dirname $(readlink -f $(which java))))/lib/security/cacerts"
软链指向系统证书库的,走系统更新那条路;是普通文件的,就得动 JDK 自己。
还有一处小矛盾要提醒:OpenJDK 公告说受影响的版本是 8u512 及更早,而测试站把 8u511 列为已修复。这两个说法在写这篇文章时还没能对齐。用 JDK 8 的站点建议直接问发行方要确切版本号,别按公告里的数字去猜。
六、这件事真正的教训
eMudhra 事件(经过见主机吧此前的梳理)从 10 月 17 日爆发到现在,已经牵出了三条独立的信任链:浏览器、操作系统、Java 运行时。三方各自修复、各自发公告,节奏完全不同步。
主机吧觉得最有价值的一点是:「我升级了浏览器」和「我的 Java 服务安全了」是两件事,中间没有任何自动传递关系。Java 那份 cacerts 是个很安静的组件——它只在 JDK 更新或管理员手工改的时候变,其他时候一动不动,包括世界另一边有人把 CA 的私钥算出来的时候。
如果你手上有跑着 Java 的服务,尤其是那些会主动往外部发 HTTPS 请求的(调第三方 API、拉配置、连消息队列、下载依赖),今天就值得花十分钟做一件事:确认它的 JVM 到底用哪份信任库,以及那份信任库里那两张根证书还在不在。查询命令一行就够:
keytool -list -cacerts -storepass changeit | grep -iE "emsign|comodoaaaca"
如果有输出,说明你的 Java 此刻还在信任那批证书。有 blocked.certs 这条精准路径在,处理起来不算麻烦——前提是你知道自己要动的是哪个文件。
主机吧 · 本文实测基于 OpenJDK 20(Zulu 20.28+85-CA),数据引自 Oracle 安全警报、OpenJDK 2026/10/21 公告与 emudhra.we-have-a-problem.com 公布的受影响证书清单。
