序列化破坏单例模式以及如何防御

序列化介绍

Serializable介绍

如果单例类实现了序列化接口Serializable,那么就可能被序列化攻击来破坏单例模式。

public class Singleton implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    private static Singleton singleton  = new Singleton();

    private Singleton(){
    
    
    }
    public static Singleton getInstance(){
    
    
        return singleton;
    }

}

ObjectOutputStream 介绍
FileOutputStream介绍
FileInputStream介绍
Java File类介绍
ObjectInputStream类介绍

序列化攻击

将序列化的对象存到本地磁盘里面,然后再从磁盘读出生成对象。

public class SerializableTest {
    
    

    public static void main(String[] args) throws IOException, ClassNotFoundException {
    
    

        //获取单例对象
        Singleton singleton = Singleton.getInstance();
        //创建ObjectOutputStream对象,构造方法中传递字节输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\singleton.txt"));
        //使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
        oos.writeObject(singleton);


        //Java文件类以抽象的方式代表文件名和目录路径名。该类主要用于文件和目录的创建、文件的查找和文件的删除等。
        //File对象代表磁盘中实际存在的文件和目录。
        File file = new File("D:\\singleton.txt");
        //将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象,以流的方式读取对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

        Singleton newSingleton = (Singleton) ois.readObject();

        System.out.println(singleton);
        System.out.println(newSingleton);
        if(singleton!=newSingleton){
    
    
            System.out.println("单例模式被破坏,生成了两个实例");
        }else{
    
    
            System.out.println("单例模式坚守成功,只生成了一个实例");
        }


        EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
        oos.writeObject(enumSingleton);

        EnumSingleton newEnumSingleton = (EnumSingleton) ois.readObject();

        System.out.println(enumSingleton);
        System.out.println(newEnumSingleton);
        if(enumSingleton!=newEnumSingleton){
    
    
            System.out.println("枚举式单例模式被破坏,生成了两个实例");
        }else{
    
    
            System.out.println("枚举式单例模式坚守成功,只生成了一个实例");
        }

        oos.close();
        ois.close();
    }
}

除开枚举式实现,其他方式实现的单例模式都能被序列化攻击破坏在这里插入图片描述

枚举式序列化过程debug

那我们简单看一看运行流程,枚举式是怎么避免序列化攻击的

我们经过调用如下函数反序列化,返回一个实例,debug进去看看大致流程

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumSingleton newEnumSingleton = (EnumSingleton) ois.readObject();

下面我只放关键的业务跳转(删减了一些与创建对象无关的代码,避免文章篇幅过长),建议大家自己实操看一下更有感触

ObjectInputStream类(就是上面代码实例ois的类)里面有个bin字段

//用于处理块数据转换的过滤流
private final BlockDataInputStream bin;

通过构造函数将序列化数据传入,BlockDataInputStream 是一个内部私有类,他的实现我们暂且不管,我们只用知道bin里面的数据肯定是从序列化的数据里面得到了一定的信息。

    public ObjectInputStream(InputStream in) throws IOException {
    
    
        verifySubclass();
        bin = new BlockDataInputStream(in);
    }

然后,我们调用readObject()方法

EnumSingleton newEnumSingleton = (EnumSingleton) ois.readObject();

