使用 LocalDate 字段读取对象时自定义 ObjectInputStream 的意外行为
Posted
技术标签:
【中文标题】使用 LocalDate 字段读取对象时自定义 ObjectInputStream 的意外行为【英文标题】:unexpected behavior from custom ObjectInputStream when reading object with LocalDate field 【发布时间】:2021-07-18 23:25:30 【问题描述】:我正在使用来自此 repo https://github.com/kojenov/serial/tree/master/3-4.%20upload 的示例,它提供了一种方法,通过定义自定义 ObjectInputStream 并覆盖受保护的方法 resolveClass 来指定保护 Java 中表单不安全反序列化的方法我们必须在其中指定允许反序列化的类。 我的问题是我在 Planet 类中添加了一个 LocalDate 字段,当我反序列化一个序列化对象时,我得到了这个异常:
除了不支持的类之外的无效类; java.time.Ser
我在网上搜索,我找不到任何其他遇到这个问题,所以我真的很困惑。我尝试使用 LocalDate 而不是添加 LocalDateTime, 再次发生相同的错误。据我发现,类 java.time.Ser 是该包中类层次结构中某处的受保护类。 LocalDate 类是可序列化的,因此不应发生这种情况。我确定问题出在 LocalDate 中,因为如果我使该字段 transient 代码按预期工作。我是否遗漏了什么或者它只是 Java 对象序列化的一个错误? 顺便说一句,这些例子最初来自 Alexei Kojenov 的演讲,他的网站是 kojenov.com,但我找不到他的电子邮件来亲自问他。
【问题讨论】:
也许该示例认为 java.time 类对反序列化不安全? (但无法猜测原因。正如你所说,它们是可序列化的。) 我不认为该示例与特定的 java.time 包有任何关系。如果您查看自定义 ObjectInputStream 它基本上检查对象是否属于某个类名,而不是检查它的任何成员数据。再说一次,这里发生了什么:D 【参考方案1】:序列化是递归的过程,这意味着当你序列化一个复杂的对象时,首先你需要序列化它的所有属性。反序列化也会发生同样的事情。
Planet
对象包含 int
、double
和 java.lang.String
类型的字段,它们是原语,不需要特殊的(反)序列化。 LocalDate
或 LocalDateTime
不是原语,它们被序列化,然后用 SafeObjectInputStream
反序列化。
序列化破解
正如java.io.Serializable
documentation 中所说,对象可以通过定义方法writeReplace
来修改其序列化行为,甚至将序列化委托给另一个类。
JavaDoc 引用:
在将对象写入流时需要指定要使用的替代对象的可序列化类应使用确切的签名实现此特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
如果该方法存在并且可以从被序列化对象的类中定义的方法访问,则该 writeReplace 方法由序列化调用。因此,该方法可以具有私有、受保护和包私有访问。子类对该方法的访问遵循 java 可访问性规则。
LocalDate
和LocalDateTime
都利用了这种可能性并定义了writeReplace
方法。
例如,java.time.LocalDate
的实现:
private Object writeReplace()
return new Ser(Ser.LOCAL_DATE_TYPE, this);
java.time.Ser
是一个包私有最终类,用作java.time.*
对象的委托。
因此,当您序列化java.time.LocalDate
或java.time.LocalDateTime
时,实际上java.time.Ser
正在序列化。
自定义反序列化器
之前我们发现java.time.LocalDate
被序列化为java.time.Ser
。现在,让我们尝试使用SafeObjectInputStream
对其进行反序列化。
反序列化前,调用resolveClass
方法:
@Override
protected Class<?> resolveClass(ObjectStreamClass input)
throws IOException, ClassNotFoundException
if (!input.getName().equals(Planet.class.getName()))
throw new InvalidClassException("Unsupported class", input.getName());
return super.resolveClass(input);
它检查类名是否等于Planet.class.getName()
,但java.time.Ser
不等于,这就是您遇到异常的原因。
解决方案
要解决此问题,您需要将java.time.Ser
添加到受信任的类列表中。我建议接下来修改你的SafeObjectInputStream
:
public class SafeObjectInputStream extends ObjectInputStream
private final List<String> supportedClasses = List.of(Planet.class.getName(), "java.time.Ser");
public SafeObjectInputStream(InputStream inputStream) throws IOException
super(inputStream);
@Override
protected Class<?> resolveClass(ObjectStreamClass input)
throws IOException, ClassNotFoundException
if (!supportedClasses.contains(input.getName()))
throw new InvalidClassException("Unsupported class ", input.getName());
return super.resolveClass(input);
注意:List.of
是在 Java 9 中引入的。如果您的 Java 版本低于 9,您可以将其替换为 Arrays.asList
。
【讨论】:
是的,这行得通。我想尝试同样的方法,但我无法绕过java.time.Ser
无法获得它的名字的方式,但我想你的方法是一种破解,只是将类名定义为字符串(好的,毕竟我们在 if 语句中比较字符串)。我想知道解决方案 Kojenov 的作者是否会批准这是一个有效的解决方案。感谢您对委托序列化机制的出色而简洁的解释,因为即使我发现了相同的东西我也无法完全理解它。
正如我在回答中提到的,java.time.Ser
是包私有类,所以你不能从你的代码中访问它(除非你的代码没有放在 java.time 包中,我不推荐正在做)。类的字符串引用很好,因为它允许避免硬编码允许的类并将类名取出到外部配置(例如配置文件或 JVM 属性)
但是这个解决方案并不准确。可能是因为序列化的方式是递归的,我们只为***类指定了一个白名单,如果我们想要一个类型为Set
的成员,那么再次由于集合的委托序列化,我们必须指定(!)确切的实现(假设是 HashMap)和一些嵌套的委托java.util.CollSer
,并且再次可以使用无限递归Set
进行 Kojenov 提出的攻击。我找到了一种使用 Set 作为成员数据的方法,但它必须使用工厂方法 Set.of()
构造,它返回不可变集合。
创建白名单的原因是仅反序列化您信任的类。所以,Set
只是一个接口,它可以有很多实现。有人可以实现他们自己的包含恶意代码的集合。这就是为什么您只需要将实现列入白名单。您的示例中提供的过滤非常基础,可以修改为不仅使用ObjectStreamClass#getName
进行过滤,还可以使用其他字段。此外,还可以调整名称过滤。实际上,我不知道 Kojenov 是谁,我不确定我能否提供他会接受的解决方案。以上是关于使用 LocalDate 字段读取对象时自定义 ObjectInputStream 的意外行为的主要内容,如果未能解决你的问题,请参考以下文章
android打包生成apk时自定义文件名版本号。自定义项目字段等等
odoo10 many2one字段下拉更多选项时自定义排序方法
滚动时自定义 UITableViewCell 中的 UITextFields 文本出现在其他单元格中
当 React Material UI 中的 TextField 中存在值时自定义自动完成 CSS
插入数据时出错 java.time.LocalDate 在 SpringBoot 中包含字段 dateOfBirth 类型 LocalDate