当序列化在1997年添加到Java中时,它被认为有一定的风险。这种方法曾在研究语言(模块3)中尝试过,但从未在生产语言中使用过。虽然程序员不费什么力气就能实现分布式对象的承诺很吸引人,代价是看不见的构造函数和API与实现之间模糊的界限,在性能、安全性和维护的正确性上存在潜在的问题。支持者认为收益大于风险,但历史证明并非如此。
在本书之前的版本中描述的安全问题,结果和一些人担心的一样严重。早期讨论过的漏洞在接下来的十年里,21世纪头十年变成了严重的网络攻击,其中著名的包括针对旧金山大都会运输署(San Francisco Metropolitan Transit Agency)的勒索软件攻击2016年11月,市政铁路(SFMTA Muni)关闭了整个收费系统两天[Gallagher16]。
序列化的一个基本问题是它的攻击面太大而无法保护,并且不断增长:通过调用ObjectInputStream上的readObject方法来反序列化对象图。这个方法本质上是一个神奇的构造函数,可以用来实例化类路径上几乎任何类型的对象,只要该类型实现Serializable接口。在反序列化字节流的过程中,此方法可以执行来自任何这些类型的代码,因此所有这些类型的代码都是攻击面的一部分。
攻击面包括Java平台库、第三方库(如Apache Commons collection)和应用程序本身中的类。即使您坚持所有相关的最佳实践并成功地编写了不受攻击的可序列化类,您的应用程序仍然可能是脆弱的。引用CERT协调中心技术经理Robert Seacord的话:
image.png
(Java反序列化是一个明显而又存在的危险,因为它被应用程序直接和间接地广泛使用,比如RMI
(远程方法调用)、JMX (Java管理扩展)和
JMS (Java消息传递系统)。不可信流的反序列化可能导致远程代码执行(RCE)、拒绝服务(DoS)和一系列其他攻击。应用程序很容易受到这些攻击,即使它们没有做错什么。(Seacord17))
攻击者和安全研究人员研究Java库和常用的第三方库中的可序列化类型,寻找在反序列化过程中调用的执行潜在危险活动的方法。这种方法称为gadget。多个gadget可以同时使用,形成一个gadget链。偶尔会发现一个小部件链,它的功能足够强大,允许攻击者在底层硬件上执行任意的本机代码,只允许提交精心设计的字节流进行反序列化。这正是SFMTA Muni袭击中发生的事情。这次袭击并不是孤立的,已经有了,而且还会有更多。
不使用任何小工具,您就可以通过导致需要很长时间反序列化的短流反序列化,轻松地发起拒绝服务攻击。种流被称为反序列化炸弹[Svoboda16]。下面是Wouter Coekaerts的一个例子,它只使用哈希集和字符串[Coekaerts15]:
image.png
对象图由201个HashSet实例组成,每个实例包含3个或更少的对象引用。整个流的长度为5,744字节,但是在您对其进行反序列化之前,太阳就已经耗尽了。问题是反序列化HashSet实例需要计算其元素的哈希码。根哈希集的两个元素本身就是包含哈希集的哈希集2个哈希集合元素,每个哈希集合元素包含2个哈希集合元素,以此类推,100等级深度.因此,反序列化set会导致hashCode方法被调用超过2^100次。除了反序列化会持续很长时间之外,反序列化器没有任何错误的迹象。生成的对象很少,并且堆栈深度是有界的。
那么你能做些什么来抵御这些问题呢?当您反序列化一个您不信任的字节流时,您就会受到攻击。避免序列化利用的最佳方法是永远不要反序列化任何东西。用1983年电影《战争游戏》(WarGames)中名为约书亚(Joshua)的电脑的话来说,“唯一的制胜招就是不玩。”没有理由在编写的任何新系统中使用Java序列化。还有其他一些机制可以在对象和字节序列之间进行转换,从而避免了Java序列化的许多危险,同时提供了许多优势,比如跨平台支持、高性能、大型工具生态系统和广泛的专家社区。在本书中,我们将这些机制称为跨平台结构数据表示。虽然其他人有时将它们称为序列化系统,但本书避免使用这种用法,以免与Java序列化混淆。
这些表示的共同之处在于,它们要比原来Java序列化简单得多.它们不支持任意对象图的自动序列化和反序列化。相反,它们支持由一组属性值对组成的简单结构化数据对象。只支持少数基本数据类型和数组数据类型。事实证明,这个简单的抽象足以构建功能极其强大的分布式系统,而且足够简单,可以避免Java序列化从一开始就存在的严重问题.
领先的跨平台结构化数据表示是JSON [JSON]和协议缓冲区,也称为protobuf [protobuf]。JSON是由
Douglas Crockford用于浏览器-服务器通信,协议缓冲区由谷歌设计用于在其服务器之间存储和交换结构化数据。尽管这些表示有时被称为语言中立的,
JSON最初是为JavaScript开发的,而protobuf最初是为c++开发的;这两种表述都保留了其起源的痕迹。
SON和protobuf之间最显著的区别是JSON是基于文本的,并且是人类可读的,而protobuf是二进制的,而且本质上更有效;JSON是一种专门的数据表示,而protobuf提供模式(类型)来记录和执行适当的用法。虽然protobuf比JSON更有效,但是JSON对于基于文本的表示非常有效。虽然protobuf是一种二进制表示,但它确实提供了另一种文本表示,可用于需要人类可读性的地方(pbtxt)。
如果您不能完全避免Java序列化,可能是因为您在需要它的遗留系统上下文中工作,那么您的下一个最佳选择就是永远不要反序列化不可信的数据。特别是,您不应该接受来自不可信源的RMI流量。Java的官方安全编码指南说
“不可信数据的反序列化本质上是危险的,应该避免。”:这个句子是用大的、粗体的、斜体的、红色的字体设置的,它是整个文档中唯一得到这种处理的文本[Java-secure]。
如果无法避免序列化,并且不能绝对确定反序列化的数据的安全性,请使用Java9添加进来的对象反序列化筛选
并向后移植到早期版本(Java .io. objectinputfilter)。该工具允许您指定一个过滤器,该过滤器在反序列化数据流之前应用于数据流。它在类粒度上运行,允许您接受或拒绝某些类。默认接受类并拒绝潜在危险类的列表称为黑名单;在缺省情况下拒绝类并接受假定安全的类的列表称为白名单。比起黑名单,更喜欢白名单,因为黑名单只保护你免受已知的威胁。一个工具叫串行白名单应用培训器(SWAT)可用于为您的应用程序自动准备白名单[Schneider16]。过滤工具还将保护您免受过度内存使用和过于深入的对象图的影响,但它不能保护您免受如上面所示的序列化炸弹的影响。
不幸的是,序列化在Java生态系统中仍然很普遍。如果您正在维护一个基于Java序列化的系统,请认真考虑迁移到跨平台的结构化数据表示,尽管这可能是一项耗时的工作。实际上,您可能仍然需要编写或维护一个可序列化的类。编写一个正确、安全、高效的可序列化类需要非常小心。本章的其余部分将提供何时以及如何进行此操作的建议。
总之,序列化是危险的,应该避免。如果您从头开始设计一个系统,可以使用跨平台的结构化数据表示,如JSON或protobuf。不要反序列化不可信的数据。如果必须这样做,请使用对象反序列化过滤,但要注意,它不能保证阻止所有攻击。避免编写可序列化的类。如果你必须这样做,一定要非常小心。
本文写于2019.7.23,历时1天
网友评论