セキュリティ

安全でないデシリアライゼーションとは?RCEに至る仕組みと言語別の防御実装を解説

受け取ったバイト列をそのままオブジェクトへ戻す処理が、任意コード実行の入口になります。安全でないデシリアライゼーション(Insecure Deserialization)はCWE-502として整理された欠陥で、JavaのObjectInputStream、PHPのunserialize、Pythonのpickleが代表的な発生源です。本記事では、ガジェットチェーンでRCEに到達する流れ、言語ごとに用意された防御機構と適用できるバージョン、JSONやYAMLへ移しても残る経路、稼働中のシステムを止めずに直す段階手順までを実装目線で整理します。読み終えた時点で、自社コードのどこから潰すかが決まります。

まとめ:安全でないデシリアライゼーション対策の結論と優先順位

結論を先に置きます。この欠陥を止める本命は、信頼できない入力をオブジェクト復元APIへ渡さない設計に変えることです。ObjectInputStreamunserializepickleが読み込むのは「データ」ではなく「オブジェクトを再構築する手順」でした。復元の途中でコンストラクタや特殊メソッドが動くため、復元後に値を検査しても遅い。入力を文字列として検査する発想では防げません。

置換が終わるまでの緩和策は許可リスト方式のフィルタです。JavaはJDK 9のJEP 290で入ったObjectInputFilter、JDK 17のJEP 415で追加されたフィルタファクトリ。PHPはunserializeallowed_classes、PythonはUnpickler.find_classのオーバーライド。いずれも復元してよいクラスを列挙する方式で、拒否リストの更新に頼りません。

逆に、単独では対策にならない手が3つあります。署名の付与だけで済ませること、WAFのシグネチャに任せること、既知ガジェットを含むライブラリを1本外して終わりにすること。着手順は、危険APIの呼び出し箇所を棚卸しし、信頼境界をまたぐ入口からJSONなどのデータ専用フォーマットへ置換し、置換できない箇所にフィルタを当てる、の3段です。

安全でないデシリアライゼーションの定義とRCEへ至る攻撃連鎖の全体像

まず言葉の範囲を揃えます。攻撃が成立する条件と、成立したときに何が起きるかを分けて把握すると、後段の対策選定が速くなります。

シリアライズとデシリアライズの役割分担とCWE-502が指す欠陥

シリアライズは、メモリ上のオブジェクトをファイルやネットワークで運べるバイト列・文字列へ変換する処理です。デシリアライズはその逆で、受け取った表現からオブジェクトを組み立て直します。問題は、多くの言語の標準形式が「値」だけでなく「クラス名」を持ち、復元側がその名前を見てクラスをロードする点にあります。

CWE-502はこの欠陥を「信頼できないデータのデシリアライゼーション」と定義しています。OWASP Top 10では2017年版でA08として独立項目でしたが、2021年版でソフトウェアとデータの整合性不備へ統合され、2025年版でもA08「整合性不備」の枠内に収まりました。項目名が消えたことと、脅威が消えたことは別です。カテゴリの再編経緯はOWASP Top 10 2025年版の10項目と2021年版からの変更点で整理しています。

マジックメソッドとガジェットチェーンでRCEに到達するまでの流れ

攻撃は3段構えです。第1段は、復元時に自動で呼ばれるメソッド。PHPの__wakeup__destruct、JavaのreadObject、Pythonの__reduce__がこれにあたります。第2段は、その先で呼ばれる危険な処理(sink)で、リフレクション経由のメソッド呼び出しやプロセス起動が該当します。第3段が、両者をつなぐ既存クラスの連鎖です。

この連鎖をガジェットチェーンと呼びます。攻撃者は新しいコードを送り込むのではなく、アプリのクラスパスに元からあるクラスを部品として並べ替えるだけ。Java向けにはysoserialというペイロード生成ツールが公開されており、Apache Commons Collectionsを使った連鎖はOracle WebLogicのCVE-2015-4852など実被害を出しました。攻撃コードそのものの分類はexploitの仕組みと種類で解説しています。

権限昇格・DoS・状態改ざんを含む被害範囲と深刻度の切り分け

被害はRCEだけではありません。使えるガジェットが見つからない環境でも、シリアライズされたセッションオブジェクトのisAdmin相当のフィールドを書き換えれば権限昇格が成立します。深くネストした構造を送り込めばスタックを消費させるサービス停止も起こせる。PHPが7.4でmax_depth(既定4096)を追加したのは、この種の入力への対処でした。

深刻度の切り分けは実務的に2軸で足ります。復元されたオブジェクトが認可判断に使われるか、そしてクラスパスに既知のガジェットを含むライブラリがあるか。前者が真なら、RCEに至らなくても権限昇格として扱う。両方が真なら、緩和策を挟む前に該当エンドポイントを止める判断が要ります。

