java反序列化会直接执行代码,因为objectinputstream读取字节流时动态加载类并调用readobject(),而某些类(如annotationinvocationhandler)的该方法内置反射或命令执行逻辑,导致恶意载荷在反序列化瞬间触发rce。

Java反序列化为什么会直接执行代码
因为 ObjectInputStream 在读取字节流时,会根据类名动态加载类、调用 readObject() 方法,而很多类(比如 AnnotationInvocationHandler、BadAttributeValueExpException)的 readObject() 内部会触发反射、方法调用或表达式解析——攻击者只要构造好恶意字节流,就能在反序列化瞬间执行任意命令。
这不是 Java 的 bug,而是设计使然:序列化本就要求还原对象状态,包括行为逻辑。所以“反序列化即执行”是默认行为,不是异常现象。
- 常见错误现象:
java.lang.ClassNotFoundException看似安全,实则只是攻击载荷没命中目标类;真正危险的是没报错却悄悄拉了外连、删了文件、启了进程 - 所有通过
ObjectInputStream读取不可信输入的地方都高危,比如 RMI、JMX、HTTP POST body、Redis 缓存值、消息队列 payload - JDK 9+ 默认启用
jdk.serialFilter全局过滤器,但老项目往往还在用 JDK 8,且该配置极易被绕过(如用sun.misc.Unsafe替代标准反序列化路径)
如何用 jdk.serialFilter 配置基础白名单
这是最轻量、兼容性最好的防御手段,适用于无法立刻重构反序列化逻辑的存量系统。
它通过 JVM 启动参数或 System.setProperty() 控制哪些类允许被反序列化,底层由 ObjectInputStream.filterCheck() 触发校验。
立即学习“Java免费学习笔记(深入)”;
- 推荐写法:
-Djdk.serialFilter=java.base/*;java.desktop/*;my.app.model.**;!*—— 显式放行基础包和你自己的模型类,最后用!*拦住一切未声明的类 - 注意通配符规则:
**匹配子包,*只匹配当前包下类(不含子包),!xxx表示排除,顺序重要,靠前的规则先匹配 - 别写
my.app.**;!*这种看似宽松的配置:一旦引入新依赖(比如com.fasterxml.jackson.databind.相关类),它们也会被放行,而这些类常含 gadget 链 - 该配置对
ObjectInputStream.resolveClass()重写无效——如果代码里手动绕过 filter 调用该方法,过滤就失效了
为什么自定义 ObjectInputStream.checkResolveClass 不够用
有人会在子类里重写 checkResolveClass() 做白名单判断,但这个方法只在类加载阶段调用,不覆盖整个反序列化过程中的其他危险点。
比如:某些 gadget 利用 readObject() 中的 Runtime.getRuntime().exec(),或通过 Transformer 链触发 InvokerTransformer,这些都在对象字段反序列化完成后才执行——此时 checkResolveClass() 早已返回。
- 典型误判场景:白名单允许
java.util.HashSet,但它在反序列化时会调用内部元素的readObject(),若元素是恶意构造的AnnotationInvocationHandler,照样触发链式调用 - 性能影响小,但防护面窄——它只管“加载哪个类”,不管“这个类会不会干坏事”
- 如果你真要重写,必须配合
enableResolveObject(false)关闭对象替换机制,并确保所有字段类型都在白名单内,否则容易漏判
替代方案:用 Jackson 或 Gson 替换原生序列化
最彻底的办法是根本不用 ObjectInputStream。JSON 类库默认不执行代码,只要禁用 DefaultTyping(Jackson)或 setLenient(true)(Gson),就能规避绝大多数反序列化漏洞。
- Jackson 推荐配置:
new ObjectMapper().disable(DefaultTyping.NATURAL)—— 关键是禁掉自动类型识别,否则@class字段仍可指定任意类 - Gson 安全用法:不要用
GsonBuilder.registerTypeAdapter()绑定带逻辑的反序列化器;避免TypeToken.getParameterized(List.class, YourClass.class)中传入不可信类型 - 兼容性代价:需统一前后端序列化协议;已有二进制缓存(如 Redis 中的
byte[])不能直接读,得迁移或双写 - 注意边界:即使换 JSON,若业务代码把反序列化结果再喂给
ScriptEngine.eval()或SpEL,依然可能 RCE
真正麻烦的从来不是加一行 jdk.serialFilter,而是确认所有反序列化入口——包括那些藏在第三方 SDK 里的、没文档说明的、测试时从不走的冷路径。一个被遗忘的 ObjectInputStream 实例,就足以让整套过滤形同虚设。










