CodeGym /コース /JAVA 25 SELF /シリアライゼーションのセキュリティ: ベストプラクティス

シリアライゼーションのセキュリティ: ベストプラクティス

JAVA 25 SELF
レベル 43 , レッスン 4
使用可能

1. 安全なシリアライゼーションのための基本的なベストプラクティス

シリアライゼーションは空港で荷物を梱包するようなものです。中身を知らず、誰にスーツケースを預けるのかも分からないと、保安検査で不愉快なサプライズを招きかねません。Java のシリアライゼーションはオブジェクトの保存と復元を容易にしますが、信頼できないソースからデータが来る場合には、さまざまな攻撃に道を開いてしまいます。

典型的な脅威:

Java のシリアライゼーションは安全とは限りません。攻撃者が悪意あるストリームを差し込むと、デシリアライゼーション時に最悪の結果を招く可能性があります。フィールドの改変から望ましくないコードの実行まで起こり得ます。これは教科書の脅しではありません。実際に Java の歴史の中で、この仕組みを悪用した攻撃事例が存在します。

なぜそうなるのか?

デシリアライゼーションは単なるフィールド値の復元ではありません。処理の中で完全なオブジェクトが生成され、特別なメソッド(例えば readObjectreadResolve)が呼ばれることがあり、場合によってはリフレクション経由で脆弱なコードパスに触れてしまうこともあります。特にサードパーティ製ライブラリのクラスは危険で、デシリアライゼーション段階ですでに何らかの動作を行うものがあります。したがって、外部から受け取ったシリアライズ済みデータを決して信用しないでください。

transient を敏感情報に使う

クラスにパスワード、トークン、秘密鍵などの機微情報を含むフィールドがある場合は、それらを transient として宣言してください。これらのデータはシリアライズされたストリームに含まれません。

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // シリアライズされない

    // ...コンストラクタ、ゲッター、セッター...
}

デシリアライズ時はどうなる? フィールド password はデフォルト値(文字列なら null)になります。これは好ましい挙動で、パスワードがファイルに保存されたりネットワークで送信されたりしません.

serialVersionUID を明示的に定義する

常に明示的に serialVersionUID を指定しましょう。互換性エラーの可能性を下げ、デシリアライズ時のクラス差し替えリスクを最小化します。

private static final long serialVersionUID = 1L;

セキュリティ上なぜ重要? serialVersionUID を指定しない場合、コンパイラがクラス構造に基づいて自動生成します。これにより予期せぬ不一致が起きたり、理論上は同名だが構造の異なるクラスに差し替えられる悪用を招く可能性があります。

デシリアライゼーション時にオブジェクトの型を検証する

ネットワークやファイルから来たものを信用しないでください。デシリアライズ後は常に、得られたオブジェクトが期待する型であることを確認してから扱いましょう。

Object obj = objectInputStream.readObject();
if (obj instanceof User) {
    User user = (User) obj;
    // user を安全に扱う
} else {
    // 予期しない型 — 例外を投げるかエラー処理する
}

なぜ必要? 悪意あるストリームには、Serializable を実装していてもビジネスロジックに合致しない別のクラスのオブジェクトが含まれている可能性があります。

デシリアライズを許可するクラスを制限する(ObjectInputFilter

Java 9 以降ではフィルター — ObjectInputFilter を使って、デシリアライズを許可するクラスの集合を制限しましょう。入場ゲートのホワイトリストのようなものです。

例: フィルターの設定

import java.io.*;

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;com.example.Address;!*"
);

ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);

Object obj = in.readObject(); // これでデシリアライズされるのは User と Address のみ

このフィルターはアプリケーションの UserAddress クラスのみを許可します。その他はブロックされ、例外が送出されます。これにより悪意あるオブジェクトの混入リスクが大幅に低減します。

信頼できないソースのデータはデシリアライズしない

