Walt You - 行是知之始

《Effective Java》学习日志(十一)85:更喜欢Java序列化的替代品

2019-02-12

进入第十一部分的学习:序列化。

就是Java的框架用于将对象编码为字节流(序列化)并从其编码中重构对象(反序列化)的过程。 一旦对象被序列化,其编码可以从一个VM发送到另一个VM或存储在磁盘上以便以后反序列化。 本章重点介绍序列化的危险以及如何将序列化最小化。


学习资料主要参考: 《Effective Java Third Edition》,作者:Joshua Bloch



当序列化在1997年被添加到Java时,它被认为有点风险。 该方法已经在一种研究语言(Modula-3)中尝试过,但从未用过生产语言。 虽然程序员很少花费分布式对象的承诺是有吸引力的,但价格是隐形的构造函数和API与实现之间模糊的界限,可能存在正确性,性能,安全性和维护方面的问题。 支持者认为这些好处超过了风险,但历史已经证明不是这样。

本书前几版中描述的安全问题与一些人担心的一样严重。 在未来十年中讨论的漏洞在未来十年转变为严重漏洞,其中包括对旧金山都市交通局市政铁路(SFMTA Muni)的勒索软件攻击,该铁路在2016年11月关闭整个收费系统两天。

序列化的一个基本问题是它的攻击面太大而无法保护并且不断增长:通过在ObjectInputStream上调用readObject方法来反序列化对象图。 这个方法本质上是一个神奇的构造函数,只要类型实现了Serializable接口,就可以在类路径上实例化几乎任何类型的对象。 在反序列化字节流的过程中,此方法可以从任何这些类型执行代码,因此所有这些类型的代码都是攻击面的一部分。

攻击面包括Java平台库中的类,第二方库(如Apache Commons Collections)和应用程序本身。 即使您遵守所有相关的最佳实践并成功编写无法攻击的可序列化类,您的应用程序仍可能容易受到攻击。 引用CERT协调中心技术经理Robert Seacord的话:

Java反序列化是一个明显且存在的危险,因为它直接被应用程序广泛使用,并间接地由Java子系统(如RMI(远程方法调用),JMX(Java管理扩展)和JMS(Java消息系统))广泛使用。 不受信任的流的反序列化可能导致远程代码执行(RCE),拒绝服务(DoS)以及一系列其他漏洞利用。 应用程序即使没有做错也容易受到这些攻击。

攻击者和安全研究人员研究Java库和常用第三方库中的可序列化类型,查找在反序列化期间调用的执行潜在危险活动的方法。 这种方法称为小工具。 可以一起使用多个小工具来形成小工具链。 有时会发现一个足够强大的小工具链,允许攻击者在底层硬件上执行任意本机代码,只要有机会提交精心设计的字节流进行反序列化。 这正是SFMTA Muni攻击中发生的事情。 这次袭击没有孤立。 还有其他人,会有更多。

在不使用任何小工具的情况下,您可以通过导致需要很长时间反序列化的短流的反序列化来轻松地发起拒绝服务攻击。 这种流被称为反序列化炸弹。 这是Wouter Coekaerts的一个例子,它只使用Set和字符串:

// Deserialization bomb - deserializing this stream takes forever
static byte[] bomb() {
    Set<Object> root = new HashSet<>();
    Set<Object> s1 = root;
    Set<Object> s2 = new HashSet<>();
    for (int i = 0; i < 100; i++) {
        Set<Object> t1 = new HashSet<>();
        Set<Object> t2 = new HashSet<>();
        t1.add("foo"); // Make t1 unequal to t2
        s1.add(t1); s1.add(t2);
        s2.add(t1); s2.add(t2);
        s1 = t1;
        s2 = t2;
    } 
    return serialize(root); // Method omitted for brevity
}

对象图由201个HashSet实例组成,每个实例包含3个或更少的对象引用。 整个流的长度为5,744字节,但是在你将其反序列化之前很久就会烧掉太阳。 问题是反序列化HashSet实例需要计算其元素的哈希码。 根哈希集的2个元素本身是包含2个哈希集元素的哈希集,每个哈希集元素包含2个哈希集元素,依此类推,深度为100个级别。 因此,反序列化集会导致hashCode方法被调用超过2100次。 除了反序列化永远存在的事实之外,解串器没有任何迹象表明任何问题。 产生的对象很少,并且堆栈深度是有界的。

