序列化指的是将对象编码为字节流、反序列化指的是将字节流重新构建为对象
将不被信任的字节流进行反序列化,可能导致
如果系统中的执行结果依赖于某个类的可信任状态(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); } }