ITEM 88: 防御实现 readObject 方法

ITEM 88: WRITE READOBJECT METHODS DEFENSIVELY
  item 50 包含一个具有可变私有日期字段的不可变日期范围类。这个类通过在它的构造函数和访问器中防御性地复制 Date 对象,竭尽所能地保持它的不变性和不变性。如下:

// Immutable class that uses defensive copying
public final class Period { 
  private final Date start; 
  private final Date end; 
  /**
  * @param start the beginning of the period
  * @param end the end of the period; must not precede start 
  * @throws IllegalArgumentException if start is after end
  * @throws NullPointerException if start or end is null
  */
  public Period(Date start, Date end) {
    this.start = new Date(start.getTime()); 
    this.end = new Date(end.getTime()); 
    if (this.start.compareTo(this.end) > 0)
      throw new IllegalArgumentException( start + " after " + end);
}
  public Date start () { return new Date(start.getTime()); }
  public Date end () { return new Date(end.getTime()); }
  public String toString() { return start + " - " + end; }
  ... // Remainder omitted 
}

  假设您希望这个类是可序列化的。因为 Period 对象的物理表示准确地反映了其逻辑数据内容,所以使用默认的序列化形式(item 87)也不是不合理的。因此,要使类可序列化,似乎只需添加 implements serializable。但是,如果您这样做,该类将不再保证它的关键不变量。问题是,readObject 方法实际上是另一个公共构造函数,它需要像其他构造函数一样小心。正如构造函数必须检查其参数的有效性(item 49),并在适当的地方对参数进行防御性复制(item 50),readObject 方法也必须如此。如果readObject 方法没有做到这两件事中的任何一件,那么攻击者违反类的不变量是相对简单的事情。
  简单地说,readObject 是一个构造函数,它只接受字节流作为参数。在正常使用中,字节流是通过序列化一个正常构造的实例来生成的。当向 readObject 提供一个字节流时,问题就出现了,该字节流是人为构造来生成违反其类的不变量的对象的。这样的字节流可以用来创建一个不可能的对象,这是使用普通构造函数无法创建的。
  假设我们只是将 implements Serializable 添加到 Period 的类声明中。然后,这个丑陋的程序将生成一个周期实例,它的结束先于它的开始。对字节值的强制转换,其高阶位被设置,是 Java 缺乏字节文字的结果,加上不幸的决定,使字节类型签名:

public class BogusPeriod {
// Byte stream couldn't have come from a real Period instance!
  private static final byte[] serializedForm = {(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8, 0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02, 0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f, 0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75, 0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a, (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00, 0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf, 0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03, 0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22, 0x00, 0x78};
  public static void main(String[] args) {
    Period p = (Period) deserialize(serializedForm); 
    System.out.println(p);
  }
  // Returns the object with the specified serialized form 
  static Object deserialize(byte[] sf) {
    try {
      return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
    } catch (IOException | ClassNotFoundException e) {
      throw new IllegalArgumentException(e); 
    }
  } 
}

  用于初始化 serializedForm 的字节数组文字是通过序列化一个普通的 Period 实例并手动编辑产生的字节流生成的。流的细节对于这个示例并不重要,但是如果您好奇的话,序列化字节流格式在 Java 对象序列化规范[serialization, 6]中有描述。如果你运行这个程序,它打印 1999 年 1 月 1 日星期五12:00:00 PST - 1984年1月1日星期日12:00:00 PST。简单地声明 Period serializable 使我们能够创建违反其类不变量的对象。
  要解决这个问题,为Period提供一个readObject方法,该方法调 defaultReadObject,然后检查反序列化对象的有效性。如果有效性检查失败,readObject 方法抛出 InvalidObjectException,阻止反序列化完成:

// readObject method with validity checking - insufficient!
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
  s.defaultReadObject();
  // Check that our invariants are satisfied 
  if (start.compareTo(end) > 0)
    throw new InvalidObjectException(start +" after "+ end); 
}

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

public class MutablePeriod { 
  // A period instance 
  public final Period period;
  // period's start field, to which we shouldn't have access 
  public final Date start;
  // period's end field, to which we shouldn't have access 
  public final Date end;
  public MutablePeriod() { 
    try {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream out = new ObjectOutputStream(bos);
      // Serialize a valid Period instance 
      out.writeObject(new Period(new Date(), new Date()));
      /*
      * Append rogue "previous object refs" for internal * Date fields in Period. For details, see "Java
      * Object Serialization Specification," Section 6.4. 
      */
      byte[]ref={0x71,0,0x7e,0,5}; //Ref#5 
      bos.write(ref); // The start field
      ref[4] = 4; // Ref # 4
      bos.write(ref); // The end field
      // Deserialize Period and "stolen" 
      Date references ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); 
      period = (Period) in.readObject();
      start = (Date) in.readObject();
      end = (Date) in.readObject();
    } catch (IOException | ClassNotFoundException e) { 
      throw new AssertionError(e);
    } 
  }
}