言語別の危険なAPIと現行ランタイムが備える防御機構の対応表

言語ごとに、危険なAPIと標準で用意された歯止めは大きく違います。手元のスタックがどの段階にあるかを先に確定させてください。

Javaのシリアライゼーションフィルタと版ごとの適用範囲の違い

JavaはJDK 9で入ったJEP 290により、ObjectInputFilterでクラス名・配列長・深さ・参照数に上限を課せます。JVM全体へ効かせるjdk.serialFilterと、ストリーム単位で設定する方式の2通り。ただしJVM全体に1本のフィルタを置く運用は、ライブラリが何層も重なるアプリだと広すぎるか狭すぎるかのどちらかに倒れます。

JDK 17のJEP 415が追加したのがフィルタファクトリです。ObjectInputStreamが生成されるたびに呼ばれる工場を1つ登録し、呼び出し文脈ごとに別のフィルタを返す。既存の復元箇所へ手を入れずに、文脈別のポリシーを合成できます。判断としては、JDK 17以降ならフィルタファクトリ、JDK 11に留まる資産はストリーム単位のフィルタで個別に塞ぐ、という切り分けが現実的です。

PHPのunserializeとallowed_classes指定の限界

PHPのunserializeは第2引数のオプション配列でallowed_classesを取ります。PHP 7.0で追加された指定で、falseならすべてのオブジェクトが__PHP_Incomplete_Classになり、クラス名の配列を渡せばその範囲だけ復元されます。既定値はtrue、つまり無制限です。

ここで注意したいのが公式マニュアルの警告文です。allowed_classesの値にかかわらず、信頼できないユーザー入力をunserializeへ渡してはならない、と明記されています。オブジェクトの生成とオートロードを通じてコードが読み込まれ実行される余地が残るためです。つまりallowed_classesは移行期間の緩和策であって、恒久対策ではありません。恒久対策はjson_decodeへの置換です。

Python・Ruby・.NETで危険なAPIと代替手段の対応表

主要ランタイムの現状を1枚に並べます。既定の防御機構が「なし」の欄は、言語側が守ってくれないという意味で、実装側で入口を塞ぐ以外に手がありません。

言語・ランタイム 危険なAPI 既定の防御機構 実務での代替
Java(JDK 17以降) ObjectInputStream フィルタファクトリ JSON+許可リスト
PHP 8系 unserialize allowed_classes指定 json_decode
Python 3系 pickle.loads なし(公式が警告) json+HMAC署名
Ruby 3系 Marshal.load なし JSONへ置換
.NET 9以降 BinaryFormatter 実装を削除済み System.Text.Json

Pythonの公式ドキュメントは「pickleは安全ではない。信頼するデータだけをunpickleせよ」と明言し、改ざん検知が要るならhmacでの署名を検討するよう書いています。制限付きで使う場合はUnpickler.find_classをオーバーライドし、許可するモジュールとクラス名だけを返す実装例も掲載されています。

JSON・YAML利用時にも残るデシリアライゼーション経路の落とし穴

フォーマットをJSONやYAMLへ変えても、ライブラリ設定次第でクラス名から任意の型を組み立てる経路が復活します。移行後に見落とされやすい2つの型を押さえます。

Jacksonのポリモーフィック型指定で生じる任意クラス復元

Javaで広く使われるjackson-databindには、JSONの中に型名を埋めて多態を復元する機能があります。古いenableDefaultTyping()を有効にすると、入力側が指定した型名でクラスをロードするため、JDBCデータソース系のクラスを踏み台にした攻撃が成立しました。CVE-2017-7525を皮切りに、同型の報告が長く続いています。

2.10で導入されたactivateDefaultTyping(PolymorphicTypeValidator, ...)が現在の正解です。enableDefaultTyping()は非推奨となり、許可する型を開発者が明示する形へ変わりました。拒否リストを更新し続ける運用から、許可リストを宣言する運用への転換です。依存ライブラリの版と既知脆弱性の突き合わせ自体はSCA(ソフトウェア構成分析)の仕組みとSAST・SBOMとの違いで扱っています。

SnakeYAMLのSafeConstructor既定化と旧版に残るリスク

YAMLも同じ構図でした。SnakeYAMLは1.x系の既定で汎用オブジェクトへの復元を許し、クラスパス上の任意クラスを組み立てられたためCVE-2022-1471として報告されています。設定ファイルの読み込みに使っていたライブラリが、そのまま入力経路になっていた形です。

2.0ではConstructorSafeConstructorを継承する形に変わり、既定でプリミティブと基本コレクションだけを扱います。1.x系に留まる場合は、SafeConstructorを明示的に渡す実装へ直すのが緩和策。判断は単純で、外部から来るYAMLを読む箇所が1つでもあるなら、版上げを他の改修より前に置いてください。

