Jackson 在 10 月 17 日(UTC)同时发了 2.22.4、2.21.8 和 2.18.12 三个补丁版,三条线都修了一批带 CVE 编号的问题。databind 里最值得看的是 CVE-2026-110879:类型 id 写成泛型时,尖括号里的类以前没有被 PolymorphicTypeValidator 好好检查。我在 Spring Boot 3.5.16 里复现了一遍,2.21.7 会把白名单外的类实例化并调用它的 setter,2.21.8 直接拒绝。
GitHub 上三个 tag 的时间换算成北京时间,是 10 月 18 日凌晨 2 点 24 分(2.18.12)、4 点 09 分(2.21.8)和早上 7 点 11 分(2.22.4)。各版本的官方发布说明在 Jackson 的 wiki 上:2.22.4、2.21.8、2.18.12。
这次修了哪些 CVE
2.22.4 和 2.21.8 的发布说明内容一样。按模块列出来是这样:
| 模块 | 问题 | CVE |
|---|---|---|
| Databind | #6287 校验类型 id 中的类型参数和数组元素类型 | CVE-2026-110879 |
| Avro | #819 bytes 值在检查实际内容前就按声明长度分配 | CVE-2026-104015 |
| CBOR | #821 CBORParser.TagList 的数组应按倍数扩容 | CVE-2026-106554 |
| Protobuf | #817 加强带长度前缀值的长度校验 | CVE-2026-104016、CVE-2026-104017、CVE-2026-110747 |
| Smile | #825 非阻塞解析器校验原始二进制长度,不预先分配整块缓冲 | CVE-2026-104895 |
带 CVE 编号的条目里,2.18.12 有两处不同。Databind 多了一条 #5967(被 @JsonIgnore 的 setter 改名后,仍能通过私有字段反序列化),编号 CVE-2026-54516。Protobuf 的 #817 只列了 CVE-2026-104016 和 CVE-2026-104017 两个编号。另外 2.18.12 的 Streaming 部分写的是 None,2.22.4 和 2.21.8 则有 7 条 jackson-core 的解析修复,都没带 CVE 编号。
发布说明只给了 CVE 编号,没有严重级别。截至 10 月 18 日下午(北京时间),CVE-2026-110879 在 CVE 官网上仍是保留(RESERVED)状态,NVD 没有收录,GitHub Advisory Database 里也查不到。#6287 的 PR 描述引用的 GitHub 安全公告 GHSA-m8xf-2cr7-77ph 还没公开,jackson-databind 仓库已公开的安全公告里,最新一条是 9 月 22 日的。官方渠道目前都没有给出严重级别。
3.x 随后也跟进了,3.1.8 和 3.2.4 已经发布。jackson-databind 两个 tag 的时间换算成北京时间,是 10 月 18 日早上 8 点 16 分和 10 月 19 日早上 6 点 44 分,发布说明里同样修了 CVE-2026-110879 等这批编号。
泛型类型 id 的问题出在哪
下面按 PR #6287 的描述来讲。它只跟类名形式的类型 id 有关,也就是 Id.CLASS 和 Id.MINIMAL_CLASS。
以前只有最外层的类会被正经校验。类型参数里写的类,只走 validateSubClassName() 和 validateSubType()。如果用的是 DefaultBaseTypeLimitingValidator,它的拦截都放在 validateBaseType() 里,于是类型参数什么都能过。更直接的一种情况是,外层基类型一旦被 validateBaseType() 判为 ALLOWED,StdTypeResolverBuilder 会换上 LaissezFaireSubTypeValidator,配置的校验器对类型参数就完全不管了。普通数组类型 id 的元素类型也有同样的问题,比如 allowIfBaseType(Serializable.class) 时,任何数组类型都能过。
修复后,类型 id 里出现的每个类型参数,递归下去包括数组元素类型,都要和直接写在类型 id 里的类走同样的检查。Object、String、基本类型和包装类、枚举、java.util 里的集合与 Map 等类型豁免,但容器里装的类型照样要查。PR 原文举的例子是:在 allowIfBaseType(Msg.class) 下,Env<PlainMsg> 能过,Env<Gadget> 不能过。下面的实测就照这个例子搭。
同一个 CVE 下,10 月 17 日还合入了一个后续修复 PR #6289。它处理的是数组:如果某条规则对同一维度的所有数组都放行,比如 allowIfSubType(Object[][].class),元素类型仍要单独校验。三个版本的 tag 都包含这个提交。
Spring Boot 默认带的是哪个版本
我查了 Spring Boot 两个当前版本 BOM 里管理的 Jackson 版本(摘录):
$ curl -s https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/3.5.16/spring-boot-dependencies-3.5.16.pom | grep -E '<jackson(-2)?-bom.version>'
<jackson-bom.version>2.21.4</jackson-bom.version>
$ curl -s https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/4.1.1/spring-boot-dependencies-4.1.1.pom | grep -E '<jackson(-2)?-bom.version>'
<jackson-2-bom.version>2.21.5</jackson-2-bom.version>
<jackson-bom.version>3.1.5</jackson-bom.version>
Spring Boot 3.5.16 默认是 2.21.4,Spring Boot 4.1.1 里给 Jackson 2 用的是 2.21.5,都不含这次的修复。最新的里程碑版 4.2.0-M2 管理的是 2.21.6,同样不含。Boot 4 默认用的是 Jackson 3(迁移过程见之前那篇讲 Boot 4 换 Jackson 3 的文章),BOM 里 jackson-bom.version 管的是 Jackson 3,Jackson 2 的版本属性叫 jackson-2-bom.version,4.1.1 和 4.2.0-M2 都是这个名字。下面的复现用的是 Boot 3.5.16,覆盖的属性是 jackson-bom.version;在 Boot 4 上要改 Jackson 2 的版本,得覆盖 jackson-2-bom.version。
在 Spring Boot 3.5.16 里复现
环境是 Java 21.0.12.1,构建用 Maven 3.9.9。JDK 版本输出摘录如下:
$ java -version
openjdk version "21.0.12.1" 2026-08-18
OpenJDK Runtime Environment (build 21.0.12.1+1-1-deb13u1-Debian)
OpenJDK 64-Bit Server VM (build 21.0.12.1+1-1-deb13u1-Debian, mixed mode, sharing)
项目只依赖 spring-boot-starter-web,父 POM 是 spring-boot-starter-parent 3.5.16。消息接口用类名做类型 id:
package com.example.ptvdemo;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public interface Msg {
}
Env 是带泛型的信封,PlainMsg 是正常的消息:
package com.example.ptvdemo;
public class Env<T> implements Msg {
public T payload;
}
package com.example.ptvdemo;
public class PlainMsg implements Msg {
public String text;
}
Gadget 不实现 Msg,构造器和 setter 只打一行日志,用来看它会不会被创建出来:
package com.example.ptvdemo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 不实现 Msg,也不在任何白名单里,只用来观察它会不会被实例化
public class Gadget {
private static final Logger log = LoggerFactory.getLogger(Gadget.class);
public Gadget() {
log.warn("Gadget constructor called");
}
public void setCmd(String cmd) {
log.warn("Gadget.setCmd called with: {}", cmd);
}
}
校验器只放行 Msg 的子类型,通过 Jackson2ObjectMapperBuilderCustomizer 设到 Boot 自动配置的 ObjectMapper 上。启动后再打印一下实际加载的 jackson-databind 版本:
package com.example.ptvdemo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
@SpringBootApplication
public class PtvDemoApplication {
private static final Logger log = LoggerFactory.getLogger(PtvDemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(PtvDemoApplication.class, args);
}
@Bean
Jackson2ObjectMapperBuilderCustomizer onlyMsgSubtypes() {
BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Msg.class)
.build();
return builder -> builder.postConfigurer(mapper -> mapper.setPolymorphicTypeValidator(ptv));
}
@Bean
ApplicationRunner printJacksonVersion() {
return args -> log.info("jackson-databind version: {}",
com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION);
}
}
接口收一个 Inbox,里面只有一个 Msg msg 字段,返回反序列化出来的 payload 类型:
package com.example.ptvdemo;
public class Inbox {
public Msg msg;
}
package com.example.ptvdemo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class InboxController {
@PostMapping("/inbox")
public String receive(@RequestBody Inbox inbox) {
Msg msg = inbox.msg;
if (msg instanceof Env<?> env) {
Object payload = env.payload;
return "accepted Env, payload type = "
+ (payload == null ? "null" : payload.getClass().getName()) + "\n";
}
return "accepted " + msg.getClass().getSimpleName() + "\n";
}
}
端口配在 src/main/resources/application.properties 里:
server.port=18080
logging.level.org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver=warn
logging.level.org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver=warn
准备了三个请求体,放在项目的 requests/ 目录。第一个是正常的 Env<PlainMsg>,第二个把类型参数换成 Gadget,第三个直接拿 Gadget 当类型 id:
requests/env-plain.json
{"msg":{"@class":"com.example.ptvdemo.Env<com.example.ptvdemo.PlainMsg>","payload":{"@class":"com.example.ptvdemo.PlainMsg","text":"hi"}}}
requests/env-gadget.json
{"msg":{"@class":"com.example.ptvdemo.Env<com.example.ptvdemo.Gadget>","payload":{"cmd":"touch-file"}}}
requests/direct-gadget.json
{"msg":{"@class":"com.example.ptvdemo.Gadget","cmd":"touch-file"}}
同一份代码打了三次包:Boot 默认版本、2.21.7、2.21.8。版本用 -Djackson-bom.version 覆盖,打完包再看 jar 里实际带的是哪个 databind(摘录):
$ mvn -B clean package -DskipTests -Djackson-bom.version=2.21.7
[INFO] BUILD SUCCESS
$ jar tf target/jackson-ptv-demo-0.0.1.jar | grep jackson-databind
BOOT-INF/lib/jackson-databind-2.21.7.jar
$ mvn -B clean package -DskipTests -Djackson-bom.version=2.21.8
[INFO] BUILD SUCCESS
$ jar tf target/jackson-ptv-demo-0.0.1.jar | grep jackson-databind
BOOT-INF/lib/jackson-databind-2.21.8.jar
默认那次不带 -Djackson-bom.version,jar 里是 jackson-databind-2.21.4.jar。
2.21.7:Gadget 被创建,setter 也被调用了
正常消息和泛型里塞 Gadget 的请求都返回 200:
$ curl -s -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d @requests/env-plain.json localhost:18080/inbox
accepted Env, payload type = com.example.ptvdemo.PlainMsg
HTTP 200
$ curl -s -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d @requests/env-gadget.json localhost:18080/inbox
accepted Env, payload type = com.example.ptvdemo.Gadget
HTTP 200
应用日志里能看到 Gadget 的构造器和 setCmd 都执行了。直接把 Gadget 写成类型 id 的第三个请求则被拦下,原因是 Not a subtype(摘录):
2026-10-18T08:16:09.496+08:00 INFO 87409 --- [ main] com.example.ptvdemo.PtvDemoApplication : jackson-databind version: 2.21.7
2026-10-18T08:16:09.955+08:00 WARN 87409 --- [io-18080-exec-3] com.example.ptvdemo.Gadget : Gadget constructor called
2026-10-18T08:16:09.955+08:00 WARN 87409 --- [io-18080-exec-3] com.example.ptvdemo.Gadget : Gadget.setCmd called with: touch-file
2026-10-18T08:16:09.970+08:00 WARN 87409 --- [io-18080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Could not resolve type id 'com.example.ptvdemo.Gadget' as a subtype of `com.example.ptvdemo.Msg`: Not a subtype]
Boot 默认的 2.21.4 跑出来是同样的结果(摘录):
2026-10-18T08:16:04.533+08:00 INFO 87074 --- [ main] com.example.ptvdemo.PtvDemoApplication : jackson-databind version: 2.21.4
2026-10-18T08:16:05.175+08:00 WARN 87074 --- [io-18080-exec-2] com.example.ptvdemo.Gadget : Gadget constructor called
2026-10-18T08:16:05.176+08:00 WARN 87074 --- [io-18080-exec-2] com.example.ptvdemo.Gadget : Gadget.setCmd called with: touch-file
也就是说,外层的 Env 因为是 Msg 的子类型被放行以后,尖括号里的 Gadget 没有再过校验器。
2.21.8:泛型参数里的 Gadget 被拒绝
同样三个请求,正常消息照常返回 200,带 Gadget 的请求变成 400:
$ curl -s -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d @requests/env-plain.json localhost:18080/inbox
accepted Env, payload type = com.example.ptvdemo.PlainMsg
HTTP 200
$ curl -s -w '\nHTTP %{http_code}\n' -H 'Content-Type: application/json' -d @requests/env-gadget.json localhost:18080/inbox
{"timestamp":"2026-10-18T00:16:14.854+00:00","status":400,"error":"Bad Request","path":"/inbox"}
HTTP 400
日志里没有出现 Gadget constructor called。异常信息直接点名是配置的校验器拒绝了 contained type(摘录):
2026-10-18T08:16:14.415+08:00 INFO 87728 --- [ main] com.example.ptvdemo.PtvDemoApplication : jackson-databind version: 2.21.8
2026-10-18T08:16:14.845+08:00 WARN 87728 --- [io-18080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Could not resolve type id 'com.example.ptvdemo.Env<com.example.ptvdemo.Gadget>' as a subtype of `com.example.ptvdemo.Msg`: Configured `PolymorphicTypeValidator` (of type `com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator`) denied resolution of contained type `com.example.ptvdemo.Gadget`]
2026-10-18T08:16:14.883+08:00 WARN 87728 --- [io-18080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Could not resolve type id 'com.example.ptvdemo.Gadget' as a subtype of `com.example.ptvdemo.Msg`: Not a subtype]
400 的响应体是 Spring Boot 默认的错误 JSON,不带具体原因,原因只在服务端日志里。
项目信息
Jackson 用的是 Apache License 2.0,databind 仓库在 github.com/FasterXML/jackson-databind。Avro、CBOR、Protobuf、Smile 几个二进制格式模块在 github.com/FasterXML/jackson-dataformats-binary。这次的修复说明以三个版本的 wiki 发布说明为准,泛型类型 id 的细节来自 PR #6287 的描述。
一起交流
分享你的思考,让讨论更进一步。