Effective Java(3rd)-Item88 防御性地编写readObject方法

  项目50包含一个具有可变私有日期字段的不可变日期范围类。该类通过在构造函数和访问器中防御性地复制Date对象,不遗余力地保持其不变性和不可变性。类是这样的:


Effective Java(3rd)-Item88 防御性地编写readObject方法_第1张图片
image.png

  假设您决定让这个类可序列化。由于句点对象的物理表示精确地反映了它的逻辑数据内容,所以使用默认的序列化形式并不是不合理的( item87)。因此,要使类可序列化,似乎只需将实现Serializable的单词添加到类声明中。但是,如果这样做,该类将不再保证它的临界不变量。

  问题是readObject方法实际上是另一个公共构造函数,它需要与任何其他构造函数相同的关注。正如构造函数必须检查其参数的有效性(item49 )并在适当的地方复制参数(item50)一样,readObject方法也必须这样做。如果readObject方法没有做到这两件事中的任何一件,那么攻击者就很容易违反类的不变量。

  松散地说,readObject是一个构造函数,它唯一的参数是字节流。在正常使用中,字节流是通过序列化一个正常构造的实例生成的。当readObject呈现一个字节流时,问题就出现了,这个字节流是人为构造的,用来生成一个违反类不变量的对象。这样的字节流可用于创建一个不可能的对象,而该对象不能使用普通构造函数创建。
  假设我们只是简单地将Serializable添加到类声明Period中。然后,这个丑陋的程序将生成一个Period实例,其结束先于开始。对字节值进行强制转换,其高阶位被设置,这是由于Java缺乏字节字面量,并且错误地决定对字节类型进行签名:


Effective Java(3rd)-Item88 防御性地编写readObject方法_第2张图片
image.png

  用于初始化serializedForm的字节数组文本是通过序列化一个普通Period实例并手工编辑得到的字节流生成的。流的细节对示例并不重要,但是如果您感兴趣,可以在Java对象序列化中描述序列化字节流格式规范[序列化,6]。如果您运行这个程序,它将打印1月1日星期五
1999年太平洋夏令时12:00:00 - 1984年1月1日星期日12:00:00。只需声明Period serializable,就可以创建一个违反其类不变量的对象。
  要解决此问题,请为Period提供一个readObject方法,该方法调用defaultReadObject,然后检查反序列化对象的有效性。如果有效性检查失败,readObject方法抛出InvalidObjectException,阻止反序列化完成:


Effective Java(3rd)-Item88 防御性地编写readObject方法_第3张图片
image.png

  虽然这可以防止攻击者创建无效的Period实例,但还有一个更微妙的问题仍然潜伏着。可以通过创建一个字节流来创建一个可变的句点实例,该字节流以一个有效的Period实例开始,然后向句点实例内部的私有日期字段追加额外的引用。攻击者从ObjectInputStream中读取Period实例,然后读取附加到流中的“流氓对象引用”。这些引用使攻击者能够访问私有对象引用的对象
句点对象中的日期字段。通过修改这些日期实例,攻击者可以修改Period实例。下面的类演示了这种攻击:


Effective Java(3rd)-Item88 防御性地编写readObject方法_第4张图片
image.png

Effective Java(3rd)-Item88 防御性地编写readObject方法_第5张图片
image.png

  要查看攻击的实际情况,请运行以下程序:


Effective Java(3rd)-Item88 防御性地编写readObject方法_第6张图片
image.png

  在我的语言环境中,运行这个程序会产生以下输出:


image.png

  虽然创建Period实例时保留了它的不变量,但是可以随意修改它的内部组件。一旦拥有一个可变的Period实例,攻击者可能会将实例传递给一个依赖于Period的不变性来保证其安全性的类,从而造成极大的危害。这并不是牵强附会的:有些类依赖于String的不变性来保证其安全性。
  问题的根源在于Period的readObject方法没有进行足够的防御性复制。当对象被反序列化时,防御性地复制任何包含客户端不能拥有的对象引用的字段是至关重要的。因此,每个包含私有可变组件的可序列化不可变类都必须防御性地在其readObject方法中复制这些组件。下面的readObject方法足以保证周期的不变性,并保持其不变性:

Effective Java(3rd)-Item88 防御性地编写readObject方法_第7张图片
image.png

  注意,防御副本是在有效性检查之前执行的,我们没有使用Date的clone方法来执行防御副本。这两个细节都需要保护期间免受攻击( item50)。还要注意,防御性复制不可能用于final字段。要使用readObject方法,必须使start和end字段非final。这是不幸的,但却是两害相权取其轻。使用新的readObject方法,并从开始和结束字段中删除最后的修饰符,MutablePeriod类将无效。上面的攻击程序现在生成这个输出:

image.png

  下面是一个简单的石蕊测试,用于判断默认的readObject方法是否适合类:您是否愿意添加一个公共构造函数,它将对象中每个非瞬态字段的值作为参数,并在没有任何验证的情况下将值存储在字段中?如果没有,则必须提供readObject方法,并且它必须执行构造函数所需的所有有效性检查和防御性复制。或者,您可以使用序列化代理模式(item90 )。强烈推荐使用这种模式,因为它会在安全反序列化方面花费大量精力。

  readObject方法和构造函数之间还有一个相似之处,适用于非最终序列化类。与构造函数一样,readObject方法不能直接或间接调用可覆盖的方法(item19)。如果违反了这条规则,并且涉及的方法被覆盖,则覆盖方法将在子类的状态反序列化之前运行。程序失败很可能导致[Bloch05, Puzzle 91]。

  总而言之,无论何时编写readObject方法,都要采用这样一种思维方式,即您正在编写一个公共构造函数,该构造函数必须生成一个有效的实例,而不管给定的是什么字节流。不要假设字节流表示实际的序列化实例。虽然本项目中的示例涉及使用默认序列化表单的类,但是所引发的所有问题都同样适用于具有自定义序列化表单的类。下面是编写readObject方法的指导原则:

  • 对于具有必须保持私有的对象引用字段的类,防御性地复制该字段中的每个对象。不可变类的可变组件属于这一类。
  • 检查任何不变量,如果检查失败,则抛出InvalidObjectException。
    检查应该遵循任何防御性复制。
  • 如果必须在反序列化后验证整个对象图,请使用
    ObjectInputValidation接口(在本书中没有讨论)。
  • 不要直接或间接地调用类中任何可覆盖的方法。
      
    本文写于2019.7.24,历时1天

你可能感兴趣的:(Effective Java(3rd)-Item88 防御性地编写readObject方法)