java-recipes

ホーム シリアライズ・デシリアライズ › Ser-04

Ser-04: Java デシリアライズの脆弱性

Java の ObjectInputStream.readObject() は 便利ですが、信頼できないソースから受け取ったデータをそのままデシリアライズすると 深刻なセキュリティリスクになります。 このページでは脆弱性の仕組みと、ObjectInputFilter などの対策を解説します。

セキュリティリスク: 信頼できないデータのデシリアライズは危険です

信頼できないソース(ネットワーク・外部ファイルなど)から受け取ったバイト列をObjectInputStream.readObject() で読み込むと、 攻撃者が細工した「ガジェットチェーン」によって任意コードが実行される可能性があります(RCE: Remote Code Execution)。 2015 年には Apache Commons Collections の脆弱性(CVE-2015-7501)として大きく報告されました。

脆弱性の仕組みと対策

Java のデシリアライズは、バイト列からオブジェクトを復元するときに クラスのコンストラクタを経由せずに直接フィールドを設定します。 このため、クラスパス上に存在するクラスを組み合わせた「ガジェットチェーン」を バイト列に埋め込むことで、デシリアライズ時に任意のコードを実行させることができます。

対策方法Java バージョン概要
resolveClass オーバーライドJava 8+ObjectInputStream を継承してクラス名をホワイトリストで検証
ObjectInputFilterJava 9+(推奨)setObjectInputFilter() でパターン文字列によるホワイトリスト設定
sealed interfaceJava 17+(設計レベル)許可する型をコンパイル時に明示して意図しないクラスを除外
JSON への移行バージョン不問ObjectInputStream を使わず Jackson/Gson などで交換

サンプルコード

Java 8 版では resolveClass をオーバーライドしてホワイトリスト検証を実装しています。 Java 17 版では Java 9+ で追加された ObjectInputFilter を使った 簡潔なホワイトリスト設定を紹介します。 Java 21 版では sealed interface で デシリアライズ可能な型を設計レベルで制限し、パターンマッチング switch で型安全に処理します。

DeserializationSecuritySample.java
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class DeserializationSecuritySample {

    // 安全なクラス(ホワイトリストに含める)
    static class SafeData implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String value;

        SafeData(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "SafeData{value='" + value + "'}";
        }
    }

    // Java 9+ の ObjectInputFilter を使ったホワイトリスト検証
    // Java 8 では手動で resolveClass をオーバーライドして検証
    static class SecureObjectInputStream extends ObjectInputStream {
        private final List<String> allowedClasses;

        SecureObjectInputStream(InputStream in, List<String> allowedClasses)
                throws IOException {
            super(in);
            this.allowedClasses = allowedClasses;
        }

        @Override
        protected Class<?> resolveClass(ObjectStreamClass desc)
                throws IOException, ClassNotFoundException {
            String className = desc.getName();
            boolean allowed = false;
            for (String allowedClass : allowedClasses) {
                if (className.equals(allowedClass)) {
                    allowed = true;
                    break;
                }
            }
            if (!allowed) {
                throw new InvalidClassException(
                    "デシリアライズ拒否(ホワイトリスト外のクラス): " + className);
            }
            return super.resolveClass(desc);
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("=== デシリアライズのセキュリティリスク ===");
        System.out.println();
        System.out.println("【脆弱性の仕組み】");
        System.out.println("1. 攻撃者が細工した悪意あるバイト列を送信");
        System.out.println("2. サーバーが ObjectInputStream.readObject() で読み込む");
        System.out.println("3. デシリアライズ時にガジェットチェーンが実行される");
        System.out.println("4. 任意コード実行(RCE: Remote Code Execution)");
        System.out.println();
        System.out.println("【有名な脆弱性事例】");
        System.out.println("- Apache Commons Collections(2015年)");
        System.out.println("- WebLogic, JBoss, Jenkins などに影響");
        System.out.println("- CVE-2015-7501 など");

        // 安全なデシリアライズの例
        SafeData original = new SafeData("テストデータ");

        // シリアライズ
        byte[] bytes;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(original);
            bytes = baos.toByteArray();
        }

        // ホワイトリスト付きデシリアライズ
        List<String> allowedClasses = new ArrayList<>();
        allowedClasses.add(DeserializationSecuritySample.class.getName() + "$SafeData");
        allowedClasses.add("java.lang.String");

        try (SecureObjectInputStream sois = new SecureObjectInputStream(
                new ByteArrayInputStream(bytes), allowedClasses)) {
            Object obj = sois.readObject();
            System.out.println("\n安全なデシリアライズ成功: " + obj);
        }

        System.out.println("\n=== 対策まとめ ===");
        System.out.println("1. Java 9+: ObjectInputFilter でホワイトリスト設定");
        System.out.println("   ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(");
        System.out.println("       \"com.example.SafeData;java.lang.*;!*\");");
        System.out.println("   ois.setObjectInputFilter(filter);");
        System.out.println("2. 信頼できないソースの ObjectInputStream は使用しない");
        System.out.println("3. JSON(Jackson/Gson)でのデータ交換に切り替える");
        System.out.println("4. serialization proxy パターンの採用");
    }
}

よくあるミス・注意点

ホワイトリストを設定しない ObjectInputStream は使用しない

通常の new ObjectInputStream(inputStream) は クラスパス上のあらゆるクラスをデシリアライズできます。 信頼できないソースからのデータには必ず ObjectInputFilterresolveClass オーバーライドでフィルタリングしてください。

フィルターパターンの末尾に !* を忘れない

ObjectInputFilter.Config.createFilter() のパターンで 末尾に !* を付けないと、 ホワイトリストに含まれないクラスも許可されてしまいます。 必ず "com.example.SafeClass;java.lang.*;!*" のように末尾で全拒否してください。

Java バージョンごとの違い

ObjectInputFilter は Java 9 以降で利用できます。 Java 8 環境では ObjectInputStream を継承してresolveClass をオーバーライドする方法を使います。 Java 17 以降では ObjectInputFilter が推奨です。

テストする観点

  • ホワイトリストに含まれるクラスは正常にデシリアライズできること
  • ホワイトリストに含まれないクラスのデシリアライズで例外が発生すること(境界値:1クラスのみ許可した場合)
  • ObjectInputFilter のパターンに !* を含めないとフィルターが機能しないこと
  • シリアライズしたデータを改ざんした場合に InvalidClassException または StreamCorruptedException が発生すること
  • Java 8 版の SecureObjectInputStream で、許可リスト外のクラス名を渡した場合に InvalidClassException がスローされること

GitHub でソースコードを見る →