信頼できないデータの逆シリアル化

信頼できないデータの逆シリアル化

説明

Javaのネイティブシリアル化は、バイトストリームからクラス情報とオブジェクトグラフを復元します。Java 25の ObjectInputStream ドキュメントは、信頼できないデータの逆シリアル化は本質的に危険であり、避けるべきとしています。

new ObjectInputStream(input) コンストラクターはストリームのヘッダーとバージョンを読み取って確認しますが、まだオブジェクトを復元しません。実際の逆シリアル化は readObject() または readUnshared() で始まります。参照先のオブジェクトグラフをたどり、次のようなクラス固有の処理を呼び出す場合があります。

  • シリアル化可能なクラスの readObject と readResolve
  • Externalizable 実装のコンストラクターと readExternal
  • 動的プロキシやクラス解決の処理
  • グラフに含まれる他のオブジェクトの同様の逆シリアル化処理

攻撃者がストリームを制御でき、実行時のクラスパスに悪用可能なガジェットチェーンがあると、アプリケーションの権限でコードを実行されるおそれがあります。コード実行に使えるガジェットがなくても、不正なオブジェクト状態、データ改ざん、例外の繰り返し、大きな配列や大量の参照、深い構造によるリソース枯渇が起こる場合があります。

想定される影響

  • 任意のコード実行: 逆シリアル化中にガジェットチェーンのメソッドやプロセス実行APIが呼び出される場合があります。
  • データや状態の改ざん: 通常のコンストラクターを経ずにフィールドが復元され、アプリケーションの不変条件が崩れるおそれがあります。
  • 情報漏えい: ガジェットや後続処理がファイル、シークレット、内部サービスへアクセスする可能性があります。
  • サービス拒否: 深いグラフ、多数の参照、大きな配列、悪意あるクラスの処理がCPUやメモリを消費するおそれがあります。

対処方法

1. 外部入力からJavaのネイティブシリアル化を取り除く

JSONなど、クラスの実行を伴わないデータ専用形式を使い、明示的なDTOへバインドしてください。まずリクエストサイズを制限し、DTOのフィールドをアプリケーションの業務上の制約に従って検証してください。JavaとKotlinの具体例を後述します。

2. すぐに廃止できない場合は用途別のフィルターを適用する

互換性のために一時的に ObjectInputStream を残す場合は、オブジェクトを読む前にストリームごとの ObjectInputFilter を設定してください。

  • プロトコルに必要なクラスとモジュールだけを許可し、それ以外を拒否してください。
  • UNDECIDED を許可とみなさないでください。
  • グラフの深さ、参照の総数、配列長、消費バイト数を制限してください。
  • 制限的なJVM全体のフィルターも追加の防御として適用してください。
  • フィルターの設定後にだけ readObject() または readUnshared() を呼び出してください。

Javaのシリアル化フィルターは、既定では有効になりません。クラス名の拒否リストや一部のクラスだけを確認する resolveClass のオーバーライドでは、新たなガジェット、配列、プロキシ、見落としたクラスパスをすべてカバーできません。フィルターは移行中の補助的な対策であり、データ専用形式への置き換えの代わりにはなりません。

3. 信頼境界と実行環境を制限する

  • 信頼できる作成者だけがデータを供給する必要がある場合は、逆シリアル化前に正確なバイト列の署名またはHMACを検証してください。
  • 署名やHMACは、権限を持つ悪意ある作成者や広すぎるクラス許可リストを安全にはしません。
  • ガジェットになり得る不要な依存関係をクラスパスから取り除き、JDKとライブラリーに最新のセキュリティパッチを適用してください。
  • 残る互換処理は最小権限で実行し、既存レコードをデータ専用形式へ移行してください。

例

Java

変更前

java
import java.io.ObjectInputStream;

import jakarta.servlet.http.HttpServletRequest;

final class ProfileEndpoint {
    Object readProfile(HttpServletRequest request) throws Exception {
        try (ObjectInputStream input =
                new ObjectInputStream(request.getInputStream())) {
            return input.readObject();
        }
    }
}

ObjectInputStream の生成ではヘッダーだけを読みます。オブジェクトグラフを復元してクラス固有の処理を呼び出すのは input.readObject() です。

変更後

新しいJavaプロジェクトでは、サポート対象のJackson系列の最新セキュリティパッチを使ってください。この例はJackson 3 APIを使います。原文を確認した2026年9月2日時点では、jackson-databind 3.1系列の最新タグは3.1.6でした。

Jacksonのクラス名に基づく既定のポリモーフィック型処理を有効にしないでください。多態性が必要なら、サーバーで定義した論理型名と限定的なサブタイプの許可リストを使ってください。

java
import java.io.IOException;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tools.jackson.databind.json.JsonMapper;

final class ProfileEndpoint {
    private static final int MAX_BODY_BYTES = 64 * 1024;
    private static final JsonMapper JSON = JsonMapper.builder().build();

    record Profile(String name, int age) {}

    Profile readProfile(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        if (request.getContentLengthLong() > MAX_BODY_BYTES) {
            response.sendError(413);
            return null;
        }

        byte[] body = request.getInputStream().readNBytes(MAX_BODY_BYTES + 1);
        if (body.length > MAX_BODY_BYTES) {
            response.sendError(413);
            return null;
        }

        Profile profile = JSON.readValue(body, Profile.class);
        if (profile.name() == null
                || profile.name().isBlank()
                || profile.name().length() > 100
                || profile.age() < 0
                || profile.age() > 130) {
            response.sendError(400);
            return null;
        }
        return profile;
    }
}

DTOへのバインド後の検証は、JSON値の意味を制限します。安全でないネイティブ逆シリアル化の後で返されたオブジェクトだけを検証しても、すでに実行されたガジェットの動作は取り消せません。

Kotlin

変更前

kotlin
import java.io.ObjectInputStream

import jakarta.servlet.http.HttpServletRequest

fun readProfile(request: HttpServletRequest): Any =
    ObjectInputStream(request.inputStream).use { it.readObject() }

Kotlinの use はストリームを閉じますが、信頼できないオブジェクトグラフの復元を安全にはしません。この例で危険が生じる処理は it.readObject() です。

変更後

Kotlin公式の kotlinx.serialization では、安定したJSON形式とコンパイラー生成のシリアライザーを使えます。原文を確認した2026年9月2日時点の公式導入ガイドは、Kotlin 2.4.10と kotlinx-serialization-json 1.11.0を使用しています。多態性が必要なら、サーバーで明示的に登録したサブタイプに限定してください。

kotlin
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

private const val MAX_BODY_BYTES = 64 * 1024

@Serializable
data class Profile(val name: String, val age: Int)

fun readProfile(
    request: HttpServletRequest,
    response: HttpServletResponse,
): Profile? {
    if (request.contentLengthLong > MAX_BODY_BYTES) {
        response.sendError(413)
        return null
    }

    val body = request.inputStream.readNBytes(MAX_BODY_BYTES + 1)
    if (body.size > MAX_BODY_BYTES) {
        response.sendError(413)
        return null
    }

    val profile = Json.decodeFromString<Profile>(body.decodeToString())
    if (
        profile.name.isBlank() ||
        profile.name.length > 100 ||
        profile.age !in 0..130
    ) {
        response.sendError(400)
        return null
    }
    return profile
}

この例は入力サイズを先に制限し、JSONを明示的な Profile 型へ変換して業務上の制約を確認します。入力にネイティブJVMクラス名を選ばせません。

参考資料