序列化——对于实例控制,枚举类型优先于readResolve-创新互联

普通单例模式的漏洞

第3条讲述了 Singleton 模式,并且给出了以下这个 Singleton 类的示例。

成都创新互联专注于企业全网营销推广、网站重做改版、兰山网站定制设计、自适应品牌网站建设、H5开发商城系统网站开发、集团公司官网建设、成都外贸网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为兰山等各大城市提供网站开发制作服务。

这个类限制了对其构造器的访问,确保永远只创建一个实例:

public class Singer{
    public static final Singer INSTANCE = new Singer();
    private Singer(){}

    public static Singer getInstance(){
        return INSTANCE;
    }
}

但如果在这个类实现序列化,这种方式就不能保证安全了。序列化可以轻松突破这种机制,甚至,这种错误是在无意间的,并不是蓄意破坏。之前曾说过,序列化是一种独立于构造器的创建对象的机制,或者你可以变相地认为序列化是一种以 byte[] 为参数的隐形构造器。

演示:

public class Singer implements Serializable {
    public static final Singer INSTANCE = new Singer();
    private Singer(){}

    public static Singer getInstance(){
        return INSTANCE;
    }

    private Object readResolve(){
        return INSTANCE;
    }
}
public class Client {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singer instance = Singer.getInstance();
        //序列化
        File file = new File("C:\\Users\\admin\\Desktop\\file_upload\\1.txt");
        ser(instance,file);

        //反序列化
        byte[] bytes = readBytes(file);
        Singer instance2 = deSer(bytes);

        //比较两个对象是否相同
        System.out.println(instance == instance2);
    }

    static void ser(Singer s,File file) throws IOException {
        FileOutputStream fos = new FileOutputStream(file);
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(s);
        os.close();
    }

    static Singer deSer(byte[] bytes) throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Singer p = (Singer) in.readObject();
        in.close();
        return p;
    }

    static byte[] readBytes(File file) throws IOException {
        FileInputStream in = null;
        try {
            in =new FileInputStream(file);
            //当文件没有结束时,每次读取一个字节显示
            byte[] data=new byte[in.available()];
            in.read(data);
            in.close();
            return data;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            in.close(); //关闭流
        }
        return null;
    }
}

false

可以看出,反序列化的对象和单例获得的对象并不相同,从而导致单例模式失效


使用 readResole 方法

readResole 特性允许你用 readObject 创建的实例代替另一个实例。对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve 方法,并且具备正确的声明,那么在反序列化之后,新建对象上的 readResolve 方法就会被调用。然后,该方法返回的对象应用将被返回,取代新建的对象。在这个特性的绝大多数用法中,指向新建对象的引用不需要再被保留,因此立即成为垃圾回收的对象。

比如,使用以下的方法保证单例

private Object readResolve(){
    return INSTANCE;
}

该方法忽略了被反序列化的对象,只返回该类初始化时创建的那个特殊的实例。因此,Singer 实例的序列化形式并不需要包含任何实际的数据;所有的实例域都应该声明为 transient。


readResole 方法的不足 

事实上,如果依赖 readResolve 进行实例控制,带有对象引用类型的所有实例域则都必须声明为 transient。否则,攻击者依然可以想办法在 readResolve 运行之前,获取反序列化的对象引用,得到一个单例之外的“副本”,类似于上一条中提到的 MutablePeriod攻击。

这种攻击有点复杂,但背后的思想却很简单。如果 Singleton 包含一个非 transient 对象引用域,这个域的内容就可以在 Singleton 的 readResolve 方法运行之前被反序列化。当对象引用域的内容被反序列化时,它就允许一个精心制作的流“盗用”指向最初被反序列化的 Singleton 引用。

实例演示:

import java.io.Serializable;
import java.util.Arrays;

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis(){}
    private String[] favoriteSongs = {"红颜如霜","发如雪"};
    public void printFavorites(){
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve(){
        return INSTANCE;
    }

}
import java.io.Serializable;

public class ElvisStealer implements Serializable {

    static Elvis impersonator;
    private Elvis payload;

    private Object readResolve(){
        impersonator = payload;
        return new String[]{"发如霜"};
    }
    private static final long serialVersionUID = 0;

}
import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;

public class ElvisImpersonator {
    private static final byte[] bytes = new byte[]{
            (byte) 0xac, (byte) 0xed,0x00,0x05,0x73,0x72,0x00,0x05,
            0x45,0x6c,0x76,0x69,0x73, (byte) 0x84, (byte) 0xe6,
            (byte) 0x93,0x33, (byte) 0xc3, (byte) 0xf4, (byte) 0x8b,
            0x32,0x02,0x00,0x01,0x4c,0x00,0x0d,0x66,0x61,0x76,
            0x6f,0x72,0x69,0x74,0x65,0x53,0x6f,0x6e,0x67,0x73,
            0x74,0x00,0x12,0x4c,0x6a,0x61,0x76,0x61,0x2f,0x6c,
            0x61,0x6e,0x67,0x2f,0x4f,0x62,0x6a,0x65,0x63,0x74,
            0x3b,0x78,0x70,0x73,0x72,0x00,0x0c,0x45,0x6c,0x76,
            0x69,0x73,0x53,0x74,0x65,0x61,0x6c,0x65,0x72,0x00,
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x01,
            0x4c,0x00,0x07,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64,
            0x74,0x00,0x07,0x4c,0x45,0x6c,0x76,0x69,0x73,0x3b,
            0x78,0x70,0x71,0x00,0x7e,0x00,0x02
    };

    public static void main(String[] args) {
        Elvis elvis = deser(bytes);
        Elvis impersonator = ElvisStealer.impersonator;
        elvis.printFavorites();
        impersonator.printFavorites();
    }

    @SneakyThrows
    static Elvis deser(byte[] bytes) {
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Elvis p = (Elvis) in.readObject();
        in.close();
        return p;
    }
}

通过将 favorites 声明为 transient 可以解决这个问题。但如果确实要避免出现这种错误,最好的还是使用 枚举来解决这种问题。

正如之前所提到过的:自从 jdk1.5之后,单例的最佳实现方式就是枚举。当然,readResolve 并非完全过时,有些情况可能不适合使用枚举,这时候依然需要这种方案。但一定要注意,属性设置为transient 或者 基本类型

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


本文名称:序列化——对于实例控制,枚举类型优先于readResolve-创新互联
地址分享:http://pwwzsj.com/article/dochpc.html