log4j 这批公告里,让「升到 2.25.4 就够了」变错的那一条,机器读不到

0 阅读9分钟

先说清楚:这批不是 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-34477log4j-core2.25.4
CVE-2026-34478log4j-core2.25.4
CVE-2026-34480log4j-core2.25.4
CVE-2026-34479log4j-1.2-api2.25.4
CVE-2026-34481log4j-layout-template-json2.25.4
CVE-2025-68161log4j-core2.25.3

看完这张表,几乎所有人都会得出同一个答案:升到 2.25.4

但还有第 7 条:

CVE模块修复版
CVE-2026-49844log4j-api2.25.5(2.26 线要 2.26.1)

它的 advisory 原文写着自己是 CVE-2026-34481 的不完整修复:

The fix released in version 2.25.4 did not cover all affected code paths. CVE-2026-49844 was assigned to the remaining issue, which concerns the MapMessage.asJson() serialization in Apache Log4j API and is fixed in versions 2.25.5 and 2.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}

typeunreviewed,而且 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-core34477 / 34478 / 34480 / 681612.25.4
log4j-api498442.25.5(2.26 线:2.26.1)
log4j-1.2-api344792.25.4
log4j-layout-template-json344812.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.sslVerifyHostName system 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.sslVerifyHostName system property, but not when configured through the verifyHostName attribute of the <Ssl> element. Although the verifyHostName configuration 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-68161SocketAppender + TLS
CVE-2026-34478直接Rfc5424Layout(用 SyslogAppender不受影响)
CVE-2026-34480log4j-core 的 XmlLayout
CVE-2026-34479桥的 Log4j1XmlLayout,或 log4j 1 兼容层 + org.apache.log4j.xml.XMLLayout
CVE-2026-34481JsonTemplateLayout + MapMessage/ObjectMessage 里的浮点值
CVE-2026-49844JsonTemplateLayout 的 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>)→ 中 34477verifyHostname(小写 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

github.com/xiaoqiMikko…

它输出三段:①四个模块各自扫到的版本;②配置里找到的触发条件(带文件与行号证据); ③逐条求交集后你该升到哪个版本,以及你是不是正处在「已经升过级、以为修完了」的窗口里。

配置在归档内部也扫(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 好得多。

🔴 这个工具不能证明什么(两个方向都得说)

「没找到触发条件」不等于安全,至少四种情况会让它变成假的安心:

  1. 配置是代码里构建的(ConfigurationBuilder / Configurator.initialize),配置文件里没有那个元素;
  2. 配置运行时才注入(log4j2.configurationFile 指向别处、容器里挂进来);
  3. 你依赖的第三方库自带一份 log4j2 配置而没被扫到;
  4. 你压根没把配置传进来 —— 这种情况报告会单独标成「本次没看到配置」,而不是「不适用」。 这两句话该导致完全不同的动作,所以我让它们在报告里长得不一样。

「触发条件全部成立」也不等于确认中招:多数条目还要求「攻击者能控制那个被记进日志的值」, 这一点工具判不了。所以它的结论只够用来排优先级,不够用来宣布事故。

不覆盖 log4j 1.x(log4j:log4j)—— 扫到会告警但不判定,它是另一套代码。

最后

这批公告值得花二十分钟的理由,不是它有多危险(它不危险,全是 medium), 而是它属于版本号看不出来的那一类:

你的配置写得好好的,升级也照着 advisory 做了,而某个安全设置已经悄悄失效了很久。

顺手留个我自己的教训:我一开始只查了 log4j-core 一个坐标,得出「4 条、修复版统一 2.25.4」。 这三个数字后来全错了。 把两个源摆在一起比一遍,才是 7 条、4 个模块、2.25.5。 「我以为只有一个源」这件事本身,就得用第二个源去查。