Serialization destroys singleton mode and how to prevent it

Posted by maplist on Wed, 08 Dec 2021 10:17:02 +0100

Serialization introduction

Serializable introduction

If the singleton class implements the Serializable serialization interface, it may be attacked by serialization to destroy the singleton pattern.

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;
    }

}

Introduction to ObjectOutputStream
Introduction to FileOutputStream
Introduction to FileInputStream
Introduction to Java File class
Introduction to ObjectInputStream class

Serialization attack

Save the serialized object to the local disk, and then read the generated object from the disk.

public class SerializableTest {

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

        //Get singleton object
        Singleton singleton = Singleton.getInstance();
        //Create an ObjectOutputStream object and pass the byte output stream in the construction method
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\singleton.txt"));
        //Use the method writeObject in the ObjectOutputStream object to write the object to a file
        oos.writeObject(singleton);


        //Java file classes represent file names and directory pathnames in an abstract way. This class is mainly used to create files and directories, find files and delete files.
        //The File object represents the files and directories that actually exist on the disk.
        File file = new File("D:\\singleton.txt");
        //Restore the original data previously serialized with ObjectOutputStream to an object and read the object as a stream
        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("The singleton pattern is broken and two instances are generated");
        }else{
            System.out.println("The singleton mode is stuck successfully, and only one instance is generated");
        }


        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("The enumerated singleton pattern is broken and two instances are generated");
        }else{
            System.out.println("The enumerated singleton mode was successfully persisted, and only one instance was generated");
        }

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

In addition to the enumeration implementation, the singleton mode implemented by other methods can be destroyed by serialization attack

Enumerative serialization process debug

Let's take a brief look at the running process and how enumerations avoid serialization attacks

We call the following function to deserialize, return an instance, and debug to see the general process

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

Next, I only put the key business jump (deleted some code irrelevant to the creation of objects to avoid the length of the article). I suggest you take a look at it yourself

There is a bin field in the ObjectInputStream class (that is, the class of the above code example ois)

//Filter stream for processing block data conversion
private final BlockDataInputStream bin;

The serialized data is passed in through the constructor. BlockDataInputStream is an internal private class. We don't care about its implementation for the time being. We only know that the data in bin must get some information from the serialized data.

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

Then, we call the readObject() method

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

It will call the readObject0 () method, and then there is a tc field in this method. It obtains an information from bin, and will Switch to different methods according to different tc. One of them is TC_ENUM (guess boldly at this time, it must be learned from the serialized data that this object is an enumeration class), and then it will call the readEnum() method.

 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() has the following code. The internal implementation also obtains information from the bin data and gets the type description. I did not manually implement the Serializable interface for the enumeration class, but Enum implements this interface. You can see that the serialVersionUID is 0 by default

ObjectStreamClass desc = readClassDesc(false);


In the following code, cl gets the local class descriptor of our class

private Enum<?> readEnum(boolean unshared) throws IOException {
        //Get the type description of the serialized data
        ObjectStreamClass desc = readClassDesc(false);
        // cl is class com.xt.designmode.creative.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;
    }

Important codes

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

The name is obtained from the bin field by the following method, that is, the name of our enumeration singleton "INSTANCE"

String name = readString(false);

The valueOf code is as follows: enumConstantDirectory() is a hash object, with name as the key and enumeration class instances as 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);
    }

The key value pair information, that is, the name and instance of the enumeration class, is obtained from the following methods.
Enumconstantdirectory() ---- > getenumconstantsshared() ---- > getmethod() ---- > values() method of enumeration class - > reflection call to get data

