序列化指的是将对象编码为字节流、反序列化指的是将字节流重新构建为对象
将不被信任的字节流进行反序列化,可能导致
如果系统中的执行结果依赖于某个类的可信任状态(Period中start、end的final特性,以及其构造函数内部对参数进行正确性校验),利用字节码攻击的手段可以影响start、end这两个参数的正确性。从而使依赖于Period的代码受到污染。客户端代码持有了Period实例域的引用,在客户端中可以随意修改该引用的状态。要避免这种攻击必须自定义序列化逻辑,对实例域进行保护性拷贝,确保Period类的约束条件不被破坏
系统中存在一个可被序列化的类
package com.luhc;
import java.io.Serializable;
import java.util.Date;
/**
* 内部维持了两个final的实例域
*
* @author luhuancheng
* @date 2019/3/17
*/
public final class Period implements Serializable {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
// 保护性拷贝final属性
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
// 检验参数
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
public Date getStart() {
// 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
return new Date(start.getTime());
}
public Date getEnd() {
// 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
return new Date(end.getTime());
}
@Override
public String toString() {
return "Period{" +
"start=" + start +
", end=" + end +
'}';
}
}
一个对字节码进行伪造的攻击类
package com.luhc;
import java.io.*;
import java.util.Date;
/**
* @author luhuancheng
* @date 2019/3/17
*/
public class AttackPeriod {
public final Period period;
public final Date start;
public final Date end;
public AttackPeriod() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
// 写入对象字节流
out.writeObject(new Period(new Date(), new Date()));
// 伪造字节流,之后我们可以从字节流中直接读取到Period对象中的两个实例域(start、end),这意味着我们能够修改这两个本应该是final的实例域的状态
byte[] ref = {0x71, 0, 0x7e, 0, 5};
bos.write(ref); // start
ref[4] = 4;
bos.write(ref); // end
// 将字节流反序列为对象
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
// 从字节流中反序列化Period两个final的实例域,指向AttackPeriod的公开实例域,之后可以修改这两个指针所指的对象
// 此时的Period实例域start、end与AttackPeriod中两个实例域start、end指向的是内存中同一个对象
start = (Date) in.readObject();
end = (Date) in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new AssertionError(e);
}
}
public static void main(String[] args) {
AttackPeriod ap = new AttackPeriod();
Period p = ap.period;
Date pEnd = ap.end;
pEnd.setYear(76);
System.out.println(p);
}
}
自定义readObject逻辑(进行数据的保护性拷贝、参数校验),避免字节码攻击
package com.luhc;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;
/**
* 内部维持了两个final的实例域
*
* @author luhuancheng
* @date 2019/3/17
*/
public final class Period implements Serializable {
// 在readObject中对这两个实例域进行保护性拷贝 赋值,因此无法再保持final特性
private Date start;
private Date end;
public Period(Date start, Date end) {
// 保护性拷贝final属性
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
// 检验参数
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
public Date getStart() {
// 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
return new Date(start.getTime());
}
public Date getEnd() {
// 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
return new Date(end.getTime());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 序列化规范必须的步骤
in.defaultReadObject();
// 保护性拷贝(start、end不能使用final修饰了)
start = new Date(start.getTime());
end = new Date(end.getTime());
// 参数校验
if (start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}
@Override
public String toString() {
return "Period{" +
"start=" + start +
", end=" + end +
'}';
}
}
在上一种优化处理中,在readObject方法内部需要修改start、end的值,因此这两个实例域不能再被final修饰。使用序列化代理可以维持实例域依旧是final的
package com.luhc;
import java.io.Serializable;
import java.util.Date;
/**
* 内部维持了两个final的实例域
*
* @author luhuancheng
* @date 2019/3/17
*/
public final class Period implements Serializable {
// 不可变类的内部实例域,使用final修饰确保该类真正是不可变的
private final Date start;
private final Date end;
public Period(Date start, Date end) {
// 保护性拷贝final属性
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
// 检验参数
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
public Date getStart() {
// 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
return new Date(start.getTime());
}
public Date getEnd() {
// 保护性拷贝后,返回一个新的对象,避免被外部改变了内部的状态
return new Date(end.getTime());
}
@Override
public String toString() {
return "Period{" +
"start=" + start +
", end=" + end +
'}';
}
// 序列化代理
// 1. 定义代理内部类
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
// 构造器传入外围类的实例
public SerializationProxy(Period period) {
this.start = period.start;
this.end = period.end;
}
// 3. 定义readResolve方法,返回外围类实例
private Object readResolve() {
return new Period(start, end);
}
}
// 2. 外围类定义writeReplace方法, 返回代理类
private Object writeReplace() {
return new SerializationProxy(this);
}
}