public static void main(String[] args) { 
  MutablePeriod mp = new MutablePeriod(); 
  Period p = mp.period;
  Date pEnd = mp.end;
  // Let's turn back the clock 
  pEnd.setYear(78); 
  System.out.println(p);
  // Bring back the 60s! 
  pEnd.setYear(69); 
  System.out.println(p);
}

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

Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978 
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969

  虽然创建周期实例时其不变量保持不变,但可以随意修改其内部组件。一旦拥有了一个可变的 Period 实例,攻击者可能会将该实例传递给一个依赖于 Period 的不变性来保证其安全性的类,从而造成极大的伤害。这并不是牵强附会的:有些类依赖于字符串的不变性来保证其安全性。
  问题的根源是 Period 的 readObject 方法没有做足够的防御性复制。反序列化对象时,关键是要防御性地复制任何包含客户端不能拥有的对象引用的字段。因此,每个包含私有可变组件的可序列化不可变类必须防御性地在其 readObject 方法中复制这些组件。下面的 readObject 方法足以确保 Period 的不变性,并保持其不变性:
``
// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Defensively copy our mutable components
start = new Date(start.getTime());
end = new Date(end.getTime());
// Check that our invariants are satisfied
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after "+ end);
}

  注意,防御复制是在有效性检查之前执行的,并且我们没有使用 Date 的克隆方法来执行防御复制。这两个细节都是保护周期免受攻击所必需的(item 50)。还要注意,对final 字段不可能进行防御性复制。要使用 readObject 方法,我们必须使 start 和 end字段是非 final 的。这是不幸的,但这只是两害相权取其轻。有了新的 readObject 方法并从 start 和 end 字段中删除了最后一个修饰符后,MutablePeriod 类就变得无效了。以上攻击程序现在生成如下输出:

Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017

  这里有一个简单的石蕊试法来决定默认的 readObject 方法是否可以被类接受:您是否愿意添加一个公共构造函数,它将对象中每个非瞬态字段的值作为参数,并将值存储在字段中而不进行任何验证?如果没有,则必须提供一个 readObject 方法,它必须执行构造函数所需的所有有效性检查和防御性复制。或者,您可以使用序列化代理模式(item 90)。强烈推荐使用此模式,因为它在安全反序列化方面花费了大量精力。
  readObject 方法和应用于非最终序列化类的构造函数之间还有一个相似之处。与构造函数一样,readObject 方法不能直接或间接调用可重写的方法(item 19)。如果违反了此规则,并且覆盖了所涉及的方法,则覆盖的方法将在反序列化子类的状态之前运行。程序失败很可能导致 Bloch05, Puzzle 91。
  总而言之,在编写 readObject 方法时,请采用这样一种思维方式:即编写公共构造函数时,无论给定的是什么字节流,都必须生成一个有效实例。不要假设字节流表示一个实际的序列化实例。虽然本项目中的示例涉及使用默认序列化表单的类,但所引发的所有问题都同样适用于使用自定义序列化表单的类。下面是编写 readObject 方法的指导原则:
• 对于具有必须保持私有的对象引用字段的类,防御性地将每个对象复制到这样的字段中。不可变类的可变组件属于这一类。
• 检查所有不变量,如果检查失败则抛出 InvalidObjectException。检查应该遵循任何防御性复制。
• 如果在反序列化后必须验证整个对象图,请使用 ObjectInputValidation 接口(本书未讨论)。
• 不要直接或间接调用类中的任何可重写方法。

你可能感兴趣的:(ITEM 88: 防御实现 readObject 方法)