关于序列化与反序列化安全问题
将敏感对象发送出信任区域前进行签名并加密
敏感数据传输过程中要防止窃取和恶意篡改。使用安全的加密算法加密传输对象可以保护数据。这就是所谓的对对象进行密封。而对密封的对象进行数字签名则可以防止对象被非法篡改,保持其完整性。在以下场景中,需要对对象密封和数字签名来保证数据安全:
- 序列化或传输敏感数据
- 没有诸如SSL传输通道一类的安全通信通道或者对于有限的事务来说代价太高
- 敏感数据需要长久保存(比如在硬盘驱动器上)
应该避免使用私有加密算法。这类算法大多数情况下会引入不必要的漏洞。
示例:
demo class
/**
* 测试 map demo class
*/
public class SerializableMap<K, V> implements Serializable {
final static long serialVersionUID = 45217497203262395L;
private Map<K, V> map;
public SerializableMap()
{
map = new HashMap<K, V>();
}
public V getData(K key)
{
return map.get(key);
}
public void setData(K key, V data)
{
map.put(key, data);
}
}
/**
* 构造map数据
*
*/
public class MapBuilder {
public static SerializableMap<String, Integer> buildMap() {
SerializableMap<String, Integer> map = new SerializableMap<String, Integer>();
map.setData("John Doe", new Integer(123456789));
map.setData("Richard Roe", new Integer(246813579));
return map;
}
public static void InspectMap(SerializableMap<String, Integer> map) {
System.out.println("John Doe's number is " + map.getData("John Doe"));
System.out.println("Richard Roe's number is "
+ map.getData("Richard Roe"));
}
}
开始序列化以及反序列化
public class TestSerializer {
// 加密器
private static Cipher cipher;
// 加密 解密 key
private static SecretKey key;
// 签名 key
private static KeyPair kp;
// 签名
private static Signature sig;
public static void main(String[] args) throws Exception {
// 仅加密
encryption();
// 先加密后签名,会伪造签名恶意攻击产生
// 先签名,后加密
encryptionAfterSig();
}
/**
* 只加密,无法进行可靠性验证
*
* @throws Exception
*/
private static void encryption() throws Exception {
// Build map
SerializableMap<String, Integer> map = buildMap();
SealedObject sealedMap = encrypt(map);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// Unseal map
map = (SerializableMap<String, Integer>) decrypt(sealedMap);
// Inspect map
InspectMap(map);
}
/**
* 先签名 后加密
*/
private static void encryptionAfterSig() throws Exception {
// Build map
SerializableMap<String, Integer> map = buildMap();
// sig
SignedObject signedMap = sigObject(map);
// encrypt
SealedObject sealedMap = encrypt(signedMap);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
signedMap = (SignedObject) decrypt(sealedMap);
// Verify signature and retrieve map
if (!signedMap.verify(kp.getPublic(), sig)) {
throw new GeneralSecurityException("Map failed verification");
}
map = (SerializableMap<String, Integer>) signedMap.getObject();
// Inspect map
InspectMap(map);
}
/**
* 签名
*
* @param map
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws IOException
* @throws SignatureException
*/
private static SignedObject sigObject(Serializable map) throws NoSuchAlgorithmException, InvalidKeyException, IOException, SignatureException {
// Generate signing public/private key pair & sign map
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kp = kpg.generateKeyPair();
sig = Signature.getInstance("SHA256withRSA");
return new SignedObject(map, kp.getPrivate(), sig);
}
/**
* 加密
*
* @param obj
* @return
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws IOException
* @throws IllegalBlockSizeException
* @throws InvalidKeyException
*/
private static SealedObject encrypt(Serializable obj) throws NoSuchAlgorithmException, NoSuchPaddingException, IOException, IllegalBlockSizeException, InvalidKeyException {
// password
String password = "this is an encrypted key";
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128, new SecureRandom(password.getBytes(Charset.defaultCharset())));
key = generator.generateKey();
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return new SealedObject(obj, cipher);
}
/**
* 解密
*
* @param sealedMap
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws ClassNotFoundException
* @throws IOException
*/
private static Object decrypt(SealedObject sealedMap) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, ClassNotFoundException, IOException, BadPaddingException, IllegalBlockSizeException {
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return sealedMap.getObject(cipher);
}
禁止序列化未加密的敏感数据
虽然序列化可以将对象的状态保存为一个字节序列,之后通过反序列化该字节序列又能重新构造出原来的对象,但是它并没有提供一种机制来保证序列化数据的安全性。可访问序列化数据的攻击者可以借此获取敏感信息并确定对象的实现细节。攻击者也可恶意修改其中的数据,试图在其被反序列化之后对系统造成危害。因此,敏感数据序列化之后是潜在对外暴露着的。永远不应该被序列化的敏感信息包括:密钥、数字证书、以及那些在序列化时引用敏感数据的类。此条规则的意义在于防止敏感数据被无意识的序列化导致敏感信息泄露。
在将某个包含敏感数据的类序列化时,程序必须确保敏感数据不被序列化。这包括阻止包含敏感信息的数据成员被序列化,以及不可序列化或者敏感对象的引用被序列化。该示例将相关字段声明为transient,从而使它们不包括在依照默认的序列化机制应该被序列化的字段列表中。这样既避免了错误的序列化,又防止了敏感数据被意外序列化。
通过定义serialPersistentFields数组字段来确保敏感字段被排除在序列化之外,除了上述方案,也可以通过自定义writeObject()、writeReplace()、writeExternal()这些函数,不将包含敏感信息的字段写到序列化字节流中。
public class GPSLocation implements Serializable {
private double x;
private double y;
private String id;
// 敏感字段x,y不在序列化字段数组 serialPersistentFields中,将不会被序列化
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("id", String.class)};
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String toString() {
return "GPSLocation{" +
"x=" + x +
", y=" + y +
", id='" + id + '\'' +
'}';
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
GPSLocation gps = new GPSLocation();
gps.setX(72.0);
gps.setY(118.22);
gps.setId("id");
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(gps);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
gps = (GPSLocation) in.readObject();
in.close();
System.out.println(gps);
}
}
防止序列化和反序列化被利用来绕过安全管理
序列化和反序列化可能被利用来绕过安全管理器的检查。一个可序列化类的构造器中出于防止不可信代码修改类的内部状态等原因可能需要引入安全管理器的检查。这种安全管理器的检查必须应用到所有能够构建类实例的地方。例如,如果某个类依据安全检查的结果来判定调用者是否能够读取其敏感内部状态,那么这类安全检查必须也在反序列化中应用。这就确保了攻击者无法通过反序列化对象来提取敏感信息。
// 错误示例
public final class Hometown implements Serializable
{
private static final long serialVersionUID = 9078808681344666097L;
// Private internal state
private String town;
private static final String UNKNOWN = "UNKNOWN";
void performSecurityManagerCheck() throws SecurityException
{
// verify whether current user has rights to access the file
}
void validateInput(String newCC) throws InvalidInputException
{
// ...
}
public Hometown()
{
performSecurityManagerCheck();
// Initialize town to default value
town = UNKNOWN;
}
// Allows callers to retrieve internal state
String getValue()
{
performSecurityManagerCheck();
return town;
}
// Allows callers to modify (private) internal state
public void changeTown(String newTown) throws InvalidInputException
{
if (town.equals(newTown))
{
// No change
return;
}
else
{
performSecurityManagerCheck();
validateInput(newTown);
town = newTown;
}
}
private void writeObject(ObjectOutputStream out) throws IOException
{
out.writeObject(town);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
in.defaultReadObject();
// If the deserialized name does not match
// the default value normally
// created at construction time, duplicate the checks
if (!UNKNOWN.equals(town))
{
validateInput(town);
}
}
}
错误示例中,安全管理器检查被应用在构造器中,但在序列化与反序列化涉及的writeObject()和readObject()方法中没有用到。这样会允许非信任代码恶意创建类实例。
正确示例
public final class Hometown implements Serializable
{
// ... all methods the same except the following:
// writeObject() correctly enforces checks during serialization
private void writeObject(ObjectOutputStream out) throws IOException
{
performSecurityManagerCheck();
out.writeObject(town);
}
// readObject() correctly enforces checks during deserialization
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
in.defaultReadObject();
// If the deserialized name does not match the default value normally
// created at construction time, duplicate the checks
if (!UNKNOWN.equals(town))
{
performSecurityManagerCheck();
validateInput(town);
}
}
}