会调用readObject0()方法,然后在这个方法里面有tc字段,他从bin里面获取了一个信息,根据tc的不同,会Switch到不同的方法。其中一个就是TC_ENUM(这时候要大胆猜,肯定是从序列化的数据里面得知这个对象是一个枚举类),然后会调用readEnum()方法。

 case TC_ENUM:
     if (type == String.class) {
    
    
         throw new ClassCastException("Cannot cast an enum to java.lang.String");
     }
     return checkResolve(readEnum(unshared));
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
    
    
        
        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
    
    
            bin.readByte();
            handleReset();
        }
        try {
    
    
            switch (tc) {
    
    
                case TC_NULL:
                    return readNull();
         
                case TC_ARRAY:
                    if (type == String.class) {
    
    
                        throw new ClassCastException("Cannot cast an array to java.lang.String");
                    }
                    return checkResolve(readArray(unshared));
                case TC_ENUM:
                    if (type == String.class) {
    
    
                        throw new ClassCastException("Cannot cast an enum to java.lang.String");
                    }
                    return checkResolve(readEnum(unshared));
                case TC_OBJECT:
                    if (type == String.class) {
    
    
                        throw new ClassCastException("Cannot cast an object to java.lang.String");
                    }
                    return checkResolve(readOrdinaryObject(unshared));
                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
    
    
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

readEnum()中有如下代码,内部实现也是从bin数据中获取信息,拿到了类型的描述,枚举类我没手动实现 Serializable接口,但是抽象类Enum是实现了这个接口的,可以看到serialVersionUID默认为0

ObjectStreamClass desc = readClassDesc(false);

在这里插入图片描述
抽象类Enum

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    
    

如下代码,cl拿到了我们类的本地类描述符

private Enum<?> readEnum(boolean unshared) throws IOException {
    
    
        //拿到序列化数据的类型描述信息
        ObjectStreamClass desc = readClassDesc(false);
        // cl 为class com.xt.designmode.creational.singleton.serializableDestroy.EnumSingleton
        Class<?> cl = desc.forClass();
        if (cl != null) {
    
    
            try {
    
    
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
    
    
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
    
    
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

其中重要代码

 Enum<?> en = Enum.valueOf((Class)cl, name);

其中name是通过如下方法从bin字段中拿出来的,也就是我们枚举单例的名字“INSTANCE”

String name = readString(false);

valueOf代码如下:enumConstantDirectory()是一个哈希对象,会以name做为key,枚举类实例做为value.

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
    
    
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

而那些键值对信息,也就是枚举类 的name和实例是从如下方法获取。
enumConstantDirectory()----->getEnumConstantsShared()----->getMethod()----->枚举类的values()方法—>反射调用获得枚举类实例数据

首先通过Class的getMethod()方法获取枚举类的values()方法。拿到values()的使用权,然后使用反射调用values()方法获取数据

 T[] getEnumConstantsShared() {
    
    
        if (enumConstants == null) {
    
    
            if (!isEnum()) return null;
            try {
    
    
            //获取到枚举类里面的values方法,虽然我们写的枚举类里面虽然没有values方法,但是根据枚举类的反编译代码是可以看到有个values方法的
                final Method values = getMethod("values");
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
    
    
                        public Void run() {
    
    
                                values.setAccessible(true);
                                return null;
                            }
                        });
                @SuppressWarnings("unchecked")
                T[] temporaryConstants = (T[])values.invoke(null);
                enumConstants = temporaryConstants;
            }
            // These can happen when users concoct enum-like classes
            // that don't comply with the enum spec.
            catch (InvocationTargetException | NoSuchMethodException |
                   IllegalAccessException ex) {
    
     return null; }
        }
        return enumConstants;
    }

枚举类的反编译代码: 有关于values 的字段和方法(我们代码是看不到的,只有通过反编译才能看见),values()方法会执行一个clone(),然后返回。

private static final EnumSingleton $VALUES[];

static 
{
    
    
    INSTANCE = new EnumSingleton("INSTANCE", 0);
    $VALUES = (new EnumSingleton[] {
    
    
        INSTANCE
    });
}

public static EnumSingleton[] values()
{
    
    
    return (EnumSingleton[])$VALUES.clone();
}

执行完这个代码,result的实例为
在这里插入图片描述
如下我们先前用正当方式获取的实例,和序列化获取的是同一个实例。
在这里插入图片描述
枚举类序列化创造的实例是通过序列化数据得到类的信息,然后使用反射去调用枚举类的values()方法,而values()方法我们只有通过反编译才能看见,是一个$VALUES.clone(),我们可以看见是一个枚举类的数组,对这个数组进行克隆,也就是说数组会new一个新数组,但是里面的枚举类实例还是以前的,这是一个浅克隆。因为枚举类是规定不能实现clone()方法的,会抛异常。

    /**
     * Throws CloneNotSupportedException.  This guarantees that enums
     * are never cloned, which is necessary to preserve their "singleton"
     * status.
     *
     * @return (never returns)
     */
    protected final Object clone() throws CloneNotSupportedException {
    
    
        throw new CloneNotSupportedException();
    }

所以,枚举式单例是不能被序列化攻击破坏的,不会生成新的实例。

其他单例方式的序列化攻击防御以及debug流程

那么其他方式怎么避免序列化攻击,添加readResolve()方法。
来自 :https://www.cnblogs.com/ttylinux/p/6498822.html

Deserializing an object via readUnshared invalidates the stream handle
associated with the returned object. Note that this in itself does not
always guarantee that the reference returned by readUnshared is
unique; the deserialized object may define a readResolve method which
returns an object visible to other parties, or readUnshared may return
a Class object or enum constant obtainable elsewhere in the stream or
through external means. If the deserialized object defines a
readResolve method and the invocation of that method returns an array,
then readUnshared returns a shallow clone of that array; this
guarantees that the returned array object is unique and cannot be
obtained a second time from an invocation of readObject or
readUnshared on the ObjectInputStream, even if the underlying data
stream has been manipulated.

public class Singleton implements Serializable{
    
    

    private static final long serialVersionUID = 234234234L;

    private static Singleton singleton  = new Singleton();

    private Singleton(){
    
    
    }
    public static Singleton getInstance(){
    
    
        return singleton;
    }
	//添加这个方法
    private Object readResolve(){
    
    
        return singleton;
    }
}

我们再来debug一遍,看和枚举类的序列化创建实例有何不同
前面执行的逻辑和枚举类一样
readObject() ——> readObject0()
不过,这次readObject0() 代码块里面的switch会跳到TC_OBJECT,(枚举类会跳到TC_ENUM),然后执行readOrdinaryObject()方法

case TC_OBJECT:
      if (type == String.class) {
    
    
          throw new ClassCastException("Cannot cast an object to java.lang.String");
      }
      return checkResolve(readOrdinaryObject(unshared));

readOrdinaryObject()方法,同理cl 也是获取本地类描述符
cl: class com.xt.designmode.creational.singleton.serializableDestroy.Singleton

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
    
    
        //从序列化数据获取类的描述信息
        ObjectStreamClass desc = readClassDesc(false);
        //获得类的全路径
        Class<?> cl = desc.forClass();
       
        Object obj;
        try {
    
    
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
    
    
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
    
    
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
    
    
            readExternalData((Externalizable) obj, desc);
        } else {
    
    
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);
		//重点部分,他会检查有没有实现ReadResolve方法
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
    
    
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
    
    
                rep = cloneArray(rep);
            }
            if (rep != obj) {
    
    
                // Filter the replacement object
                if (rep != null) {
    
    
                    if (rep.getClass().isArray()) {
    
    
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
    
    
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

其中

obj = desc.isInstantiable() ? desc.newInstance() : null;
//如果表示的类是可序列化/可外部化的并且可以由序列化运行时实例化,则返回 true,
//如果它是可外部化的并且定义了一个公共的无参数构造函数,或者它是不可外部化的并
//且它的第一个不可序列化的超类定义了一个可访问的无参数构造函数。 否则,返回假。
    boolean isInstantiable() {
    
    
        requireInitialized();
        return (cons != null);
    }

cons != null是判断类的构造方法是否为空,而Class类的构造方法肯定不为空,显然isInstantiable()返回true,也就是说,会通过desc.newInstance()方法生成一个新对象,且被obj接收。
在这里插入图片描述

下面是我们用正当方式获取的实例,很明显序列化还是生成了新实例
在这里插入图片描述
newInstance()函数说明(这里就不放代码了,放代码的注释):如下所示,并不是通过反射去调用单例类的私有构造方法去创建新实例,而是调用无参构造方法,所以说开销不大。

创建表示的类的新实例。 如果该类是可外部化的,则调用其公共无参数构造函数;
否则,如果该类是可序列化的,则调用第一个不可序列化的超类的无参数构造函数。
如果此类描述符未与类关联、关联的类不可序列化或相应的无参数构造函数不可访问/不可用,则抛出
UnsupportedOperationException。

执行流程如下,不是通过反射去构建的新实例。
在这里插入图片描述

那么为啥测试的时候还是返回的同一个实例呢?
我们接着看,下面会执行如下方法,然后rep拿到了以前的那个实例
在这里插入图片描述
是怎么拿到以前的实例的呢?
里面有一个readResolveMethod.invoke(),接下来就是反射的流程,通过反射获取

Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
    
    
        requireInitialized();
        if (readResolveMethod != null) {
    
    
            try {
    
    
                return readResolveMethod.invoke(obj, (Object[]) null);
            } 
    }

这是debug的方法调用栈,可以发现,会使用反射去调用我们写在单例类里面的readResolve方法获取到先前创建的实例。
在这里插入图片描述

然后rep和obj做比较,如果不一样,就会obj = rep ,并返回obj,所以里面还是新创建了实例,只是通过反射的方式获取到了以前的实例,然后返回以前的实例。(那这样,虽然还是会有新实例诞生,但是是调用的第一个不可序列化的超类的无参数构造函数,所以所开销不大,也就无所谓了)。并且序列化生成的实例没有人引用,后续应该会被GC掉(不知道我这么理解有没有错误,知道的大佬评论区踢我一下)。
在这里插入图片描述

所以说这也是为什么不事先不利用反射去判断单例类有没有readResolve()方法的原因
因为反射耗时,但是调用无参构造函数去new一个里面没有任何属性的单例实例不怎么耗时。

你会说,那无参构造函数new的实例有什么用,确实没什么用。正好单例模式的类也不允许这么创建新的单例实例。
那可以不new这个实例啊。但是这个ObjectInputStream类还有其他的应用场景。不单单是为了单例这么设计的。有其他的考量。(暂时是这么理解的,如有错误,还望诸位大佬不吝赐教)

References:

  1. https://www.cnblogs.com/ttylinux/p/6498822.html
  2. https://blog.csdn.net/fragrant_no1/article/details/84965028
  3. https://zhuanlan.zhihu.com/p/136769959
  4. https://www.cnblogs.com/yyhuni/p/15127416.html
  5. https://blog.csdn.net/qq_37960603/article/details/104075412

猜你喜欢

转载自blog.csdn.net/qq_37774171/article/details/121747178
今日推荐