安全でないデシリアライゼーション(insecure deserialization・CWE-502)は、受け取ったデータを「オブジェクトに戻す」処理そのものが攻撃の実行になる欠陥です。多くの言語は、メモリ上のオブジェクトをバイト列に変換(シリアライズ)し、後で元のオブジェクトに戻す(デシリアライズ)仕組みを持ちます。この「戻す」処理に信頼できないバイト列を渡すと、戻す最中にクラスの特別なメソッドが呼ばれ、攻撃者が用意した処理の連鎖(ガジェットチェーン)を通じてコード実行まで届きます。

本記事は連載「事故から逆算するWebセキュリティ」の第26回です。セッションやキャッシュ、インポート機能などで「オブジェクトを保存して後で復元する」コードを書く開発者に向けて、何が危険で、どう防ぐかを整理します。セキュリティ全体の地図は「サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説」にまとめています。

RESTORE vs MAP根っこは『何に復元するか』

ネイティブオブジェクトに復元は危険/データ専用形式で明示マッピングなら安全

危ない:オブジェクトに復元

  • 信頼できないバイト列をそのまま復元
  • 復元の最中にマジックメソッドが走る
  • ガジェット連鎖でコード実行に至る
  • 復元前の検証は手遅れ(もう実行済み)

安全:データとして受ける

  • JSON などデータ専用形式で受ける
  • スキーマで検証してからプレーンな構造体へ
  • どのクラスを作るかを入力に決めさせない
  • コードを実行しない形式を選ぶ
出典: OWASP Deserialization / CWE-502 の防御方針(データ専用形式へ・2026-11-06 整理)

30秒でわかる

  • ・何が起きるか:信頼できないバイト列を復元する処理で、復元の最中に任意のコードが実行される(CWE-502)。RCE・改ざんへ
  • ・どこに潜むか:PHP unserialize・Python pickle・Java readObject・Ruby Marshal.load/セッション・キャッシュ・キュー
  • ・なぜ検証では不十分か:復元の最中にマジックメソッドが走るため、「復元してから検証」では手遅れ
  • ・効く設計:外部入力はオブジェクトに復元せず、JSONなどで受けてスキーマ検証し、値だけをプレーンな構造体に詰め替える

関連して、復元したオブジェクトからコマンドを組み立てて実行すればOSコマンドインジェクションに、セッションの格納方式が甘ければセッション管理とCookieの属性の問題に接続します。デシリアライゼーションは、その入口で「データ」を「生きたオブジェクト」に変えてしまうところが起点です。

安全でないデシリアライゼーションはどこで起きるか

影響を受けるのは、「保存したオブジェクトを後で復元する」処理です。言語やフレームワークを問わず、次の経路が候補になります。

WHERE IT HIDES『バイト列を復元する』処理が候補

言語の復元APIと、入口の運び手に潜む

  1. 1.言語ごとの復元API

    PHP unserialize・Python pickle.loads・Java ObjectInputStream.readObject・Ruby Marshal.load/YAML.load・.NET BinaryFormatter

  2. 2.セッション・Cookie

    セッションやCookieにシリアライズしたオブジェクトを格納。署名が無い/鍵が漏れると偽造される

  3. 3.キャッシュ・キュー・RPC

    Memcached/Redis・メッセージキュー・RPC で、オブジェクトをシリアライズして往復させている経路

  4. 4.アップロード・インポート

    設定・状態の復元、インポート機能が、ユーザー由来のシリアライズ済みデータを読む

出典: CWE-502(Deserialization of Untrusted Data)が挙げる典型経路を整理(2026-11-06)

・言語ごとの復元API:PHP unserialize()、Python pickle.loads()、Java ObjectInputStream.readObject()、Ruby Marshal.load / YAML.load、.NET BinaryFormatter
・セッション・Cookie:シリアライズしたオブジェクトをセッションやCookieに格納する。署名が無い、または署名鍵が漏れると偽造できる
・キャッシュ・キュー・RPC:Memcached/Redis・メッセージキュー・RPC で、オブジェクトをシリアライズして往復させている経路
・アップロード・インポート:設定や状態の復元、インポート機能が、ユーザー由来のシリアライズ済みデータを読む

