Java序列化攻击和最佳实践

85. 其他序列化优于Java序列化

Java序列化机制很危险,在新编写的系统中,没有理由再使用Java序列化,应该使用其他序列化机制,例如跨平台的JSON序列化;尤其不要反序列化任何不信任的数据,因为容易遭受序列化攻击。

原因在于,反序列化不被信任的数据,容易受到序列化攻击,例如远程代码执行(RCE),拒绝服务(DoS)等等。研究人员可以基于Java序列化机制开发出很多巧妙的攻击片段。例如:

public class BombSerializable {
   
    public static void main(String[] args) throws IOException, ClassNotFoundException {
   
        serialize();
        System.out.println("serialized");
        deserialize();
    }

    // Deserialization bomb - deserializing this stream takes forever
    static void serialize() throws IOException {
   
        Set<Object> root = new HashSet<>();
        Set<Object> s1 = root;
        Set<Object> s2 = new HashSet<>();
        for (int i = 0; i < 100; i++) {
   
            Set<Object> t1 = new HashSet<>();
            Set<Object> t2 = new HashSet<>();
            t1.add("foo"); // Make t1 unequal to t2
            s1.add(t1);
            s1.add(t2);
            s2.add(t1);
            s2.add(t2);
            s1 = t1;
            s2 = t2;
        }
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("bomb.txt"));
        oos.writeObject(root);
    }

    static Object deserialize() throws IOException, ClassNotFoundException {
   
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("bomb.txt"));
        Object o = ois.readObject();
        return o;
    }

}

上述代码在反序列的时候会卡住,原因在于,调用流的读取/写入方法之后,会自动调用序列化对象的readObject, wirteObject等方法。但HashSet重写了readObject,wirteObject,如下代码,它会根据集合的数量从流中调用相应次数的写入/读取方法,递归调用。原来是没有问题的,因为Java序列化发现当前对象已经序列化过,它不会再深入递归。但关键在于,readObject方法调用了map.put->hash->hashCode,该方法会递归遍历所有元素。这样一来,hashCode的方法就会被调用超过2^100次方次,造成反序列化爆炸。

以上的例子,指出了Java序列化的风险本质——序列化之后调用的方法(例如readObject,wirteObject)由序列化对象提供,尤其是反序列化的时候,这些超出了调用者(程序)的控制。

86. 明智地实现Serializable

Serializable是一个标记接口,看起来只要实现它就能享受序列化机制,非常简单,毫不费力,但实际上需要付出长期开销非常高。以下列出相关建议并解释原因。

最大的代价是,一旦类被发布,大大降低了“改变这个类的实现”的灵活性

相当于类所有的私有和包私有的实例域都会变成导出API的一部分——你必须保证这些域一直符合序列化要求,这就不符合“最低限度访问原则”。
而且一旦类内部改变,自动生成的序列化UID就会改变,新版本可能无法反序列化旧版本的产物。总之,一旦实现类序列化接口,就意味着未来的实现受到众多限制。

第二个代价是,增加了出现BUG和安全漏洞的可能性。

Java序列化绕开了构造器,同时也绕开了构造器可能设定的类约束。

第三个代价是,测试成本。

如果要保证新版本序列化的实例,旧版本可以反序列化(应用兼容性要求),那么就必须对所有旧版本的类进行测试,开销 = 序列化类的数量 x 版本数量。

是否实现Serializable,需要谨慎地决定

正如上面及上一条目提及,首先Java的序列化机制本身就是有缺陷的,实现Serializable意味着,以后所有版本中,相关的类都必须强制遵循约定(很可能要一直使用危险的Java序列化机制)。所以看起来是一个“免费”的标记接口,其实“免费的才是最贵的”。
同理,为继承而设计的类也尽可能不要使用Serializable序列化,因为这会给未来的子类、所有实现带来沉重的负担。

内部类不该使用Serializable

内部类指非静态内部类,它们持有对外部类的引用,目前内部类的序列化形式没有清楚的定义。但静态内部类可以,因为静态内部类不持有外部引用。

87. 考虑使用自定义序列化替换默认序列化

物理表示法与逻辑内容

只有一个对象的序列化物理表示法 = 逻辑内容,才可以使用默认序列化

怎么理解呢?书中有一个精彩的案例如下。这是一个用于保存字符串集合的双向链表,如果直接使用默认序列化,则物理上(物理存储)会直接保留双向链表的形式,实际上只需要保存字符串集合,以及集合的size即可。

// Awful candidate for default serialized form
public final class StringList implements Serializable {
   
   private int size = 0;
   private Entry head = null;
   private static class Entry implements Serializable {
   
       String data;
       Entry next;
       Entry  previous;
   }
   ... // Remainder omitted
}

当物理表示法不等于逻辑内容的时候,使用默认序列化,会增加很多没有必要的开销。
具体而言,有如下缺点;

  1. 这个类的导出API会永远束缚在内部表示法上。例如在案例中,明明是在内部维护一个私有的链表,但由于使用了默认序列化形式,导致即使后面将字符串集合内部存储换为列表,你还是得维护这个链表,因为之前的序列化导出API就是链表。
  2. 空间开销。
  3. 性能开销。序列化并不知道内部的拓扑结构,他只能递归地进行昂贵的图遍历,并且容易引起stack over flow
  4. 破坏拓扑结构,默认序列化方式完全不能用。在上述案例中,虽然慢,但是拓扑还是正确的,如果是像散列表这种对象,那么默认的序列化会破坏散列表的整个拓扑结构。

这时候可以考虑自定义序列化形式,只需要序列化字符串和size即可。

// StringList with a reasonable custom serialized form
public final class StringList implements Serializable {
   
    private transient int size   = 0;
    private transient Entry head = null;
    // No longer Serializable!
    private static class Entry {
   
        String data;
        Entry next;
        Entry  previous;
    }
    // Appends the specified string to the list
    public final void add(String s) {
    ... }
    /**
    * Serialize this {@code StringList} instance.
    *
    * @serialData The size of the list (the number of strings * it contains) is emitted ({@code int}), followed by all of * its elements (each a {@code String}), in the proper
    * sequence.
    */
    private void writeObject(ObjectOutputStream s)
            throws IOException {
   
        s.defaultWriteObject();
        s.writeInt(size);
        // Write out all elements in the proper order.
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data);
    }
    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
   
        s.defaultReadObject();
        int numElements = s.readInt();
        // Read in all elements and insert them in list
        for (int i = 0; i < numElements; i++)
            add((String) s.readObject());
    }
    ..
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值