鉄則: データの出所に自信がないなら — デシリアライズしない。解析時にコードを実行しないフォーマット(例えば JSON、安全なパーサーを用いた XML)を優先しましょう。

悪いプラクティスの例:

// インターネット由来のデータでは絶対にこうしないこと!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // 危険!

どうするのが良いか?

  • JSON パーサー(例: Gson/Jackson)または検証付きの XML パーサーを使う。
  • バイナリシリアライゼーションが必要な場合は — ObjectInputFilter でクラスをフィルタし、instanceof で型を検証する。

外部システムとのやり取りには代替フォーマットを使う

統合用途では、解析時にコードを実行しないフォーマット(JSON、XML、Protocol Buffers など)を使いましょう。これによりデシリアライズ経由の攻撃はほぼ排除できます。

// ObjectInputStream の代わりに JSON パーサーを使う
User user = gson.fromJson(jsonString, User.class);

シリアライズ済みオブジェクトを公開領域に保存しない

シリアライズされたオブジェクトのファイルは機微情報を含むことがあります。公開ディレクトリに保存せず、ファイルシステムレベルでアクセス権を制限してください。

完全性の担保にシリアライゼーションを頼らない

シリアライゼーションはデータの完全性や真正性を保証しません。変更が許されない場合はデジタル署名、チェックサム、または暗号化を使用してください。

2. 実践: ObjectInputFilter の例と脆弱性のデモ

クラスのフィルタリング例

例えば、User クラスがあるとします:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password;

    // ...コンストラクタ、ゲッター、セッター...
}

フィルターは User のみを許可します:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;!*"
);
in.setObjectInputFilter(filter);

この状態で別のクラスのオブジェクトを送り込もうとしても、デシリアライズはエラーで終了します。

潜在的な脆弱性のデモ

悪意のあるクラス:

// こんなクラスを誰かが紛れ込ませたと仮定する
public class Evil implements java.io.Serializable {
    static {
        System.out.println("悪意あるコードが実行されました!");
        // ここには何でも書ける...
    }
}

クラスをフィルタしないと、デシリアライズ時に Evil オブジェクトが生成され、クラスの読み込み時に静的イニシャライザが実行されます — これは現実的な攻撃になり得ます。

4. シリアライゼーションを安全に保つ上での典型的な誤り

誤り1: フィルタや型検証なしのデシリアライズ。 開発者がストリームから読み出したオブジェクトをすぐに目的の型にキャストしてしまうことがよくあります。これは攻撃への扉を開きます。ObjectInputFilter を使い、instanceof で型を検証してください。

誤り2: 機微情報を transient にしない。 パスワードや鍵を transient として宣言し忘れると、それらがストリームに入り、ファイルとともに漏えいする可能性があります。

誤り3: serialVersionUID を指定しない。 明示的な serialVersionUID がないと、予期しない互換性エラーや、クラス差し替えに伴うリスクが発生する可能性があります。

誤り4: 外部連携にシリアライゼーションを使う。 バイナリシリアライゼーションはアプリ内部(例: キャッシュ)では便利ですが、外部とのやり取りでは危険です。安全なパーサーを用いた JSON/XML/Proto を優先してください。

誤り5: データ完全性の無視。 シリアライズ済みファイルのバイト列を改変しても検知されません。デジタル署名、チェックサム、または暗号化を適用してください。

1
タスク
JAVA 25 SELF, レベル 43, レッスン 4
ロック未解除
魔導士のアーティファクト:宝物のバージョン識別子
魔導士のアーティファクト:宝物のバージョン識別子
1
タスク
JAVA 25 SELF, レベル 43, レッスン 4
ロック未解除
CRMクライアント: 読み込み時のデータ整合性の保証
CRMクライアント: 読み込み時のデータ整合性の保証
1
アンケート/クイズ
シリアライズの設定、レベル 43、レッスン 4
使用不可
シリアライズの設定
シリアライズの設定
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION