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 接口(本书未讨论)。
• 不要直接或间接调用类中的任何可重写方法。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352