2026 年 10 月 2 日,Apache Struts 把 7.4.0 和 6.12.0 一起标成 General Availability。10 月 5 日,官网公告页又列出四份安全公告,S2-075 到 S2-078,修复都在这两个版本里。还停在 7.3.0、6.11.0 或更早版本的应用,这次应该尽快升级。漏洞之外,7.x 应用要留意参数绑定:struts.parameters.requireAnnotations 为 true 时,ModelDriven 动作自己的成员也要 @StrutsParameter,没有注解的 fluent setter 会被拒绝。6.x 没有这道注解门。两条线一起动的是 REST 默认栈,restDefaultStack 和 beanValidationDefaultStack 都加进了 coep、coop、fetchMetadata,请求正文长过 struts.rest.content.maxLength(默认 2097152 个字符)会在动作执行前抛 RequestBodyTooLargeException。
四份安全公告,两条线都修了
四份公告给的建议一样:升级到 7.4.0,或者在 6.x 维护线上升级到 6.12.0。受影响范围都覆盖 6.0.0 到 6.11.0、7.0.0 到 7.3.0,以及部分已经 EOL 的 2.x 版本,只是各自的触发条件不同。
- S2-075(CVE-2026-104711,WW-5706):遗留 RESTful action mapper 存在 OGNL 注入,可能导致远程代码执行,最高评级 Moderate。只有配置了这个遗留 mapper 的应用受影响,默认 action mapper、
restful2mapper 和 REST 插件都不受影响。7.x 只有在关掉 OGNL allowlist 时才受影响,而 allowlist 默认是开着的。不能马上升级的,官方给的绕行办法是换回默认 mapper、restful2或 REST 插件。 - S2-076(CVE-2026-104712,WW-5711):请求参数绑定到
java.math.BigDecimal属性,再经 Struts 标签库渲染时,响应可能比请求大好几个数量级,不用认证、低流量就能把 CPU 和出口带宽耗尽,最高评级 Moderate。JSON 插件和 REST 插件的响应不受影响,double、float、long、int等类型也不受影响。修复后,小数位极多的值会四舍五入后再渲染。 - S2-077(CVE-2026-104713,WW-5723):REST 插件读请求正文时没有上限,单个请求就能把堆内存吃满,最高评级 Important,是这四份里最高的。用 REST 插件接收请求正文的应用在默认配置下就暴露,不需要额外开关。修复就是上面那个 2097152 个字符的默认上限,确实要收大正文的应用可以用
struts.rest.content.maxLength调高。表单提交和 multipart 上传不经过这个插件,不受这个上限影响。暂时不能升级的,官方建议先在反向代理或 Servlet 容器上限制正文大小,Struts 内部没有只改配置就能绕开的办法。 - S2-078(CVE-2026-104714,WW-5724):全局文本提供器缓存的
MessageFormat被并发请求共用。本地化消息里如果格式化了日期或时间参数,一个用户的值可能出现在另一个用户的响应里,也可能直接报服务端错误,最高评级 Moderate。Struts 自带的消息都不格式化日期时间,问题只出在应用自己的消息包里。
10 月 2 日的两条发版公告本身没有带安全编号,Version Notes 后来也补上了 Security 一节,对应的工单号就是上面括号里的那几个。
下载页把这两个版本并列成当前的完整发行。页面写,不再列在上面的版本是 End-of-Life,Apache Struts Team 不再提供安全补丁。最低规格没有变:7.x 仍要求 Java 17 和 JakartaEE,6.x 仍要求 Servlet API 3.1、JSP API 2.1 和 Java 8。7.4.0 的公告在版本说明之前多一句,要先读 Migration Guide;6.12.0 只指向 Version Notes。这次没有检索到一份单独成篇、标题落在 7.4.0 上的迁移指南正文,下面的细节以 Version Notes 和插件页为准。Maven Central 上 org.apache.struts:struts2-core 的 latest 和 release 都已是 7.4.0,版本列表里也有 6.12.0。
参数注解只写进 7.4.0
6.12.0 的 Breaking changes 只有 REST 那两条,没有 @StrutsParameter、ModelDriven 或 fluent setter。下面这些都只在 7.4.0 的版本说明里。
安全文档写,struts.parameters.requireAnnotations=true 从 6.4 起可以打开,从 7.0 起默认就是打开。7.4.0 的版本说明把新规则放在这个开关为 true 的前提下:ModelDriven 动作自己的成员现在也要 @StrutsParameter,只有模型对象上的成员继续豁免。迁移路径的配置键写成 struts.parameters.requireAnnotations.transitionMode。安全页对这个模式的说明仍是:打开之后,只有嵌套参数需要注解,也就是动作上 public getter 所代表的 DTO 或集合;动作上的 public setter 继续可以注入。版本说明没有再写 transitionMode 在 ModelDriven 上豁免到哪一层。升级时把这两处原文对着看,不要假设模型豁免会借这个开关扩回动作自身。
同一版开始识别 fluent setter。版本说明的原话是:@StrutsParameter 的强制执行现在认得 fluent(非 void)setter,没有注解的 fluent setter 会像 void setter 一样被拒绝。工单标题写的是,注解强制执行原先认不出这种非 void setter。相关方法 StrutsParameterAuthorizer.hasValidAnnotatedPropertyDescriptor 标成废弃,替换写法是 findBindableAccessor 配合 hasValidAnnotatedMethod,移除跟踪在 WW-5739。
允许名单上有一处方向不同的修正。标在 ModelDriven 动作自身上的嵌套属性,现在会预先放进 OGNL allowlist,于是在 struts.allowlist.enable=true 时可以绑定。安全页写,allowlist 从 6.4 起提供,从 7.0 起默认开启。另外,单参数的 get* 或双参数的 set*,如果并不是某个真实属性的索引访问器,参数绑定时不再调用。工单是 WW-5697,ReflectionContextState.DENY_INDEXED_ACCESS_EXECUTION 同时标成废弃。
7.4.0 的 breaking changes 不止参数绑定。例如 SecurityMemberAccess 去掉了五个 devMode 配置 setter:useDevMode、useDevModeExcludedClasses、useDevModeExcludedPackageNamePatterns、useDevModeExcludedPackageNames、useDevModeExcludedPackageExemptClasses。剩下的配置 setter 不再由容器注入。struts.devMode.* 这些设置本身不变。完整清单在 Version Notes,这里不逐条复述。
REST 默认栈,两边的原文一致
7.4.0 和 6.12.0 的版本说明里,下面两条 breaking change 原文一致。
REST 插件的 restDefaultStack,以及 Bean Validation 插件的 beanValidationDefaultStack,现在包含 coep、coop 和 fetchMetadata。面向跨站浏览器客户的 REST API,必须配置 fetchMetadata.exemptedPaths,或者禁用这个 interceptor-ref。工单 WW-5718。改进项的标题把它们称作 resource-isolation interceptors,并写明原先是插件提供的默认栈漏掉了它们。版本说明点名的是这两个栈,没有改写核心 defaultStack 的成员名单。
Fetch Metadata 拦截器文档写,默认资源隔离策略会拒绝那些不是顶层导航的跨源请求。判断式原文是 Sec-Fetch-Site == 'cross-site' AND (Sec-Fetch-Mode != 'navigate'/'nested-navigate' OR method NOT IN [GET, HEAD])。exemptedPaths 默认空,路径要带前导斜杠,并且是相对路径。这就是版本说明把 fetchMetadata.exemptedPaths 写成升级条件的原因。
COOP 和 COEP 是同一批加进这两个栈的。版本说明没有写它们开始拦截请求体。COOP 拦截器文档写,默认 mode 是 same-origin,作用是响应头。COEP 拦截器文档写,enforcingMode 默认 false:enforcing 会拦截并报告违规,report-only 只报告。默认落在 report-only。版本说明要求跨站浏览器客户处理的,是 fetchMetadata 那一项。
第二条共同变更是正文长度,也就是 S2-077 的修复。REST 插件拒绝长过 struts.rest.content.maxLength 的请求正文,默认 2097152,异常类型是 RequestBodyTooLargeException,发生在动作运行之前。工单 WW-5723。S2-077 写明这个值按字符计,约合 2 MB;被拒的请求以 Struts 异常抛出,应用可以用 exception mapping 映射成自己想要的响应。6.12.0 的 GitHub release 说明把 WW-5718 和 WW-5723 写成 6.x backport,工单号和 7.4.0 对得上。
Jackson 按 Java 成员名授权,没有进 6.12.0。7.4.0 写:REST 正文授权现在以 Java 成员名为键,不再以 Jackson 的线上名为键,所以用 @JsonProperty 改了名的成员,按其自身注解授权。工单 WW-5715。单参数构造 AuthorizingSettableBeanProperty(SettableBeanProperty) 废弃,改用 (SettableBeanProperty, String memberName),移除跟踪 WW-5744。
7.4.0 的 REST 正文还有几处 6.12.0 没有的行为变化。Jackson 处理不再把请求正文合并进一个已经持有值的多态属性;值通过授权过的路径被替换,正文必须带上 type id。struts.parameters.requireAnnotations=true 时,使用基于属性的 @JsonIdentityInfo 的类型,其 id 属性在 REST 正文里和其他属性一样要过 @StrutsParameter。被 @StrutsParameter 裁掉的 REST 正文对象,不再把剩余字段漏进外层对象。6.12.0 的缺陷列表里没有 WW-5715、WW-5726、WW-5727、WW-5747。两边都列了 WW-5749:REST 的 XML 处理在没有渲染目标时抛出空指针。
两条线还一起废弃了 RestfulActionMapper 和 Restful2ActionMapper。废弃记在 WW-5707,版本说明写改用 REST 插件,移除跟踪是 WW-5708。S2-075 出在遗留的 RESTful mapper 上,还在用它的应用除了升级,也可以借这次把路由迁到 REST 插件。
6.12.0 版本说明里列出的工单,在 7.4.0 的说明里都能找到同号。反过来不成立。6.12.0 和 Tiles 相关、7.4.0 也有的一项是 WW-5716,给按 locale 的定义缓存加上界。OGNL: 默认关闭不在 6.12.0 的说明里。
html5 主题、Tiles 前缀、报表库
从校验器推导 HTML5 约束属性,只在 7.4.0。新功能 WW-5695 写:用 html5 主题渲染的表单,现在带上从动作校验器推导出的 HTML5 约束属性,浏览器会在提交前做校验。同一版修了推导和校验器不一致的几处。HTML5 的 pattern 会误拒正则校验器接受的纯空白输入。radio 和 file 上的 required,在绑定属性从不为 null 时会误拒。visitor 校验字段的约束消息,要按被访问对象来解析。主题文档页没有列出新增了哪些属性名。pattern 和 required 是缺陷标题里出现的名字。
同一版把 xhtml 和 css_xhtml 主题里的 JavaScript 客户端校验标成废弃,点名表单上的 validate="true" 和 Form.getValidators。废弃记在 WW-5694,移除跟踪是 WW-5696。版本说明写改用 html5 主题的约束属性。html5 主题在更早的版本里已经存在。7.4.0 加上的是约束属性从动作校验器推导。
Tiles 插件的遗留 OGNL: 属性求值器默认关闭,只在 7.4.0。版本说明写,它会抛 EvaluationException,过渡期用 struts.tiles.ognl.legacy.enabled=true 恢复。插件页把差异写清楚了:这个前缀来自 Tiles 自身,表达式对着 Tiles 请求求值,不经过 Struts 的成员访问策略、allowlist 和表达式防护。S2: 前缀从 Struts 2.5.3 起就可以用,求值交给 ValueStack。插件页写,从 7.4.0 起把仍依赖 OGNL: 的定义迁到 S2:,或改成普通的 Tiles 属性。常量从拥有该 Tiles 容器的 Web 应用的 Struts 配置读取,在第一次对 OGNL: 求值时读取;遗留求值器被激活时记一条警告。常量和 OGNLAttributeEvaluator 都已废弃。版本说明写移除跟踪 WW-5714,插件页写会在未来的大版本去掉。
<constant name="struts.tiles.ognl.legacy.enabled" value="true"/>
JasperReports 7 插件不再传递 net.sf.jasperreports:jasperreports,只在 7.4.0。版本说明写,应用必须自己声明它,PDF 输出还要自己声明 jasperreports-pdf。缺陷标题给出的原因是:插件把 LGPL 的 jasperreports jar 打进了发行包。插件页写得更具体。插件从 Struts 7.1.0 起提供,构件是 struts2-jasperreports7-plugin,按 JasperReports 7.0.7 构建。7.1.0 到 7.3.0 把它声明为 compile,于是会传递进来,并打进发行包。JasperReports 是 LGPL,不能放进 Apache 发行物,所以 7.4.0 起改为 provided,和老的 JasperReports 插件一直以来的做法相同,工单 WW-5735。升级后不补上依赖,应用会在运行期因为缺少 JasperReports 类而失败。两个 JasperReports 构件要用同一个版本。插件页的安装片段用的是占位符,版本号要自己填:
<dependencies>
...
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jasperreports7-plugin</artifactId>
<version>STRUTS_VERSION</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>JASPERREPORTS_VERSION</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-pdf</artifactId>
<version>JASPERREPORTS_VERSION</version>
</dependency>
...
</dependencies>
这次发布里,STRUTS_VERSION 对应 7.4.0。
报表插件在 7.4.0 还有几处和依赖范围无关、升级后会碰到的修复。老 JasperReports 插件页写:没有放进 reportParameters 的报表参数,会按名字到值栈上查找;这个回落从 6.0.0 到 7.3.0 实际上到不了 JasperReports,参数为 null;7.4.0 起再次生效,工单 WW-5729。7 插件的 CSV 导出曾经把记录分隔符设成字段分隔符。格式名以前区分大小写,CSV 会找不到 exporter;7.4.0 起精确匹配失败时会再试小写。导出器曾经在报表写出之前关掉响应流,Tomcat 上得到空响应。
一起交流
分享你的思考,让讨论更进一步。