讨论对象的序列化 把对象编码成字节流(序列化),并从字节流编码中重新构建对象(反序列化)
本章关注序列化的风险,以及将风险降到最低
第八十五条 ,其他方法优先于java序列化
1.反序列化炸弹
//创建了201个对象,而以指数级的插入2的100次方的HashMap
public static byte[] bomb(){
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");
s1.add(t1);s1.add(t2);
s2.add(t1);s2.add(t2);
s1=t1;
s2=t2;
}
return serialize(root);
}
2.避免序列化攻击的最佳方式是永远不要序列化任何东西,在新编写的任何新的系统中都没理由再使用java序列化,
永远不要反序列化不被信任的数据
3.可以使用json和protocol
4.java9提供了对象反序列化过滤器,它可以在数据流被反序列化之前,为他们定义一个过滤器,他可以操作粒度,
允许接受或者拒绝某些类,默认接受类,拒绝可能存在危险的黑名单;默认拒绝类,接受白名单安全的类。白名单
优先于黑名单,有一个工具SWAT,他可以替应用准备好白名单
第八十六条,谨慎地实现serializable接口
1.实现serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了改变这个类的实现的灵活性,新旧序
列化文件兼容的问题
2.serializable需要添加一个UID,来保证兼容,不加系统自动生成一个,但是是根据类的信息来生成的,如果增加一个
静态方法,可能生成的UID就不一样
3.实现serializable的第二个代价是,它增加了出现的Bug和安全漏洞的可能性(85)
4.实现serializable的第三个代价是,随着类发行新的版本,相关的测试负担也会增加,需要兼容旧版,测试可序列化类
* 版本的乘积
5.实现serializable接口并不是一个很轻松就可以做出的决定
6.为了继承而设计的类应该尽可能少地去实现serializable接口,用户的接口也应该尽可能少继承serializable接口
7.如果带有一个实例域的类,实例域有一些限制条件,防止子类覆盖finalize方法,造成终结器的攻击,需要
覆盖finalize并设置为final
8.如果类有限制条件,当类的实例域被初始化成默认值,就会违背这些限制条件,需要添加一个private void
readObjectNoData()
9.如果一个为了继承而设计的类是不可序列化的,那么子类序列化就需要调用父类无参的构造器,如果没有提供,
子类被迫使用序列化代理模式(90)
10.内部类不应该实现serializable接口,他们使用编译器的合成域来保存指向外围实例的引用,以及保存外围作用域的局
部变量的值,这些域如何对应到对应的类定义中并没有明确的规定,就好像没有指定匿名类和局部类的名称一样
因此,内部类的默认序列化形式是定义不清楚的,然而,静态成员类却可以实现serializable接口
第八十七条,考虑使用自定义的序列化形式
1.如果事先没有认真考虑默认的序列化形式是否合适,则不要贸然接受
2.如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式
3.即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性
4.@serial和@serialData 告诉javadoc把文档信息放在有关序列化形式的特殊文档页中
5.当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认的序列化形式会有以下4个缺点
1.它使这个类的导出API永远地束缚在该类的内部表示法上
2.它会消耗更多的空间
3.它会消耗过多的时间
4.它会引起栈溢出
6.@transient忽略序列化 ,在决定将一个域非transient之前,请一定确信它的值将是该对象逻辑状态的一部分
7.defaultReadObject和defaultWriteObject可以兼容前后版本,没有的多的域可以忽略
8.readObject可以恢复那些被@transient忽略序列化的域的值,先调用defaultReadObject;然后恢复那些值;或者这些transient值
可以在第一次访问的时候自动恢复
9.如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步没,否则可能
资源排列死锁问题
10.不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列化UID
private static final long serialVersionUID = randomLongValue;
11.不要修改序列化版本UID,否则将会破坏类现有的已被序列化实例的兼容性
第八十八条,保护性地编写readObject方法(防止恶意的字节码)
1.当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个域包含了这样的对象引用,就必须要
做保护性拷贝,这是非常常重要的
2.readObject不要调用可覆盖的方法,不管是直接或者间接,在子类的状态被反序列化之前会先执行
3.编写健壮的readObject
1.对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象,不可变的类的可变组件就属于这一
类别
2.对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常,这些检查动作应该跟在所有的保护性
拷贝之后
3.如果整个对象图在反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
4.不管是直接或者间接,都不要调用任何可被覆盖的方法
第八十九条,对于实例控制,枚举类型优先于readResolve
1.如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient
2.readResolve的可访问性很重要,私有,子类不能用,包级私有,包级的子类可用,受保护和公有的,子类没有覆盖,
子类反序列化时会产生一个父类,导致ClassCastException
3.使用枚举,java保证除了声明的常量之外,不会有其他实例,除非攻击者恶意的使用了取得特权的方法,
AccessibleObject.setAccessible,如果攻击者可以这样做,那么他可以执行任何本地的代码了,这是很危险的
第九十条,考虑用序列化代理替代序列化实例
1.当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法时,就应该考虑使用序列化代理
模式,要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的反方
2.首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态,这个嵌套类被称为
序列化代理类,它应该有一个单独的构造器,其参数类型就是外围类,这个构造器只从他的参数中复制数据,它
不需要进行任何一致性检查和保护性拷贝,外围类和序列化代理都需要实现serializable,可序列化的类有readResolve
方法,里面直接通过构造器和静态工厂来产生外围类
3.private Object writeReplace(){ return new 序列化代理类();},这个可以让序列化系统产生一个序列化代理类替换外围实例
4.外围类的readObject方法抛出异常即可
网友评论