既存システムでの段階的な移行手順と移行できない場合の緩和策の設計

稼働中のシステムでは、危険APIを一斉に消す改修は現実的ではありません。入口の性質で優先度をつけ、順に信頼境界の外側から塞ぎます。

入口の棚卸しからフォーマット置換までの4段階の移行手順と検証

手順は4段階に分けると管理できます。各段階の完了条件を先に決めておくと、途中で止まっても現在地が分かります。

  1. 危険APIの呼び出し箇所を全文検索で列挙し、ファイルと行番号の一覧を作る(Javaはストリーム生成箇所、PHPは復元関数、Pythonはロード関数)
  2. 各箇所の入力元を、外部リクエスト・内部キュー・自プロセス生成の3種に分類し、外部リクエスト由来を最優先に置く
  3. 外部由来の箇所からJSONなどデータ専用フォーマットへ置換し、送信側と受信側を同一リリースで切り替える
  4. 置換後、旧形式のペイロードを送って復元が拒否されることを回帰テストで確認し、拒否ログが出ることまで見る

4段目を省くと、旧形式を受け付けるフォールバックが残ったままリリースされる事故が起きます。移行の完了条件は「新形式が動くこと」ではなく「旧形式が拒否されること」です。

置換できない箇所へ適用する許可リスト方式フィルタの設計と基準

外部ライブラリの内部で復元が起きる場合など、置換できない箇所は残ります。ここに当てるフィルタの設計基準は4つ。復元を許すクラスを列挙すること、配列長・オブジェクト参照数・ネスト深さに上限を置くこと、拒否をアプリのログとして残すこと、そして入口ごとに別のポリシーを持たせることです。

上限値は本番のペイロードを実測してから決めます。ネスト深さは実測値の2倍程度、参照数は余裕を持たせて丸めるのが扱いやすい。文字列の中身を検査するルールは書かない方がよく、その理由は復元前に型が確定していないためです。入力の値そのものを検査する対策の適用範囲は入力バリデーションで防げる攻撃の範囲と許可リスト設計で切り分けています。

HMAC署名を併用する場合の鍵管理と検証順序に関する判断基準

クッキーや隠しフィールドに状態を持たせる設計では、HMAC署名の併用が選ばれます。ここで守る順序は1つだけ。署名を検証してから復元する、です。復元してから署名を見る実装は、検証前にコードが動くため意味を成しません。

鍵はソースへ埋めずシークレット管理サービスから読み、比較は定数時間比較の関数を使います。鍵のローテーションは旧鍵での検証を一定期間だけ許す二重受け入れで実施する。ただし署名で守れるのは改ざんの検知までで、復元処理そのものの安全性は担保されません。署名は許可リストの代わりにはなりません。

対策として採用してはいけない設計と実務で繰り返される失敗パターン

ここからは判断を言い切ります。現場で「対策済み」と報告されがちで、実際には塞げていない3つの型です。

署名付与のみで対策が完了したと見なす設計を採用しない理由と条件

署名だけを対策とする設計は採用しません。理由は、署名鍵が漏れた瞬間に防御がゼロへ戻ること、そして署名を検証しても復元先クラスが無制限のままである点にあります。鍵の漏えい経路は、設定ファイル・CIのログ・古いリポジトリ履歴です。復元先が無制限なら、鍵を得た攻撃者はそのままRCEへ進めます。

署名のみで許容できる場面は限定的です。自プロセスが生成して自プロセスだけが復元する短命キャッシュで、復元先クラスが固定され、鍵がシークレット管理下にある場合。この3条件が同時に成り立たないなら、署名に加えて許可リストのフィルタを必ず置いてください。

WAFのシグネチャ頼みとガジェット削除だけで止める判断の限界

WAFのシグネチャに任せる判断も採用できません。シリアライズ済みデータはBase64やgzip、多重エンコードで容易に見た目を変えられ、パターン照合は回避されます。WAFは既知ペイロードの初期観測を減らす層であって、脆弱な復元処理を安全にする層ではない。仮想パッチとして時間を稼ぐ用途に限定してください。

もう1つが、既知ガジェットを含むライブラリを1本外して完了とする判断です。ysoserialが公開しているガジェットチェーンは数十種類あり、依存が入れ替われば別の連鎖が成立します。ライブラリを外す作業は「今日のペイロードを無効化する」だけで、復元処理を許可リストで囲う作業の代わりにはなりません。

サポート外パッケージでBinaryFormatterを延命する選択の見送り基準

.NETでは廃止の道筋が明示されています。BinaryFormatterは.NET 5でSYSLIB0011として非推奨になり、.NET 8ではほとんどのプロジェクト種別で実行時に例外を投げるようになりました。.NET 9からはランタイム内の実装自体が削除され、APIは残るものの常に例外を返します(.NET Frameworkは対象外)。