First, get the values() method of the enumeration Class through the getMethod() method of Class. Get the right to use values(), and then use reflection to call the values() method

 T[] getEnumConstantsShared() {
        if (enumConstants == null) {
            if (!isEnum()) return null;
            try {
            //Get the values method in the enumeration class. Although there is no values method in the enumeration class, you can see that there is a values method according to the decompiled code of the enumeration class
                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;
    }

Decompile code of enumeration class: there are fields and methods about values (we can't see them in the code, and we can only see them through decompilation). The values() method will execute a clone() and then return.

private static final EnumSingleton $VALUES[];

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

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

After executing this code, the instance of result is

As follows, the instance we previously obtained in a proper way is the same instance obtained by serialization.

The instance created by enumeration class serialization is to obtain the class information by serializing the data, and then use reflection to call the values () method of enumeration class. The values () method can only be seen through decompilation. It is a $VALUES.clone(). We can see that it is an array of enumeration classes. Clone this array, that is, the array will create a new array, However, the enumeration class instance is still the same as before. This is a shallow clone. Because the enumeration class specifies that the clone() method cannot be implemented, an exception will be thrown.

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

Therefore, enumerated singletons cannot be destroyed by serialization attacks and will not generate new instances.

Serialization attack defense and debug process of other singleton methods

How to avoid serialization attack in other ways? Add readResolve() method.
come from: 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;
    }
	//Add this method
    private Object readResolve(){
        return singleton;
    }
}

Let's debug again to see the difference between serializing and creating instances of enumeration classes
The logic executed earlier is the same as that of the enumeration class
readObject() -> readObject0()
However, this time, the switch in the readObject0() code block will jump to TC_OBJECT, (the enumeration class will jump to TC_ENUM), and then execute the readOrdinaryObject() method

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

The readOrdinaryObject() method, similarly, cl is also used to obtain the local class descriptor
cl: class com.xt.designmode.creational.singleton.serializableDestroy.Singleton

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        //Gets the description of the class from the serialized data
        ObjectStreamClass desc = readClassDesc(false);
        //Get the full path of the class
        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);
		//In the key part, he will check whether the ReadResolve method is implemented
        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;
    }

among

obj = desc.isInstantiable() ? desc.newInstance() : null;
//Returns true if the represented class is serializable / externalizable and can be instantiated by the serialization runtime,
//If it is externalizable and defines a public parameterless constructor, or if it is not externalizable and
//And its first non serializable superclass defines an accessible parameterless constructor. Otherwise, it returns false.
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }

Cons! = null is to judge whether the Class constructor is empty, and the Class constructor is certainly not empty. Obviously, isinstrable() returns true, that is, a new object will be generated through the desc.newInstance() method and received by obj.

The following is the instance we obtained in a proper way. It is obvious that we serialized or generated a new instance

Description of newInstance() function: as shown below, instead of calling the private constructor of the singleton class to create a new instance through reflection, it calls the parameterless constructor, so the overhead is small.

Create a new instance of the class represented. If the class is externalizable, call its public parameterless constructor;
Otherwise, if the class is serializable, the parameterless constructor of the first non serializable superclass is called.
Thrown if the class descriptor is not associated with a class, the associated class is not serializable, or the corresponding parameterless constructor is not accessible / available
UnsupportedOperationException.
The execution process is as follows, and it is not a new instance built through reflection.

So why is the same instance returned when testing?
Let's continue. The following method will be executed, and rep gets the previous instance

There is a readResolveMethod.invoke(), followed by the reflection process

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

This is the method call stack of debug. It can be found that reflection will be used to call the readResolve method written in the singleton class to obtain the previously created instance.

Then rep and obj are compared. If they are different, obj = rep and obj is returned. Therefore, a new instance is created, but the previous instance is obtained through reflection, and then the previous instance is returned. (in this way, although a new instance will be born, it is the first non serializable super class parameterless constructor called, so the cost is small, so it doesn't matter.). In addition, the instance generated by serialization is not referenced, and it should be lost by GC in the future (I don't know if there is any error in my understanding, but I know the boss will kick me in the comment area).

So that's why you don't use reflection to determine whether a singleton class has a readResolve() method in advance
Because reflection takes time, but it doesn't take much time to call the parameterless constructor to new a singleton instance without any attributes.

You would say that the instance of the parameterless constructor new is of no use. It's really of no use. Classes in singleton mode are not allowed to create new singleton instances in this way.
The example of new is not needed. However, there are other application scenarios for this ObjectInputStream class. It's not just designed for a single example. There are other considerations. (I understand it this way for the time being. If there is any mistake, I hope you guys will give me advice.)

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

Topics: Design Pattern