Java オブジェクト シリアライゼーションのセキュリティ

Java オブジェクト シリアライゼーションの理解を深めることで得られる 5 つの知見

Java オブジェクト シリアライゼーションは、JDK 1.1 から組み込まれている仕組みです。この仕組みは、Java オブジェクトの状態をバイト配列に変換し、格納したり通信したりするのに役立ちます。この配列を復元することで、当初の状態に戻すことができます。

シリアライゼーションの仕組みは、ObjectInputStream クラスと ObjectOutputStream クラス、そして Serializable インターフェースを用いて実現されています。この仕組みは非常に便利ですが、セキュリティ面でのリスクも孕んでいます。

1. シリアライゼーションはクラスリファクタリングに対応する

Java シリアライゼーションは、一定範囲内のクラスの変更やリファクタリングを許可します。具体的には、以下のような変更が可能です:

  • 新フィールドの追加
  • static フィールドへの変更
  • transient フィールドへの変更

ただし、フィールドを削除したり、型を変更したりする場合、serialVersionUID を明示的に指定する必要があります。

2. シリアライゼーションは不安全である

Java シリアライゼーションされたデータは、完全にドキュメント化されており、容易に復元可能です。特に RMI を通じて送信されるオブジェクトのプライベートフィールドは、通常は平文で送信されます。

このリスクを軽減するため、writeObjectreadObject メソッドをオーバーライドし、データを暗号化することができます。

package com.example; public class User implements java.io.Serializable { private String name; private String familyName; private int years; private User partner; public User(String name, String familyName, int years) { this.name = name; this.familyName = familyName; this.years = years; } public String getName() { return name; } public String getFamilyName() { return familyName; } public int getYears() { return years; } public User getPartner() { return partner; } public void setName(String value) { name = value; } public void setFamilyName(String value) { familyName = value; } public void setYears(int value) { years = value; } public void setPartner(User value) { partner = value; } private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException { years = years << 2; stream.defaultWriteObject(); } private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { stream.defaultReadObject(); years = years << 2; } }

3. シリアライゼーションされたデータを署名と封印できる

敏感データを保護するためには、単なる暗号化ではなく、データを署名したり封印したりする必要があります。

Java では、java.security.SignedObjectjavax.crypto.SealedObject を用いて、この目的を達成することができます。

import java.security.SignedObject; import javax.crypto.SealedObject; public class SecureUser implements java.io.Serializable { private String name; private String familyName; private User partner; public SecureUser(String name, String familyName) { this.name = name; this.familyName = familyName; } public String getName() { return name; } public String getFamilyName() { return familyName; } public User getPartner() { return partner; } public void setName(String value) { name = value; } public void setFamilyName(String value) { familyName = value; } public void setPartner(User value) { partner = value; } private Object writeReplace() throws java.io.ObjectStreamException { return new SealedObject(this, "AES", new byte[16]); } }

4. シリアライゼーションにプロキシを組み込める

プロキシを用いて、シリアライゼーションされたデータを制御することができます。これにより、データの転送や格納を効率的に行うことができます。

class UserProxy implements java.io.Serializable { private String userData; public UserProxy(User original) { userData = original.getName() + "," + original.getFamilyName(); if (original.getPartner() != null) { User partner = original.getPartner(); userData += "," + partner.getName() + "," + partner.getFamilyName(); } } private Object readResolve() throws java.io.ObjectStreamException { String[] parts = userData.split(","); User user = new User(parts[0], parts[1]); if (parts.length > 3) { User partner = new User(parts[2], parts[3]); user.setPartner(partner); partner.setPartner(user); } return user; } } public class User implements java.io.Serializable { public User(String name, String familyName) { this.name = name; this.familyName = familyName; } private Object writeReplace() throws java.io.ObjectStreamException { return new UserProxy(this); } private String name; private String familyName; private User partner; }

5. トラストするが検証もする

シリアライゼーションされたデータを鵜呑みにせず、検証を行う必要があります。

Java では、ObjectInputValidation インターフェースを用いて、反復復元後のデータを検証できます。

public class ValidatedUser implements java.io.Serializable, java.io.ObjectInputValidation { private String name; private String familyName; private int years; public ValidatedUser(String name, String familyName, int years) { this.name = name; this.familyName = familyName; this.years = years; } public void validateObject() throws java.io.InvalidObjectException { if (years < 0 || years > 150) { throw new java.io.InvalidObjectException("年齢の範囲が不正です。"); } } }

タグ: Java シリアライゼーション セキュリティ transient SealedObject

6月3日 20:56 投稿