ObjectInputStream 默认不校验类,反序列化即执行任意代码;JDK 9+ 必须显式设置 ObjectInputFilter,null 值等于全放行;低于 JDK 9 或需动态白名单时应重写 resolveClass();全局 jdk.serialFilter 易被绕过,不可单独依赖。

ObjectInputStream 默认不做类校验,直接反序列化等于执行任意代码
只要攻击者能控制输入流(比如 HTTP 请求体、Redis 缓存值、MQ 消息),ObjectInputStream 就会按字节流里的类名动态加载类,并调用其 readObject() 方法——而很多类(如 AnnotationInvocationHandler、BadAttributeValueExpException)的该方法内部会触发反射、命令执行或表达式解析。这不是 bug,是 Java 序列化机制的设计本质:反序列化即执行。
JDK 9+ 必须显式设置 ObjectInputFilter,null 值 = 全放行
ObjectInputFilter 是 JDK 9 引入的 JEP 290 机制,但它默认为 null,等同于没开。必须在构造 ObjectInputStream 实例后、调用 readObject() 前,用 setObjectInputFilter() 绑定过滤器,晚了就无效。
- 推荐用
ObjectInputFilter.Config.createFilter()构建规则字符串,例如:"com.myapp.dto.*;maxdepth=5;maxrefs=100;maxarray=50000" - 通配符
*不匹配子包,要写com.myapp.**才覆盖嵌套路径 - 规则顺序重要:
!*放最后,com.myapp.**放前面;以!开头的规则优先级更高 - 数组长度、对象图深度、引用次数等限制能防 DoS,但不能替代类白名单
低于 JDK 9 或需动态判断时,必须重写 resolveClass()
JDK 8 及更早版本没有 ObjectInputFilter,且某些场景(如多租户系统需按租户切换白名单)必须动态校验。这时唯一可靠方式是继承 ObjectInputStream 并重写 resolveClass()。
- 不能只靠
readObject()前检查——类加载发生在resolveClass()内部 - 从
ObjectStreamClass desc取类名用desc.getName(),别用Class.forName()触发提前加载 - 白名单建议用
Set<string></string>存储,contains()查找,避免正则匹配带来的性能抖动 - 非法类名必须抛
ClassNotFoundException,抛InvalidClassException可能被绕过
全局 jdk.serialFilter 参数容易被绕过,不建议单独依赖
通过 JVM 参数 -Djdk.serialFilter 或 System.setProperty("jdk.serialFilter", "...") 设置全局过滤器,看似省事,但有硬伤:
- 对已重写
resolveClass()的子类无效——代码里手动调用super.resolveClass()就绕过了 filter - 无法区分不同业务上下文(比如管理后台和用户接口需要不同白名单)
- 老项目常混用
sun.misc.Unsafe、AtomicReferenceFieldUpdater等非标准反序列化路径,这些不走filterCheck() - 某些 gadget 链在字段反序列化完成后才触发(如
InvokerTransformer),此时 filter 早已退出
真正关键的点是:过滤必须发生在类加载那一刻,且不能被子类逻辑绕过;白名单粒度要到具体类名或精确包路径,模糊规则(如 java.util.*)等于敞开大门。


















