Skip to content

Latest commit

 

History

History
55 lines (37 loc) · 8.75 KB

第85条:其他方法优先于Java序列化 4debf4b3b71c42e3a5924abd63a5e28e.md

File metadata and controls

55 lines (37 loc) · 8.75 KB

第85条:其他方法优先于Java序列化

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

本书前几版中描述的安全问题与某些人担心的一样严重。2000年代早期讨论的小漏洞在接下来的十年被转化为严重的漏洞并被利用了,其中包括对旧金山都市交通局市政铁路(SFMTA Muni)的勒索软件攻击,该铁路在2016年11月关闭整个收费系统两天[Gallagher16]。

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

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

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

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

在不使用任何小工具的情况下,你可以轻松地通过反序列化一个需要很长的反序列化时间的短流来发起拒绝服务(denial-of-service)攻击,这种流被称为反序列化炸弹(deserialization bombs )[Svoboda16]。这是Wouter Coekaerts的一个例子,它只使用哈希集和字符串[Coekaerts15]

// 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字节,但是远在你将其反序列化之前就会烧掉太阳【CPU】。问题在于反序列化HashSet实例需要计算其元素的哈希码。根哈希集的2个元素本身是包含2个哈希集元素的哈希集,每个哈希集元素包含2个哈希集元素,依此类推,深度为100个级别。

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

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

领先的跨平台结构化数据表示是JSON [JSON]和Protocol Buffers,也称为protobuf [Protobuf]。JSON由Douglas Crockford设计用于浏览器-服务器通信,并且Protocol Buffers由Google设计用于在其服务器之间存储和交换结构化数据。即使这些表示有时被称为中立语言(language-neutral),JSON最初是为JavaScript开发的,而protobuf是为C ++开发的; 这两种表现了它们保留了其起源的痕迹。

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

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

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

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

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