- 浏览: 15138 次
最新评论
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日于南京 。
相关推荐
Java序列化是Java平台提供的一种将对象转换为字节流,以便存储、在网络上传输或者在后续时间重新创建相同对象的机制。这是Java编程中一个非常重要的概念,尤其是在分布式环境和持久化存储中。让我们深入探讨一下Java...
Java 序列化和反序列化是 Java 语言中的一种机制,用于将对象转换为字节流,以便在网络上传输或存储。序列化是将对象转换为字节流的过程,而反序列化是将字节流转换回对象的过程。 在 Java 中,序列化和反序列化是...
Java序列化是Java平台中的一种持久化机制,它允许对象的状态被转换成字节流,以便存储、网络传输或在不同时间点恢复。这个过程被称为序列化,而反向操作称为反序列化。序列化在许多场景下都非常有用,比如在分布式...
Protocol Buffer(简称PB)是Google开发的一种高效的数据序列化协议,而Java序列化是Java平台内置的一种序列化机制。两者的主要目标都是将对象转化为字节数组,便于在网络传输、持久化存储等场景中使用。然而,它们...
Java序列化是Java平台中的一种标准机制,允许将对象的状态转换为字节流,以便存储在磁盘上、通过网络进行传输或者在某些时候恢复原来的对象状态。这一过程包括两个主要步骤:对象的序列化(将对象转换为字节流)和反...
Java序列化是Java平台提供的一种持久化机制,它允许我们将一个Java对象转换为字节流,以便存储到磁盘上,或者通过网络进行传输。这使得我们可以保存和恢复对象的状态。实现序列化的类需要实现`Serializable`接口,...
Java反序列化安全漏洞是一种严重的安全威胁,它主要发生在Java程序处理序列化数据时。序列化是Java中的一种机制,可以将对象状态转换为字节流,以便存储或传输。反序列化则是将字节流恢复为Java对象的过程。 在Java...
Java序列化是Java平台中的一种标准机制,它允许将对象的状态转换为字节流,以便存储、传输或恢复。在Java中,一个类如果要实现序列化,需要实现`Serializable`接口,这是一个标记接口,不包含任何方法。下面我们将...
本篇文章将深入探讨C#和Java中的序列化与反序列化机制。 首先,我们要了解什么是序列化。序列化是指将对象的状态转化为可存储或可传输的数据格式的过程。这个过程通常将内存中的对象转换成字节流,以便保存到磁盘、...
Java序列化是Java平台中的一种标准机制,允许对象的状态被保存到磁盘或者在网络中进行传输,以便在后续的时间或地点恢复这些对象。这个过程包括两个主要操作:序列化(将对象转换为字节流)和反序列化(将字节流恢复...
5. **序列化安全**:FST可以防止恶意的反序列化攻击,通过配置可以禁止不安全的类序列化,增强系统的安全性。 6. **跨平台兼容**:与其他序列化库一样,FST序列化的数据可以在不同的Java环境中无缝传输,保证了平台...
### Java对象序列化标准知识点详解 #### 一、系统架构概览 **1.1 概览** Java 对象序列化是一种将Java对象的...以上内容涵盖了Java序列化标准的关键知识点,深入了解这些概念有助于更好地理解和应用Java序列化技术。
### Java序列化与反序列化详解 #### 一、Java序列化概述 Java序列化(Serialization...综上所述,Java序列化提供了一种简单有效的方式来处理对象的持久化和传输需求,但开发者也需要考虑其可能带来的性能和安全问题。
Java序列化和反序列化是将对象的状态转换为字节流和从字节流恢复对象状态的过程,这对于数据持久化、网络传输以及跨进程通信等场景至关重要。在Java中,`java.io.Serializable`接口用于标记一个类是可序列化的。然而...
序列化ID,即`serialVersionUID`,是Java序列化机制中一个关键的概念。它是一个类的唯一标识符,用于在序列化和反序列化过程中确定类的版本一致性。如果序列化对象和反序列化对象的`serialVersionUID`不匹配,将会抛...
Java序列化是Java平台提供的一种将对象转换为字节流,以便存储到磁盘、数据库或网络中的机制。它是Java语言内置的一种特性,主要用于持久化数据,也可以在进程间传递对象,或者在网络上传输对象。在Java中,如果一个...
1. **Java序列化机制**:Java对象序列化是通过实现`Serializable`接口来标记一个类可被序列化。`ObjectOutputStream`用于将对象写入流,`ObjectInputStream`用于从流中读取并反序列化对象。 2. **易受攻击的库**:...
因此,提供的工具可能专门针对这两个服务器的反序列化安全问题。 WebLogic反序列化漏洞,例如著名的CVE-2015-4852,允许攻击者通过发送特制的序列化对象到受影响的服务器端点,来执行任意代码。同样,JBoss应用...
Java序列化是Java平台提供的一种将对象转换为字节流,以便存储、网络传输或持久化数据的标准机制。它在Java开发中起着至关重要的作用,特别是在分布式系统和跨进程通信中,因为对象需要在网络之间传输。以下是关于...
8. **序列化安全**: - 序列化可能引发安全问题,如序列化漏洞。因此,应当谨慎处理反序列化数据,尤其是在网络环境下。 9. **优化序列化**: - 对于大型对象,序列化可能会消耗大量资源。可以通过实现`...