Java序列化攻击和最佳实践

本文探讨了Java序列化机制的风险,如易受序列化攻击,建议在新系统中避免使用Java序列化,转而采用更安全的序列化机制如JSON。明智地实现Serializable接口,考虑使用自定义序列化或序列化代理来优化物理表示法和逻辑内容的匹配。同时,文章介绍了保护性地编写readObject和readResolve以防止序列化攻击,并推荐使用枚举类型来实现单例和实例控制。
摘要由CSDN通过智能技术生成

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());
    }
    ..
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值