Tips

书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code

注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

88. 防御性地编写READOBJECT方法

条目 50 里有一个不可变的日期范围类,它包含一个可变的私有Date属性。 该类通过在其构造方法和访问器中防御性地拷贝Date对象,竭尽全力维持其不变性(invariants and immutability)。 代码如下所示:

  1. // Immutable class that uses defensive copying
  2. public final class Period {
  3. private final Date start;
  4. private final Date end;
  5. /**
  6. * @param start the beginning of the period
  7. * @param end the end of the period; must not precede start
  8. * @throws IllegalArgumentException if start is after end
  9. * @throws NullPointerException if start or end is null
  10. */
  11. public Period(Date start, Date end) {
  12. this.start = new Date(start.getTime());
  13. this.end = new Date(end.getTime());
  14. if (this.start.compareTo(this.end) > 0)
  15. throw new IllegalArgumentException(
  16. start + " after " + end);
  17. }
  18. public Date start () { return new Date(start.getTime()); }
  19. public Date end () { return new Date(end.getTime()); }
  20. public String toString() { return start + " - " + end; }
  21. ... // Remainder omitted
  22. }

假设要把这个类可序列化。由于Period对象的物理表示精确地反映了它的逻辑数据内容,所以使用默认的序列化形式是合理的(条目 87)。因此,要使类可序列化,似乎只需将implements Serializable 添加到类声明中就可以了。但是,如果这样做,该类不再保证它的关键不变性了。

问题是readObject方法实际上是另一个公共构造方法,它需要与任何其他构造方法一样的小心警惕。 正如构造方法必须检查其参数的有效性(条目 49)并在适当的地方对参数防御性拷贝(条目 50),readObject方法也要这样做。 如果readObject方法无法执行这两个操作中的任何一个,则攻击者违反类的不变性是相对简单的事情。

简而言之,readObject是一个构造方法,它将字节流作为唯一参数。 在正常使用中,字节流是通过序列化正常构造的实例生成的。当readObject展现一个字节流时,问题就出现了,这个字节流是人为构造的,用来生成一个违反类不变性的对象。 这样的字节流可用于创建一个不可能的对象,该对象无法使用普通构造方法创建。