その状態でも動かす手段として、サポート外のNuGetパッケージを参照する選択肢が案内されています。この選択は見送りが妥当です。基準は3つ。セキュリティ修正が提供されないこと、次のランタイム更新で再び動かなくなる可能性が残ること、そして移行先のSystem.Text.Jsonが標準で用意されていること。延命に充てる工数は、シリアライズ対象のDTO化に回した方が回収できます。

SAST・SCA・DASTによる検出の役割分担と診断の使い分け基準

この欠陥は、単一のツールでは見つけ切れません。何がどこまで見えるかを把握し、残った範囲を人が見る設計にします。

SAST・SCA・DASTが検知できる範囲と取りこぼしの境界

SASTは危険APIの呼び出し箇所を機械的に列挙できます。棚卸しの初手として有効ですが、その引数が外部由来かどうかの追跡はフレームワーク境界で切れやすく、誤検知と見逃しが同時に出ます。SCAは既知ガジェットを含むライブラリ版の検出に強く、jackson-databindやSnakeYAMLの版判定はこちらが速い。

DASTは苦手です。復元に成功しても応答が変わらないブラインド型が多く、外部通信を観測する仕掛けを併用しないと成立を確認できません。境界を整理すると、呼び出し箇所はSAST、ガジェットの在庫はSCA、実際に到達可能かは手動検証、という分担になります。

実行時防御と外部の脆弱性診断を組み合わせる際の判断基準と分岐点

実行時側の防御は、JVMのフィルタのように言語標準の機構を先に使い、それで覆えない範囲へ実行時保護の製品を検討する順序が無駄になりません。逆順で製品を先に入れると、標準機構で塞げたはずの箇所まで製品依存になります。

分岐点は、危険APIの呼び出しが自社コードの外にあるかどうかです。自社コード内なら改修で閉じられます。フレームワークやミドルウェアの内部にあり、版上げでも消えない場合は、到達可能性の確認を外部の手動検証に回すのが早い。判断材料が足りないときは脆弱性診断・セキュリティ診断で対象エンドポイントを絞った検証から始められます。

よくある質問:安全でないデシリアライゼーションの実装判断と運用

実装と運用の現場で判断が割れやすい5点に答えます。

安全でないデシリアライゼーションとインジェクションは何が違いますか?

インジェクションは、文字列として渡した入力が命令として解釈される欠陥です。SQLやOSコマンドが典型で、プレースホルダやエスケープという「値と命令を分ける」対策が効きます。デシリアライゼーションは、入力が最初からオブジェクトの構築手順として解釈される点が違います。値と命令を分ける前提が成り立たないため、エスケープでは止まりません。復元してよい型を列挙するか、そもそも復元しない形式へ変えるかの二択になります。

JSONへ移せばデシリアライゼーションの脆弱性は起きなくなりますか?

起きなくなるとは言えません。JSON自体はクラス名を持たない素直なデータ形式ですが、ライブラリ側で多態復元を有効にすると型名から任意クラスを組み立てる経路が復活します。jackson-databindのenableDefaultTyping()がその例です。移行時は、フォーマットを変えるだけでなく、型名を入力から受け取る設定を無効にし、必要なら許可リストを宣言するactivateDefaultTypingへ書き換えてください。

Javaのシリアライゼーションフィルタを設定すれば対策として十分ですか?

単独では不十分です。フィルタは強力ですが、JVM全体に1本だけ置く運用では、ライブラリが多層に重なるアプリで範囲が広すぎるか狭すぎるかに倒れます。JDK 17以降はフィルタファクトリで文脈ごとにポリシーを分け、あわせて配列長・深さ・参照数の上限を設定してください。加えて、外部から来る入力の復元をJSONへ置き換える作業は続けます。フィルタは移行を安全に進めるための足場という位置づけです。

攻撃を受けたかどうかはアクセスログから判別できますか?

標準的なアクセスログだけでは困難です。ペイロードはPOSTボディやクッキーに入り、Base64などで符号化されているため、URLとステータスコードの記録には現れません。判別には、復元処理の拒否イベントをアプリ側でログに出す実装が要ります。フィルタで拒否したクラス名と入口の識別子を記録しておけば、試行の有無が後から追えます。導入時にこのログ設計を同時に入れてください。

既存システムの危険な箇所を洗い出す最短の手順はありますか?

全文検索から始めるのが最短です。Javaはストリーム生成箇所、PHPは復元関数、Pythonはロード関数、.NETは廃止済みフォーマッタを対象に、ファイルと行番号の一覧を作ります。次に依存ライブラリの版を確認し、jackson-databindやSnakeYAMLが古い版で止まっていないかを見る。最後に、列挙した各箇所の入力元を外部・内部・自プロセスの3種に分類すれば、着手順が決まります。

関連記事

資料請求

RELATED POSTS 関連記事