readobject是反序列化钩子函数,攻击者可通过恶意字节流触发其执行任意代码;linkedhashset、treeset、priorityqueue等集合因内部回调逻辑易成入口;安全做法是白名单校验类名或改用json/yaml等协议。

Java反序列化时readObject为什么会执行恶意代码
因为readObject不是普通读取方法,它是反序列化流程中可被重写的钩子函数——只要类实现了Serializable且自定义了readObject,JVM在还原对象时就会调用它,不管数据来源是否可信。
- 常见错误现象:
ClassNotFoundException没报,但程序突然执行了文件删除、命令执行或远程连接 - 典型使用场景:从网络接收字节流(如RMI、HTTP POST body、Redis缓存)后直接调用
ObjectInputStream.readObject() - 关键风险点:攻击者构造恶意字节流,让
readObject触发链式反射调用(比如通过AnnotationInvocationHandler、BadAttributeValueExpException等已知gadget) - 性能影响几乎为零,但安全代价极高——一次调用就可能失守
哪些集合类特别容易触发readObject漏洞
不是所有集合都危险,但LinkedHashSet、TreeSet、PriorityQueue这类内部依赖比较器(Comparator)或回调逻辑的集合,在反序列化时会主动触发readObject甚至compare方法,成为攻击入口。
-
LinkedHashSet.readObject会重建内部HashMap,如果元素是可控类型,后续可能触发其readObject -
TreeSet.readObject会重新设置comparator,若 comparator 是 attacker-controlled lambda 或动态代理,就可能执行任意逻辑 -
PriorityQueue.readObject会调用heapify,进而触发元素的compareTo——而这个方法完全可能被污染 - 别以为
ArrayList就安全:如果它装的是你自定义的、重写了readObject的类,照样中招
怎么安全地反序列化集合(不靠禁用)
禁用readObject或全局关闭反序列化只是权宜之计;真正可行的是控制反序列化上下文,让ObjectInputStream只认可信类。
- 最简实践:继承
ObjectInputStream,重写resolveClass,白名单校验ObjectStreamClass.getname(),不在列表里就抛InvalidClassException - 用
SerialKiller或Jackson(开启DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)替代原生ObjectInputStream,它们默认拒绝未知类 - 对集合本身,优先用
Arrays.asList()或ImmutableList.of()代替反序列化——集合内容应走JSON/YAML等结构化协议传输 - 如果必须用Java序列化,确保集合元素类型是final、无
readObject、无内部状态回调的简单POJO(比如只含String/int字段)
readObject里的defaultReadObject为什么不能乱放
很多开发者以为“先调defaultReadObject再做校验”就安全了,其实错在顺序——defaultReadObject会立即还原所有字段,包括攻击者精心构造的恶意对象引用,校验逻辑还没跑,危害已经发生。
立即学习“Java免费学习笔记(深入)”;
- 正确顺序:先校验字段值(比如检查
file_path是否含../)、再调defaultReadObject、最后做二次约束(比如强制status只能是枚举值) - 常见坑:
transient字段不会被defaultReadObject还原,但如果你手动反序列化它(比如用in.readObject()),就得自己负责校验 - 更隐蔽的坑:某些 JDK 版本中,
defaultReadObject会触发字段类型的readObject,形成递归调用链——哪怕你的类没重写,它依赖的类写了,也危险
readObject存在,且输入不可信,那个方法体就是攻击者的入口点——和它是不是集合、有没有泛型、用没用stream,都没关系。