假设我们只是将implements Serializablet添加到Period类声明中。 然后,这个丑陋的程序生成一个Period实例,其结束时间在其开始时间之前。 对byte类型的值进行强制转换,其高阶位被设置,这是由于Java缺乏byte字面量,并且错误地决定对byte类型进行签名:

  1. public class BogusPeriod {
  2. // Byte stream couldn't have come from a real Period instance!
  3. private static final byte[] serializedForm = {
  4. (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
  5. 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
  6. 0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
  7. 0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
  8. 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
  9. 0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
  10. 0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
  11. 0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
  12. 0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
  13. (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
  14. 0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
  15. 0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
  16. 0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
  17. 0x00, 0x78
  18. };
  19. public static void main(String[] args) {
  20. Period p = (Period) deserialize(serializedForm);
  21. System.out.println(p);
  22. }
  23. // Returns the object with the specified serialized form
  24. static Object deserialize(byte[] sf) {
  25. try {
  26. return new ObjectInputStream(
  27. new ByteArrayInputStream(sf)).readObject();
  28. } catch (IOException | ClassNotFoundException e) {
  29. throw new IllegalArgumentException(e);
  30. }
  31. }
  32. }

用于初始化serializedForm的字节数组字面量(literal)是通过序列化正常的Period实例,并手动编辑生成的字节流生成的。 流的细节对于该示例并不重要,但是如果好奇,则在《Java Object Serialization Specification》[序列化,6]中描述了序列化字节流格式。 如果运行此程序,它会打印Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984。只需声明Period类为可序列化,我们就可以创建一个违反其类不变性的对象。

要解决此问题,请为Period提供一个readObject方法,该方法调用defaultReadObject,然后检查反序列化对象的有效性。如果有效性检查失败,readObject方法抛出InvalidObjectException异常,阻止反序列化完成:

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

虽然这样可以防止攻击者创建无效的Period实例,但仍然存在潜在的更微妙的问题。 可以通过构造以有效Period实例开头的字节流来创建可变Period实例,然后将额外引用附加到Period实例内部的私有Date属性。 攻击者从ObjectInputStream中读取Period实例,然后读取附加到流的“恶意对象引用”。 这些引用使攻击者可以访问Period对象中私有Date属性引用的对象。 通过改变这些Date实例,攻击者可以改变Period实例。 以下类演示了这种攻击:

  1. public class MutablePeriod {
  2. // A period instance
  3. public final Period period;
  4. // period's start field, to which we shouldn't have access
  5. public final Date start;
  6. // period's end field, to which we shouldn't have access
  7. public final Date end;
  8. public MutablePeriod() {
  9. try {
  10. ByteArrayOutputStream bos =
  11. new ByteArrayOutputStream();
  12. ObjectOutputStream out =
  13. new ObjectOutputStream(bos);
  14. // Serialize a valid Period instance
  15. out.writeObject(new Period(new Date(), new Date()));
  16. /*
  17. * Append rogue "previous object refs" for internal
  18. * Date fields in Period. For details, see "Java
  19. * Object Serialization Specification," Section 6.4.
  20. */
  21. byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
  22. bos.write(ref); // The start field
  23. ref[4] = 4; // Ref # 4
  24. bos.write(ref); // The end field
  25. // Deserialize Period and "stolen" Date references
  26. ObjectInputStream in = new ObjectInputStream(
  27. new ByteArrayInputStream(bos.toByteArray()));
  28. period = (Period) in.readObject();
  29. start = (Date) in.readObject();
  30. end = (Date) in.readObject();
  31. } catch (IOException | ClassNotFoundException e) {
  32. throw new AssertionError(e);
  33. }
  34. }
  35. }

要查看正在进行的攻击,请运行以下程序:

  1. public static void main(String[] args) {
  2. MutablePeriod mp = new MutablePeriod();
  3. Period p = mp.period;
  4. Date pEnd = mp.end;
  5. // Let's turn back the clock
  6. pEnd.setYear(78);
  7. System.out.println(p);
  8. // Bring back the 60s!
  9. pEnd.setYear(69);
  10. System.out.println(p);
  11. }

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

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

虽然创建了Period实例且保持了其不变性,但可以随意修改其内部组件。 一旦拥有可变的Period实例,攻击者可能会通过将实例传递给依赖于Period的安全性不变性的类来造成巨大的伤害。 这并非如此牵强:有些类就是依赖于String的不变性来保证安全性的。

问题的根源是Period类的readObject方法没有做足够的防御性拷贝。 对象反序列化时,防御性地拷贝包含客户端不能拥有的对象引用的属性,是至关重要的。 因此,每个包含私有可变组件的可序列化不可变类,必须在其readObject方法中防御性地拷贝这些组件。 以下readObject方法足以确保Period的不变性并保持其不变性:

  1. // readObject method with defensive copying and validity checking
  2. private void readObject(ObjectInputStream s)
  3. throws IOException, ClassNotFoundException {
  4. s.defaultReadObject();
  5. // Defensively copy our mutable components
  6. start = new Date(start.getTime());
  7. end = new Date(end.getTime());
  8. // Check that our invariants are satisfied
  9. if (start.compareTo(end) > 0)
  10. throw new InvalidObjectException(start +" after "+ end);
  11. }

请注意,防御性拷贝在有效性检查之前执行,并且我们没有使用Date的clone方法来执行防御性拷贝。 需要这两个细节来保护Period免受攻击(条目 50)。 另请注意,final属性无法进行防御性拷贝。 要使用readObject方法,我们必须使start和end属性不能是final类型的。 这是不幸的,但它是这两个中较好的一个做法。 使用新的readObject方法并从startend属性中删除final修饰符后,MutablePeriod类不再无效。 上面的攻击程序现在生成如下输出:

  1. Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
  2. Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017

下面是一个简单的石蕊测试(litmus test),用于确定类的默认readObject方法是否可接受:你是否愿意添加一个公共构造方法,该构造方法把对象中每个非瞬时状态的属性值作为参数,并在没有任何验证的情况下,将值保存在属性中?如果没有,则必须提供readObject方法,并且它必须执行构造方法所需的所有有效性检查和防御性拷贝。或者,可以使用序列化代理模式(serialization proxy pattern))(条目 90)。强烈推荐使用这种模式,因为它在安全反序列化方面花费了大量精力。

readObject方法和构造方法还有一个相似之处,它们适用于非final可序列化类。 与构造方法一样,readObject方法不能直接或间接调用可重写的方法(条目 19)。 如果违反此规则并且重写了相关方法,则重写方法会在子类状态被反序列化之前运行。 程序可能会导致失败[Bloch05,Puzzle 91]。

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

  • 对于具有必须保持私有的对象引用属性的类,防御性地拷贝该属性中的每个对象。不可变类的可变组件属于这一类别。

  • 检查任何不变性,如果检查失败,则抛出InvalidObjectException异常。 检查应再任何防御性拷贝之后。

  • 如果必须在反序列化后验证整个对象图(object graph),那么使用ObjectInputValidation接口(在本书中没有讨论)。

  • 不要直接或间接调用类中任何可重写的方法。

Effective Java 第三版——88. 防御性地编写READOBJECT方法的更多相关文章

  1. Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  2. Effective Java 第三版——13. 谨慎地重写 clone 方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  3. Effective Java 第三版——74. 文档化每个方法抛出的所有异常

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  4. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  5. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  6. Effective Java 第三版——50. 必要时进行防御性拷贝

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  7. Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——9. 使用try-with-resources语句替代try-finally语句

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——7. 消除过期的对象引用

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. Editor HDU - 4699 (栈)

    Problem Description   Sample Input 8 I 2 I -1 I 1 Q 3 L D R Q 2   Sample Output 2 3 Hint The followi ...

  2. Linux系统开发之路-上

    本节内容主要介绍Linux操作系统的主要特性,包括Linux与Windows操作系统的主要区别:Linux系统的分类:开发环境的推荐:Linux操作系统的安装:Linux系统下开发环境的安装和配置. ...

  3. 【java并发核心八】Fork-Join分治编程

    jdk1.7中提供了Fork/Join并行执行任务框架,主要作用就是把大任务分割成若干个小任务,再对每个小任务得到的结果进行汇总. 正常情况下,一些小任务我们可以使用单线程递归来实现,但是如果要想充分 ...

  4. Java内存空间的分配及回收

    Java中内存分为: 栈:存放简单数据类型变量(值和变量名都存在栈中),存放引用数据类型的变量名以及它所指向的实例的首地址. 堆:存放引用数据类型的实例. Java的垃圾回收 由一个后台线程gc进行垃 ...

  5. SpringCloud学习目录

    Spring Cloud直接建立在Spring Boot的企业Java创新方法上,它通过实现经过验证的模式来简化分布式.微服务风格的体系结构,从而为您的微服务带来弹性.可靠性和协调. 以上来自spri ...

  6. H5图片压缩上传

    1.所用到技术 HTML5 API:filereader.canvas 以及 formdata 目前来说,HTML5的各种新API都在移动端的webkit上得到了较好的实现.本次使用到的FileRea ...

  7. [JOISC2014]電圧

    [JOISC2014]電圧 题目大意: 一个\(n(n\le10^5)\)个点,\(m(m\le2\times10^5)\)条边的无向图.要在图中找到一条边,满足去掉这条边后,剩下的图是一个二分图,且 ...

  8. 第一篇随笔 - Hello world!

    第一篇随笔 - Hello world! 第一篇随笔 - Hello world! 第一篇随笔 - Hello world! 第一篇随笔 - Hello world! 第一篇随笔 - Hello wo ...

  9. redis的主从服务器配置

    1. redis的主从配置: (1)把redis的配置文件(reids.conf)拷贝2份 [root@192 redis]# cp redis.conf redis6380.conf [root@1 ...

  10. oracle增加表空间大小

    第一步:查看表空间的名字及文件所在位置: select tablespace_name, file_id, file_name, round(bytes/(1024*1024),0) total_sp ...