セッションの格納は特に注意が要ります。たとえば Rails は、セッションをシリアライズして署名・暗号化した上でCookieに入れますが、その署名鍵(secret_key_base)が漏れると、攻撃者が任意のオブジェクトを詰めたセッションを正当なものとして偽造でき、復元時のコード実行に直結します。直近でも、Active Storage の脆弱性で secret_key_base などが読まれうる事例(Rails Active Storage の緊急脆弱性)が公表されました。鍵の流出が「ただの情報漏えい」で終わらず、デシリアライゼーション経由のRCEに繋がる理由がここにあります。

なぜ復元前の検証や記号のチェックでは防げないのか

いちばんの誤解は、「復元したオブジェクトの中身を検証すれば安全」という考えです。これは順番の問題で破られます。

・復元中に処理が走る:多くの言語は、復元の最中にクラスの特別なメソッド(PHP の __wakeup/__destruct、Java の readObject、Python pickle の __reduce__)を呼ぶ。検証コードに到達した時点で手遅れ
・ガジェットチェーン:攻撃者は、アプリや依存ライブラリに元からあるクラスの組み合わせ(ガジェット)を連鎖させて目的を達する。単一の「危ないクラス」を禁止するだけでは足りない
・型の取り違え:期待した型と違うオブジェクトを渡され、想定外のメソッドが呼ばれる(オブジェクトインジェクション)
・署名が無ければ改ざん自在:シリアライズ済みデータに署名が無ければ、中身を好きに差し替えられる

要は、守るべきは「復元後の中身」ではなく、「信頼できないバイト列を、そもそもオブジェクトに復元する経路」そのものです。攻撃者が送れる入力と、守りが無いときに起こることを整理すると、問題が「データの中身」ではなく「何に復元するか」にあると分かります。

攻撃者が送れる入力 守りが無いと起こること
細工したシリアライズ済みバイト列 復元の最中にマジックメソッドが実行される
既存クラスを組み合わせた連鎖 ガジェットチェーンでコード実行に至る
期待と違う型のオブジェクト 想定外のメソッドが呼ばれる(オブジェクトインジェクション)
署名の無いセッション/鍵が漏れた署名 セッションを偽造し任意オブジェクトを復元させる

安全でないデシリアライゼーション対策——データ専用形式で受けて詰め替える

効くのは、記号やクラスを禁止することではなく、信頼境界を越えるデータをネイティブのオブジェクト復元に渡さず、コードを実行しないデータ形式で受けてから、自分で詰め替えることです。OWASP の Deserialization Cheat Sheet も、信頼できないデータのネイティブなデシリアライズを避け、どうしても必要なら完全性チェック(署名)とクラスの制限を併用することを勧めています。順序はこうです。

SAFE DESIGN設計の順序

ネイティブ復元しない → データ形式+スキーマ → 署名/許可リスト → 復元前検証に頼らない

  1. 1

    信頼できないデータをネイティブ復元しない

    pickle・Marshal・BinaryFormatter・unserialize を外部入力に使わない。まずこれが原則

  2. 2

    データ専用形式で受けて明示マッピング

    JSON などコードを実行しない形式で受け、スキーマ検証してからプレーンな構造体・DTO に自分で詰め替える

  3. 3

    やむを得ないなら署名と許可リスト

    HMAC 署名で自分が作ったデータだけ復元を通す。安全ローダ(yaml.safe_load・unserialize allowed_classes)・Java の ObjectInputFilter でクラスを許可リスト化

  4. 4

    『復元前の検証』に頼らない

    復元の最中に処理が走るため、復元してから検証では手遅れ。検証は形式・署名の段階で終わらせる