那么你能做些什么来抵御这些问题呢? 每当您反序列化您不信任的字节流时,您就会打开攻击。 避免序列化漏洞利用的最佳方法是永远不要反序列化任何东西。 用1983年电影“战争游戏”中名为约书亚的电脑的话来说,“唯一的胜利就是不玩。”没有理由在你编写的任何新系统中使用Java序列化。 还有其他在对象和字节序列之间进行转换的机制,可以避免Java序列化的许多危险,同时提供许多优势,例如跨平台支持,高性能,大型工具生态系统以及广泛的专业知识社区。 在本书中,我们将这些机制称为跨平台结构化数据表示。 虽然其他人有时将它们称为序列化系统,但本书避免了这种用法,以防止与Java序列化混淆。

这些表示的共同之处在于它们比Java序列化简单得多。 它们不支持任意对象图的自动序列化和反序列化。 相反,它们支持由一组属性 - 值对组成的简单结构化数据对象。 仅支持少数原始数组和数组数据类型。 这种简单的抽象结果足以构建极其强大的分布式系统,并且足够简单,可以避免从一开始就困扰Java序列化的严重问题。

领先的跨平台结构化数据表示是JSONProtocol Buffers,也称为protobuf。 JSON由Douglas Crockford设计用于浏览器 - 服务器通信,并且协议缓冲器由Google设计用于在其服务器之间存储和交换结构化数据。 即使这些表示有时被称为语言中性,JSON最初是为JavaScript开发的,而forobobf是为C ++开发的; 这两种表述都保留了其起源的痕迹。

JSON和protobuf之间最显着的区别是JSON是基于文本的,人类可读的,而protobuf是二元的,效率更高; 并且JSON完全是数据表示,而protobuf提供模式(类型)来记录和实施适当的用法。 尽管protobuf比JSON更有效,但JSON对于基于文本的表示非常有效。 虽然protobuf是二进制表示,但它确实提供了一种替代文本表示,用于需要人类可读性的用途(pbtxt)。

如果您无法完全避免Java序列化,可能是因为您在需要它的遗留系统的上下文中工作,那么您的下一个最佳选择是永远不会反序列化不受信任的数据。 特别是,您永远不应接受来自不受信任来源的RMI流量。 Java的官方安全编码指南说“不受信任的数据的反序列化本质上是危险的,应该避免。”这个句子设置为大,粗体,斜体,红色类型,它是整个文档中唯一获得此处理的文本。

如果您无法避免序列化,并且您不确定要反序列化的数据的安全性,请使用Java 9中添加的对象反序列化过滤并向后移植到早期版本(java.io.ObjectInputFilter)。此工具允许您指定在反序列化之前应用于数据流的过滤器。它以类粒度运行,允许您接受或拒绝某些类。默认接受类并拒绝潜在危险类列表称为黑名单;默认情况下拒绝类并接受假定安全的列表称为白名单。首选使用白名单而不是黑名单,因为黑名单只能保护您免受已知威胁。名为Serial Whitelist Application Trainer(SWAT)的工具可用于为您的应用程序自动准备白名单。过滤工具还可以保护您免受过多的内存使用和过深的对象图,但它不会保护您免受如上所示的序列化炸弹的攻击。

不幸的是,序列化在Java生态系统中仍然普遍存在。 如果您要维护基于Java序列化的系统,请认真考虑迁移到跨平台的结构化数据表示,即使这可能是一项耗时的工作。 实际上,您可能仍然发现自己必须编写或维护可序列化的类。 编写一个正确,安全,高效的可序列化类需要非常小心。 本章的其余部分提供了有关何时以及如何执行此操作的建议。

总之,序列化是危险的,应该避免。 如果您从头开始设计系统,请使用跨平台的结构化数据表示,例如JSON或protobuf。 不要反序列化不受信任的数据。 如果必须这样做,请使用对象反序列化过滤,但请注意,不能保证阻止所有攻击。 避免编写可序列化的类。 如果你必须这样做,请谨慎行事。


Content