概要

Java のデシリアライズ脆弱性は、2015年の Apache Commons Collections を皮切りに、WebLogic・JBoss・Jenkins など数多くのプロダクトで深刻なリモートコード実行(RCE)を引き起こしました。攻撃者が細工したバイト列を ObjectInputStream.readObject() で読み込むと、ガジェットチェーンと呼ばれるクラスの連鎖を通じて任意のコマンドが実行されます。Java 9 以降は ObjectInputFilter によるホワイトリスト方式が標準 API として提供され、許可するクラスを限定することでリスクを軽減できるようになりました。この記事では、脆弱性が発生する仕組みを概要レベルで整理したうえで、Java 8 での resolveClass オーバーライドによる手動フィルタリング、Java 9+ の ObjectInputFilter、そして Java 21 の sealed interface を組み合わせた多層防御のアプローチを、動くコードとともに解説します。

使いどころ

外部システムからバイナリデータを受け取る API で、信頼できないソースのデシリアライズを安全に処理する

レガシーシステムの Java シリアライズ通信を ObjectInputFilter で段階的に安全化する

セキュリティ監査で指摘されたデシリアライズ脆弱性に対し、ホワイトリストフィルターを導入して対策する

コード例

DeserializationSecurityDemo.java
import java.io.*;

public class DeserializationSecurityDemo {

    // シリアライズ対象の安全なクラス
    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 + "'}";
        }
    }

    public static void main(String[] args) throws Exception {
        var original = new SafeData("テストデータ");

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

        // Java 9+: ObjectInputFilter でホワイトリスト設定
        // 許可するクラスを明示し、それ以外は !* で全拒否
        String allowedClass =
                DeserializationSecurityDemo.class.getName() + "$SafeData";
        String filterPattern = allowedClass + ";java.lang.*;!*";
        ObjectInputFilter filter =
                ObjectInputFilter.Config.createFilter(filterPattern);

        try (var bais = new ByteArrayInputStream(bytes);
             var ois = new ObjectInputStream(bais)) {
            ois.setObjectInputFilter(filter);
            Object obj = ois.readObject();
            System.out.println("安全なデシリアライズ成功: " + obj);
        }

        // maxdepth / maxarray で構造も制限可能
        String strictPattern = allowedClass
                + ";java.lang.*;maxdepth=5;maxarray=100;!*";
        ObjectInputFilter strictFilter =
                ObjectInputFilter.Config.createFilter(strictPattern);
        System.out.println("厳格フィルター: " + strictPattern);

        try (var bais = new ByteArrayInputStream(bytes);
             var ois = new ObjectInputStream(bais)) {
            ois.setObjectInputFilter(strictFilter);
            Object obj = ois.readObject();
            System.out.println("厳格フィルターでも成功: " + obj);
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

ObjectInputFilter.Config.createFilter でフィルターパターンを文字列で指定できる。setObjectInputFilter でストリーム単位のフィルター設定が可能。

Java 17
// Java 17: ObjectInputFilter でホワイトリスト設定
String pattern = "com.example.SafeData;java.lang.*;!*";
ObjectInputFilter filter =
    ObjectInputFilter.Config.createFilter(pattern);
ois.setObjectInputFilter(filter);

Library Comparison

標準 API(ObjectInputFilter)Java 9 以降で Java シリアライズを使い続ける必要がある場合の必須対策。追加依存なしで導入できる。ホワイトリストの管理が手動になるため、新しいクラスの追加忘れに注意が必要。根本的な脆弱性の排除にはならない。
Jackson(JSON)Java シリアライズからの移行先として最も一般的。PolymorphicDeserialization にも ObjectMapper の設定で対応できる。Jackson 自体にも過去にデシリアライズ脆弱性(CVE-2017-7525 等)があるため、DefaultTyping の使用には注意が必要。
Protocol Buffers / Avroスキーマ定義に基づくデシリアライズで、未知の型を原理的に排除したいとき。既存の Java シリアライズからの移行コストが大きい。.proto / .avsc ファイルの管理が必要になる。

注意点

ObjectInputFilter の !*(全拒否)は必ずパターンの末尾に置く。先頭に置くと全クラスが拒否されてしまう

ホワイトリストには対象クラスだけでなく java.lang.* も含める必要がある。String 等の基本型が拒否されるとデシリアライズが失敗する

ObjectInputFilter は Java 9 以降の機能。Java 8 では ObjectInputStream を継承して resolveClass をオーバーライドする手動フィルタリングが必要

フィルターを設定しても、許可したクラス自体に脆弱性がある場合は防げない。根本的な対策は Java シリアライズから JSON 等への移行

maxdepth や maxarray の制限値はアプリケーションのデータ構造に合わせて調整する。過度に厳しい値は正常なデシリアライズも失敗させる

FAQ

ObjectInputFilter のパターン構文で !* は何を意味しますか。

!* は「それ以外の全クラスを拒否する」を意味します。許可するクラスを列挙したあと、末尾に !* を置くことでホワイトリスト方式になります。

Java 8 環境でデシリアライズのセキュリティ対策をするにはどうしますか。

ObjectInputStream を継承して resolveClass をオーバーライドし、クラス名のホワイトリストチェックを手動で実装します。あるいは Java シリアライズ自体を JSON に置き換えるのが根本的な対策です。

ObjectInputFilter を設定すればデシリアライズは安全ですか。

リスクを大幅に軽減できますが、完全ではありません。許可したクラス自体に脆弱性がある場合は防げないため、信頼できないソースからの Java シリアライズの受信は避けるのが原則です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます