先说清楚:这批不是 Log4Shell,全是 medium
先劝退一下:这批全是 medium,没有 RCE。如果你在找 Log4Shell,这篇帮不了你。
如果你是搜「log4j 漏洞」进来的,大概率想找的是 CVE-2021-44228。这篇不是讲那个的。
Apache Log4j 在 2025/2026 年发了一批新的公告,7 条,全部 medium,最高 CVSS v4 只有 6.9,一条 RCE 都没有。
它们有个统一的机理,而这个机理恰好是自动化工具最不擅长的那种:
你的配置在没有任何报错的情况下失效了,而你以为它还在保护你。
- 属性被静默忽略:
verifyHostName配了等于没配; - 属性被静默改名:换行转义悄悄停止工作,TLS framing 悄悄降级成明文 TCP;
- 日志被静默丢掉:某些字符让整条日志事件消失,只进内部 status logger。
所以光看版本号看不出你到底中没中 —— 要读配置。这也是我写那个工具的原因。
一、这批里最容易踩的一脚:升到 2.25.4 的人没升到位
7 条里 6 条的修复版都 ≤ 2.25.4:
| CVE | 模块 | 修复版 |
|---|---|---|
| CVE-2026-34477 | log4j-core | 2.25.4 |
| CVE-2026-34478 | log4j-core | 2.25.4 |
| CVE-2026-34480 | log4j-core | 2.25.4 |
| CVE-2026-34479 | log4j-1.2-api | 2.25.4 |
| CVE-2026-34481 | log4j-layout-template-json | 2.25.4 |
| CVE-2025-68161 | log4j-core | 2.25.3 |
看完这张表,几乎所有人都会得出同一个答案:升到 2.25.4。
但还有第 7 条:
| CVE | 模块 | 修复版 |
|---|---|---|
| CVE-2026-49844 | log4j-api | 2.25.5(2.26 线要 2.26.1) |
它的 advisory 原文写着自己是 CVE-2026-34481 的不完整修复:
The fix released in version
2.25.4did not cover all affected code paths. CVE-2026-49844 was assigned to the remaining issue, which concerns theMapMessage.asJson()serialization in Apache Log4j API and is fixed in versions2.25.5and2.26.1.
而这一条,机器读不到。
二、为什么说「机器读不到」
我按坐标反查了 GitHub 的 advisory 数据库(Dependabot 用的就是这份索引):
# 注意:git bash 下 gh api 的路径不要加前导斜杠
gh api -X GET advisories -f cve_id=CVE-2026-49844 \
--jq '.[] | {ghsa: .ghsa_id, type: .type, vulns: (.vulnerabilities | length)}'
结果:
{"ghsa": "GHSA-qv9r-c865-cp47", "type": "unreviewed", "vulns": 0}
type 是 unreviewed,而且 vulnerabilities 数组是空的 ——
没有包名、没有版本区间、没有修复版。也就是说,不存在任何可以和你的依赖树比对的数据。
对比一下同批的另一条:
gh api -X GET advisories -f cve_id=CVE-2026-34480 \
--jq '.[] | {ghsa: .ghsa_id, type: .type, vulns: (.vulnerabilities | length)}'
# {"ghsa": "GHSA-3pxv-7cmr-fjr4", "type": "reviewed", "vulns": 2}
换个源(OSV.dev)结论一样,而且更干净 —— 另外 6 条都能通过 GHSA alias 拿到 Maven 坐标, 只有它连 alias 都没有:
curl -s https://api.osv.dev/v1/vulns/CVE-2026-49844 | python -c "
import json,sys; d=json.load(sys.stdin); print('aliases =', d.get('aliases'))"
# aliases = None
curl -s https://api.osv.dev/v1/vulns/CVE-2026-34480 | python -c "
import json,sys; d=json.load(sys.stdin); print('aliases =', d.get('aliases'))"
# aliases = ['GHSA-3pxv-7cmr-fjr4']
🔴 这里必须说句公道话,免得被读成别的意思。
unreviewed是 GitHub 的正常流程状态(NVD 自动导入、还没人工标注受影响包), 不是失职,也不是 Dependabot 有 bug。它就是这条数据现在还不存在。 而这批公告最讽刺的地方在于: 唯一把正确答案从 2.25.4 顶到 2.25.5 的那一条,恰好是它。
三、顺带说一个更普遍、但不是工具的错的问题
我最初以为这批 7 条全在 log4j-core 上。错了,它们散在 4 个模块上:
| 模块 | 命中条目 | 该升到 |
|---|---|---|
log4j-core | 34477 / 34478 / 34480 / 68161 | 2.25.4 |
log4j-api | 49844 | 2.25.5(2.26 线:2.26.1) |
log4j-1.2-api | 34479 | 2.25.4 |
log4j-layout-template-json | 34481 | 2.25.4 |
目标版本不是同一个数字。 而绝大多数项目的 pom 里只写 log4j-core
(log4j-api 靠它传递进来,另两个按需引入),按那一个坐标反查只看得到 4 条。
🔴 但这一条不是 Dependabot 的错,别混着写。 它按你真实的依赖树逐模块告警,那 3 条它报得出来。 会漏的是「我只关心 log4j-core 的版本号」这个人为习惯 —— 尤其当你在
dependencyManagement里单独钉过log4j-api、或某个第三方 BOM 覆盖了它, 四个模块的版本就会错开,而「我把 log4j 升到 2.25.4 了」这句话此时只对了一部分。所以两个数字要分开记:按单坐标看少 3 条(习惯问题) vs 真正报不出来的 1 条(数据问题)。
四、另一条补丁缺口链:verifyHostName 配了六年等于没配
第二条链更早,而且更容易让人误以为自己安全:
CVE-2025-68161:SocketAppender 不做 TLS 主机名校验。官方说升 2.25.3。CVE-2026-34477:上面那个修复不完整 —— 它只处理了log4j2.sslVerifyHostNamesystem property 那条路, 没处理<Ssl verifyHostName="true">配置属性那条路。要 2.25.4。
原文:
The fix for CVE-2025-68161 was incomplete: it addressed hostname verification only when enabled via the
log4j2.sslVerifyHostNamesystem property, but not when configured through theverifyHostNameattribute of the<Ssl>element. Although theverifyHostNameconfiguration attribute was introduced in Log4j Core 2.12.0, it was silently ignored in all versions through 2.25.3.
2.12.0 是 2019 年。也就是说,如果你是在 log4j2.xml 里用属性配主机名校验的,
那它从引入那天起一直没生效,而配置文件看起来完全正常。
而这条链最坑的地方是:照 68161 升到 2.25.3 的人,主观上已经"修完了" —— 他是最不会再回头查的那批人。
五、光报版本不够,得回答「这 7 条里我真中几条」
这批每一条都要求你用了某个特定的 layout 或 appender:
| CVE | 触发条件(取自官方原文) |
|---|---|
| CVE-2026-34477 | <Ssl> 的 verifyHostName 属性 + Socket / Syslog / SMTP appender |
| CVE-2025-68161 | SocketAppender + TLS |
| CVE-2026-34478 | 直接配 Rfc5424Layout(用 SyslogAppender 的不受影响) |
| CVE-2026-34480 | log4j-core 的 XmlLayout |
| CVE-2026-34479 | 桥的 Log4j1XmlLayout,或 log4j 1 兼容层 + org.apache.log4j.xml.XMLLayout |
| CVE-2026-34481 | JsonTemplateLayout + MapMessage/ObjectMessage 里的浮点值 |
| CVE-2026-49844 | JsonTemplateLayout 的 message resolver,或 MapMessage.asJson() |
而这些东西全都写在 log4j2.xml 里,能读。 实测同一套装了受影响版本的四个模块:
- 配置是最常见的那种(
Console+RollingFile+PatternLayout)→ 版本层报 7 条,真中 0 条 - 配置里有
Socket+<Ssl verifyHostName>+Rfc5424Layout+XmlLayout→ 报 7 条,真中 4 条
两个负判据是官方原文明说的,工具单独成一档:
用 SyslogAppender 的不中 34478(原文:Users of the SyslogAppender are not affected);
只用 HTTP appender 的不中 34477(原文:This issue does not affect users of the HTTP appender)。
这一档和「我没找到」的可靠程度完全不同 —— 前者有原文背书,后者只是我没看见。
🔴 这里有两个坑,只 grep 名字必踩
| 一组 | 另一组 | 差别 |
|---|---|---|
verifyHostName(大写 N,<Ssl>)→ 中 34477 | verifyHostname(小写 n,HTTP appender)→ 官方写明不受影响 | 一个字母的大小写 |
XmlLayout(34480,log4j-core) | Log4j1XmlLayout(34479,1.2-api 桥) | 前者是后者的子串 |
两组的结论都是相反的。所以工具在配置层做结构化解析(XML 走 JDK 的 DOM 且关掉外部实体,
properties 按 xxx.type = Foo 建「前缀→插件」映射)而不是文本匹配 ——
只有读出「这个属性挂在哪个元素上」,才能区分上面那两组。
YAML / JSON 没有 JDK 内置解析器(而我坚持这工具零运行时依赖),只能文本匹配。 报告会把那几条标成「文本依据」并说明弱在哪,不会冒充成结构化结论。
六、工具
# Release 里直接下 jar,零依赖,不联网
java -jar log4j-check.jar app.jar # fat jar 里就带着 log4j2.xml,一步到位
java -jar log4j-check.jar ./target ./src
java -jar log4j-check.jar ~/.m2/repository --no-config
它输出三段:①四个模块各自扫到的版本;②配置里找到的触发条件(带文件与行号证据); ③逐条求交集后你该升到哪个版本,以及你是不是正处在「已经升过级、以为修完了」的窗口里。
配置在归档内部也扫(BOOT-INF/classes/、WEB-INF/classes/),
所以只丢一个 Spring Boot fat jar 给它就够 —— 不需要源码树。
log4j2-spring.xml / log4j2-test.xml / log4j.xml(1 兼容层)都认。
判定表不是手抄的,由脚本从两个一手源生成(Apache 官方 CycloneDX VDR
- GitHub advisory 按坐标反查),17 条断言任一不满足就中止不写文件。
🔧 顺便留个对别人也有用的发现:
/repos/apache/logging-log4j2/security-advisories实测返回 0 条 —— Apache 不走 GitHub Security Advisories 那套流程。 如果你写脚本盯 Apache 系组件的安全公告,只查那个端点会得到「一切太平」,而且不报错。 真正机器可读的一手源是https://logging.apache.org/cyclonedx/vdr.xml(CycloneDX VDR), 逐模块给精确版本区间、带描述与修复建议原文,比爬 HTML 好得多。
🔴 这个工具不能证明什么(两个方向都得说)
「没找到触发条件」不等于安全,至少四种情况会让它变成假的安心:
- 配置是代码里构建的(
ConfigurationBuilder/Configurator.initialize),配置文件里没有那个元素; - 配置运行时才注入(
log4j2.configurationFile指向别处、容器里挂进来); - 你依赖的第三方库自带一份 log4j2 配置而没被扫到;
- 你压根没把配置传进来 —— 这种情况报告会单独标成「本次没看到配置」,而不是「不适用」。 这两句话该导致完全不同的动作,所以我让它们在报告里长得不一样。
「触发条件全部成立」也不等于确认中招:多数条目还要求「攻击者能控制那个被记进日志的值」, 这一点工具判不了。所以它的结论只够用来排优先级,不够用来宣布事故。
不覆盖 log4j 1.x(log4j:log4j)—— 扫到会告警但不判定,它是另一套代码。
最后
这批公告值得花二十分钟的理由,不是它有多危险(它不危险,全是 medium), 而是它属于版本号看不出来的那一类:
你的配置写得好好的,升级也照着 advisory 做了,而某个安全设置已经悄悄失效了很久。
顺手留个我自己的教训:我一开始只查了 log4j-core 一个坐标,得出「4 条、修复版统一 2.25.4」。
这三个数字后来全错了。 把两个源摆在一起比一遍,才是 7 条、4 个模块、2.25.5。
「我以为只有一个源」这件事本身,就得用第二个源去查。