Java序列化安全机制
前言
上一篇最后我们提到采用java默认的序列化机制是存在安全漏洞的。第一种漏洞就是在网络中传播二进制流时被黑客截获,获取其中的一些敏感信息,比如账号密码以及上文提到的苹果的价格;另一种就是黑客截获到了这些信息后加以修改,再通过网络发送出去,比如恶意修改了苹果价格信息,那么销售商将会面临破产的危机。基于此此,java提供自定义序列化机制来避免第一种漏洞;采用反序列化的验证机制来避免第二种漏洞。另外,一定要理解上一篇文章中提到的序列化流的格式,即分为三部分:序列化头信息部分、类的描述部分以及属性域的值部分,下文中会多次提到。
自定义序列化
所谓java自定义序列化, java提供了三种实现方案,前两种方案实际上是自定义第三部分信息(属性域的值部分)的输出方式,而第三种方法不仅可以自定义第三部分信息的输出,还可以自定义第二部分信息(类描述部分)信息的输出。第一种方案采用默认机制与自定义机制相结合的方法;第二种是完全自定义序列化属性值的方法,不仅可以有选择的储存本对象包含的数据,还可以存储其他非this对象包含的数据。这两种自定义的程度还只是停留在定制对象内部属性的描述,而对于序列化对象本身的描述无法定制,这时我们可以采用第三种方案:新建一个自己的序列化类来实现。
java.io.Serializable自定义形式
这种自定义序列化方案就是有选择的序列化对象域,而不是把对象的所有域内容都序列化。通过在成员变量上添加transient关键字,我们可以不让默认的序列化机制序列化该成员(比如苹果价格),我们可以将该成员加密后手动的写入到序列化流中。java.io.Serializable接口自定义序列化的核心是:在要序列化的类中添加如下签名方式的两个方法:
---------------------------------------------------------------------------------------------------------------------------------
private void readObject(java.io.ObjectInputStream stream)
throws IOException, ClassNotFoundException;
private void writeObject(java.io.ObjectOutputStream stream)
throws IOException
---------------------------------------------------------------------------------------------------------------------------------
改写后的Apple类如下:
---------------------------------------------------------------------------------------------------------------------------------
package com.fnst.infoQ;
public class Apple extends Fruit{
private String name;
private transient int price;
public Apple(String _name,int _price){
super(); this.name = _name; this.price = _price;
}
public Apple() {this.name = "Default Apple"; price = 4;}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException{
in.defaultReadObject();
//将price解密
int passPrice = in.readInt();
this.price = deciphering(passPrice);
}
private void writeObject(java.io.ObjectOutputStream out)
throws IOException{
out.defaultWriteObject();
//将price加密后,写入流中
int passPrice = encryption(price);
out.writeInt(passPrice);
}
}
---------------------------------------------------------------------------------------------------------------------------------
很多人对ObjcetOutputStream类的writeObject(readObject)与defaultWriteObect(defaultReadObject)方法感到疑惑,不知道如何使用,就是知道如何使用又不清楚为何这样使用。其实比较简单,writeObject(readObject)的作用是序列化(反序列化)后两部分内容,即类的描述部分和属性域的值部分;而defaultWriteObect(defaultReadObject)的作用就是序列化(反序列化)最后一部分内容,即属性域的值部分。JDK中序列化的调用栈与具体代码如下:
java.io.ObjectOutputStream.writeObject ()……………………………………………………………………①
└java.io.ObjectOutputStream.writeObject0()………………………………………………………………②
└java.io.ObjectOutputStream.writeOrdinaryObject()………………………………………③
├java.io.ObjectOutputStream.writeClassDesc()………………………………………………④
└java.io.ObjectOutputStream.writeSerialData ()…………………………………………⑤
其中序列化第二部分信息(类的描述部分)由③④完成,而⑤主要完成的就是第三部分信息的序列化,代码如下:
---------------------------------------------------------------------------------------------------------------------------------
1448 private void writeSerialData(Object obj, ObjectStreamClass desc)
1449 throws IOException
1450 {
//获取序列化对象由子类到父类的类描述集合
1451 ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
1452 for (int i = 0; i < slots.length; i++) {
1453 ObjectStreamClass slotDesc = slots[i].desc;
//判断序列化类是否实现了writeObject方法
1454 if (slotDesc.hasWriteObjectMethod()) {
1455 PutFieldImpl oldPut = curPut;
1456 curPut = null;
-中略-
1464 SerialCallbackContext oldContext = curContext;
1465 try {
1466 curContext = new SerialCallbackContext(obj, slotDesc);
1467
1468 bout.setBlockDataMode(true);
//如果实现了wirteObjcet方法,通过反射机制调用该方法
1469 slotDesc.invokeWriteObject(obj, this);
1470 bout.setBlockDataMode(false);
1471 bout.writeByte(TC_ENDBLOCKDATA);
1472 } finally {
-中略-
1482 } else {
//如果序列化类未实现writeObject方法,调用默认的属性值序列化方式
1483 defaultWriteFields(obj, slotDesc);
-下略-
---------------------------------------------------------------------------------------------------------------------------------
上述代码1483行,java.io.FileOutputStream.writeSerialData方法调用的defaultWriteField方法,其实就是defaultWriteObject方法的核心实现。
java.io.FileOutputStream.defaultWriteObject代码如下:
---------------------------------------------------------------------------------------------------------------------------------
415 public void defaultWriteObject() throws IOException {
416 if (curContext == null) {
417 throw new NotActiveException("not in call to writeObject");
418 }
419 Object curObj = curContext.getObj();
420 ObjectStreamClass curDesc = curContext.getDesc();
421 bout.setBlockDataMode(false);
//调用默认的属性值序列化方式
422 defaultWriteFields(curObj, curDesc);
423 bout.setBlockDataMode(true);
424 }
---------------------------------------------------------------------------------------------------------------------------------
根据defaultWriteObject的422行代码,可以对writeObject方法和defaultWriteObect方法的作用有了一个清晰的认识。在Java API官方文档描述defaultWrite(Read)Object方法只能从正在序列化的类的 writeObject 方法中调用。如果从其他地方调用该字段,则将抛出 NotActiveException,由defautWriteObject代码的416-418行可以找到答案,curContext是一个描述当前序列化类上下文的类对象(包含序列化类的类描述信息以及具体的类对象),也就是说再未执行③④步骤(将类描述信息序列化到流中,而curContext则代表当前已经序列化到流中的类描述信息上下文)时,是不允许将序列化类的属性值写入到流的。
还有一点需要说明,就是在序列化类中的writeObjcet方法中再次调用out.writeObject(this),程序会不会陷入死循环?代码修改如下:
---------------------------------------------------------------------------------------------------------------------------------
private void writeObject(java.io.ObjectOutputStream out)
throws IOException{
out.defaultWriteObject();
//将price加密后,写入流中
int passPrice = encryption(price);
out.writeInt(passPrice);
//添加代码
out.writeObject(this);
}
---------------------------------------------------------------------------------------------------------------------------------
答案是否定的,但是如果调用的是out.writeUnshared(this);就会陷入死循环并且最终导致栈溢出异常。其实ObjectOutputStream类的writeObject和writeUnshared方法是基本一致的,只是在写入同一个对象时采用的方式不同。前者采用共享对象的方式(unshared变量为false),同一个对象只可以写入一次;而后者采用非共享的方式(unshared变量为true),同一个可以写入多次。调用栈如下:
1. writeObject方法调用栈
java.io.ObjectOutputStream.writeObject()
└java.io.ObjectOutputStream.writeObject0(…,unshared=false)
└java.io.ObjectOutputStream.writeOrdinaryObject(…,unshared=false)
2. writeUnshared方法调用栈
java.io.ObjectOutputStream.writeUnshared()
└java.io.ObjectOutputStream.writeObject0(…,unshared=true)
└java.io.ObjectOutputStream.writeOrdinaryObject(…,unshared=true)
两者的主要区别就是unshared这个布尔 变量的值了。writeOrdinaryObject代码如下:
---------------------------------------------------------------------------------------------------------------------------------
1085 private void writeObject0(Object obj, boolean unshared)
1086 throws IOException
1087 {
-中略-
1092 int h;
1093 if ((obj = subs.lookup(obj)) == null) {
1094 writeNull();
1095 return;
//如果该对象是共享的并且该对象之前已经写入,那么将会指定writeHandle方、//法并返回。writeHandle方法的作用就是在流中写入如下4个字节:
//0x71 : TC_REFERENCE 该类对象的引用已经在流中
//0x7e0000+类对象在存储栈中的序号
//否则的话会将该对象再次写入到流中
1096 } else if (!unshared && (h = handles.lookup(obj)) != -1) {
1097 writeHandle(h);
1098 return;
1099 } else if (obj instanceof Class) {
-下略-
---------------------------------------------------------------------
java.io.Externalizable自定义形式
当对象实现了java.io.Externalizable接口时,就可以灵活的控制它的序列化和反序列化过程,该接口继承自java.io.Serializable。Externalizable接口定义了两个方法writeExternal和readExternal。我们的苹果实例可以修改成如下的形式:
---------------------------------------------------------------------------------------------------------------------------------
package com.fnst.infoQ;
public class Apple extends Fruit implements Externalizable{
private String name;
private transient int price;
public Apple(String _name,int _price){
super(); this.name = _name; his.price = _price;
}
public Apple() {this.name = "Default Apple"; price = 4;}
public void writeExternal(ObjectOutput out) throws IOException {
//可以选择性的将任何类型的域成员值写入到流中
out.writeInt(price);
out.writeObject(name);
String keyWord = "This is Externalizable test!";
//可以将非this对象包含的数据写入序列化流中
out.writeObject(keyWord);
}
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
this.price = in.readInt();
this.name = (String)in.readObject();
System.out.println(in.readObject());
}
}
---------------------------------------------------------------------------------------------------------------------------------
Externalizable接口的writeExternal(readExternal)方法的作用仍是序列化第三部分信息(属性域的值部分)。当实现了该接口后,再在该类中添加writeObject(readObjcet)方法,那么writeObject(readObjcet)方法将会失效,具体原因通过阅读源码就了然了。首先调用栈如下:
java.io.ObjectOutputStream.writeObject()
└java.io.ObjectOutputStream.writeObject0()
└java.io.ObjectOutputStream.writeOrdinaryObject()
---------------------------------------------------------------------------------------------------------------------------------
1381 private void writeOrdinaryObject(Object obj,
1382 ObjectStreamClass desc,
1383 boolean unshared)
1384 throws IOException
1385 {
-中略-
1397 if (desc.isExternalizable() && !desc.isProxy()) {
//序列化实现java.io.Externalizable类对象属性的值
1398 writeExternalData((Externalizable) obj);
1399 } else {
//序列化实现java.io.Serializable类对象属性的值
1400 writeSerialData(obj, desc);
1401 }
-下略-
----------------------------------------------------------------------------------------------------
由上述代码的1397行可知,当序列化类是Externalizable类型时将执行
writeExternalData方法,当是Serializable类型的是否才执行writeSerialData方法,而在writeExternalData方法中,序列化类会调用自定义的writeExternal方法执行自定义的序列化操作。
新建一个自己的序列化类
自定义一个自己的序列化类需要三个步骤,第一步需要继承ObjectOutputStream(ObjectInputStream);第二步在构造函数调用父类的无参构造函数;第三步重写父类的writeObjectOverride(readObjcetOverride)方法,在该方法中自定义序列化方案。
----------------------------------------------------------------------------------------------------
public class CustomObjectOutputStream extends ObjectOutputStream{
private OutputStream objOut;
public CustomObjectOutputStream(OutputStream out) throws IOException {
super();
this.objOut = out;
}
@Override
protected void writeObjectOverride(Object obj) throws IOException {
//自定义序列化方案
}
}
----------------------------------------------------------------------------------------------------
以上是自定义的序列化类的实现方式;反序列化类与此类似,这里不再赘述。这样做的原理是什么?我们一看JDK源码就清楚了。
java.io
├java.io.ObjectOutputStream()
└java.io.ObjectOutputStream.writeObject()
----------------------------------------------------------------------------------------------------
254 protected ObjectOutputStream()
throws IOException, SecurityException {
-中略-
//分配私有数据成员为空,这样便于自定义自己的序列化类
259 bout = null;
260 handles = null;
261 subs = null;
//自定义方法的调用的关键变量
262 enableOverride = true;
263 debugInfoStack = null;
264 }
-中略-
324 public final void writeObject(Object obj) throws IOException {
325 if (enableOverride) {
326 writeObjectOverride(obj);
327 return;
328 }
-下略-
----------------------------------------------------------------------------------------------------
在自定义序列化类的构造函数中调用父类的无参构造函数,保证了enableOverride变量为true。那么当执行writeObject序列化类时,会就会调用自定义序列化方法writeObjectOverride。
序列化流验证机制
一般情况下,我们认为序列化流中的数据总是与最初写到流中的数据一致,这并没有问题。但当黑客获取流信息并篡改一些敏感信息重新序列化到流中后,用户通过反序列化得到的将是被篡改的信息。Java序列化提供一套验证机制。序列化类通过实现
java.io.ObjectInputValidation接口,就可以做到验证了。改写后的Apple类如下:
----------------------------------------------------------------------------------------------------
package com.fnst.infoQ;
public class Apple extends Fruit implements
Serializable,ObjectInputValidation
{
private String name;
private transient int price;
public Apple(String _name,int _price){
super(); this.name = _name; this.price = _price;
}
public Apple() {this.name = "Default Apple"; price = 4;}
public void validateObject() throws InvalidObjectException {
// TODO Auto-generated method stub
//添加验证的对象属性的hash值,来判断序列化流是否被篡改。
boolean flag = hash();
if(flag){
//未被篡改
}else{
throw new InvalidObjectException("流信息被篡改了");
}
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
//将该序列化对象注册到序列化流对象上
in.registerValidation(this, 0);
}
}
----------------------------------------------------------------------------------------------------
Java序列化验证机制的基本原理:将想要验证的序列化类注册到
java.io.ObjectInputStream类的验证回调列表中,将对象从流中反序列化出来后,会遍历回调列表,调用序列化类的validateObject方法来进行验证操作。
java.io.ObjectInputStream.readObject代码如下:
----------------------------------------------------------------------------------------------------
340 public final Object readObject()
341 throws IOException, ClassNotFoundException
342 {
-中略-
//从流中反序列化出对象obj
350 Object obj = readObject0(false);
351 handles.markDependency(outerHandle, passHandle);
//查看反序列化出来的类是否存在,如果不存在则对象缓冲器中将存在、、//ClassNotFoundException 异常对象
352 ClassNotFoundException ex = handles.lookupException(passHandle);
353 if (ex != null) {
354 throw ex;
355 }
356 if (depth == 0) {
//遍历回调列表,并调用各个注册对象的validateObject方法
357 vlist.doCallbacks();
358 }
359 return obj;
-下略-
----------------------------------------------------------------------------------------------------
在Apple.readObject方法中in.registerValidation(this, 0);的调用就是将本类对象(this)注册到验证回调列表(vlist)中。Vlist对象的doCallbacks方法如下:
java.io.ObjectOutputStream.readObject()
└java.io.ObjectOutputStream.ValidationList.doCallbacks ()
----------------------------------------------------------------------------------------------------
2199 void doCallbacks() throws InvalidObjectException {
-中略-
//遍历验证回调列表
2201 while (list != null) {
//java安全特权代码区域,具体的含义我们将在下一期的java安全管理中介绍
2202 AccessController.doPrivileged(
2203 new PrivilegedExceptionAction()
2204 {
2205 public Object run() throws InvalidObjectException {
//调用各个注册对象的validateObject方法
2206 list.obj.validateObject();
2207 return null;
2208 }
2209 }, list.acc);
2210 list = list.next;
2211 }
----------------------------------------------------------------------------------------------------
小结
本文从JDK源码的角度分析了java序列化提供的安全机制,包括自定义序列化机制以及序列化流的验证机制;其中自定义序列化提供了三种方案: java.io.Serializable、
java.io.Externalizable和自定义序列化类。第一种主要优点是java提供默认的内建支持,并且易于实现,缺点是占用空间过大,速度较慢;第二种主要优点就是系统开销较少,速度较快,但是实现需要程序员来完成;第三种,存在很大的灵活性,程序员完全可以发挥自己的聪明才智编写出更好的序列化方案。除了安全性支持外,java序列化还提供了序列化类的重构性,例如当类中增加了一项属性域的时候,当序列化化类的父类版本发生变化的时候,是否还能兼容以前的序列化数据?这些我们将在下一篇<<java序列化的可重构性>>中详细讨论。
-以上-
2013年09月22日于南京 。