出典: OWASP Deserialization Cheat Sheet / 各言語公式の方針(2026-11-06)。ガジェット連鎖の具体手口は非掲載

ポイントは、「何を作るか」を入力に決めさせないことです。JSONなどで受け、スキーマで形を検証し、生成するクラスはコード側が固定して、値だけをプレーンな構造体やDTOに詰めます。これなら、復元の最中に任意のクラスが生成される余地がありません。守り方ごとに、何を防ぎ、何を防がないかを並べます。

守り方 何を防ぐか 何を防がないか
危ないクラスのブラックリスト 既知の単一ガジェット 未知の連鎖・ライブラリ更新で増える経路
データ専用形式(JSON)+スキーマ+明示マッピング 復元時のコード実行(オブジェクトを作らない) スキーマの緩さ・詰め替え後のロジック不備
署名(HMAC)で自分のデータだけ復元 外部からの改ざん・偽造 署名鍵の流出(鍵管理が前提)
クラス許可リスト(ObjectInputFilter 等) 許可外クラスの生成 許可したクラス同士のガジェット

いちばん固いのは、信頼境界を越えるデータにネイティブのシリアライズを使わない設計です。どうしても必要なときだけ、署名とクラス許可リストを重ねます。

言語ごとの正しい書き方の要点

どの言語も「危険な復元API」と「安全な代替」が分かれています。公式ドキュメントの対応づけは次のとおりです。

言語 避ける:危険な復元 安全:データ形式/安全ローダ
Python pickle.loads() / yaml.load()(全ロード) json / yaml.safe_load()
PHP unserialize($untrusted) JSON / unserialize($s, ["allowed_classes" => false])
Java ObjectInputStream.readObject() データ形式+スキーマ/ObjectInputFilter でクラス許可リスト
Ruby Marshal.load / YAML.load JSON / YAML.safe_load(permitted_classes を明示)
.NET BinaryFormatter(非推奨・既定で無効化) System.Text.Json
Node.js node-serialize 等の eval 系 JSON.parse +スキーマ検証

注意点が二つあります。一つ目は、「安全ローダ」も万能ではないことです。たとえば YAML の安全ロードは任意オブジェクト生成を止めますが、許可するクラスを自分で広げすぎれば意味がありません。二つ目は、JSONでも自作の「型名→クラス生成」は危険だということです。入力のフィールドを見てクラスを動的に選ぶ実装は、形式をJSONに変えてもデシリアライゼーションの危険を再発させます。生成するクラスはコード側で固定します。

自分のコードを確認する

ここからは読み取りだけの確認コマンドです。いずれも判定ではなく構成の確認で、ヒットの有無だけでは安全とは言えません。該当箇所を目で追う起点として使います。. は自分のリポジトリのルートに読み替えてください。

まず、危険な復元APIを呼んでいそうな箇所を洗い出します。

grep -rnE "(unserialize|pickle\.loads|readObject|Marshal\.load|BinaryFormatter|yaml\.load\(|node-serialize)" . --include="*.php" --include="*.py" --include="*.java" --include="*.rb" --include="*.cs" --include="*.js" --include="*.ts"

次に、その入力が信頼境界を越えて来ていないか(リクエスト・Cookie・アップロード・キュー由来か)を、ヒット箇所ごとに追います。安全ローダや許可リストが後ろにあるかも確認します。

grep -rnE "(safe_load|allowed_classes|permitted_classes|ObjectInputFilter)" . --include="*.php" --include="*.py" --include="*.java" --include="*.rb"

いずれも、小さなダミーのプロジェクトで陽性(信頼できない入力を危険APIに渡す側)と陰性(データ形式+スキーマに直した側)の両方を走らせ、拾うべきものを拾い、無いときに何も出ないことを確かめてから載せています。ヒットした箇所が本当に危険かは、その入力の出所と、復元前に署名・形式の検証があるかを、コードを読んで確かめます。ガジェットチェーンの具体的な作り方や攻撃ペイロードは、悪用を助けるため本記事では扱いません。

参照ソース