受け取ったバイト列を元のオブジェクトに戻す処理が危険になるのは、中身が変なデータだからではありません。形式によっては、「どの型のオブジェクトを組み立てるか」まで外部データが決められるからです。
なぜ「入力検証」では間に合わないのか
純粋なデータ形式(JSON / XML)
外部データが決めるのは値だけ → 受け取ってから、自分の型に詰め替える → 検証が効く
オブジェクトを復元する形式
外部データが決めるのは値と「型」の両方 → 復元の過程で選ばれた型のコードが動く → 値を検証する場所に到達しない
多くの入力検証は「値が正しいか」を見ます。しかしオブジェクトを復元する形式では、復元の過程そのもの(どのクラスを実体化し、その過程でどのコードが動くか)が外部データに左右されます。あなたの検証コードが呼ばれる前に、すでに何かが実行され終えていることがあり得るのです。
これは前に扱った プロトタイプ汚染 と同じ構図です。外部データが、値ではなく「構造」を決めてしまう。名前も言語も違いますが、防ぎ方の発想は共通しています。
何が起きるのか(OWASPが挙げる3つ)
- サービス停止(DoS)
- 復元処理そのものを重くする・壊すことでアプリを止める
- アクセス制御の迂回
- 権限やロールを表すオブジェクトを、外部から都合よく組み立てられる
- リモートコード実行(RCE)
- 復元の過程で呼ばれる処理を連鎖させ、任意のコード実行に至る。最も深刻な結果
「うちのフレームワークは大丈夫」とは限りません
これは古い言語だけの話ではありません。当サイトがNVDのデータでNext.jsの脆弱性を数えたとき、CWEの上位にデシリアライズ(CWE-502)が入っていました。Server Actions のようにリクエストのボディをデシリアライズする経路を、現代のフレームワークは標準で持っているからです(Next.jsとReactは脆弱性が多いのか)。「自分では書いていない」=「存在しない」ではありません。
対策の軸は2つ
① 型を選ばせない(第一選択)
OWASPは次のように書いています。「JSONやXMLのような純粋なデータ形式に切り替えることで、独自のデシリアライズ処理が悪意ある目的に転用される可能性を減らせる」。
外部から来るものは値だけを運ぶ形式で受け取り、自分のコードで自分の型に詰め替える。これで、攻撃者が「何を作るか」を選べなくなります。多くの現場で、これだけで問題が解決します。
② 署名して、無いものは復元しない
独自形式をどうしても使う場合、OWASPは「シリアライズの過程で署名し、認証された署名を持たないメッセージはデシリアライズしないという選択ができる」としています。
ポイントは復元する前に検証すること。復元してから中身を確かめるのでは、上で書いた理由により手遅れになり得ます。順序がすべてです。
設定では安全にできない仕組みがある
OWASPは .NET の BinaryFormatter について、「この型は危険であり、安全にすることはできない」と明言しています。
この一文は、実務上とても重要です。世の中には「正しく使えば大丈夫」が成り立たない道具が実在します。オプションを固める、検証を足す、許可リストを書く……そのどれでも安全にならない。唯一の対策は使わないことです。
だから点検では、まず「これは設定で守れるものか、そもそも守れないものか」を分けてください。後者に時間を使うのは、対策ではなく先延ばしです。
言語別に、OWASPが名指ししているもの
- PHP
unserialize()の使用を避け、JSONを優先する- Python
pickle/c_pickle、PyYAML のload、jsonpickleは危険として挙げられている- Java
ObjectInputStream#resolveClass()をオーバーライドして、復元できるクラスを制限する- .NET
BinaryFormatterは危険であり、安全にすることはできない(使用しない)
共通しているのは、言語が用意している「便利な永続化機構」ほど、外部データに向けてはいけないという点です。それらは信頼できる相手との間でオブジェクトを丸ごと運ぶために作られており、敵対的な入力を想定していません。
安全な書き方と危険な書き方(言語別の対応表)
各言語の公式ドキュメントが、外部データに使わないよう警告しているものと、その代わりに勧めているものを並べました。左の列が自分のコードに出てきたら、データがどこから来ているかを確認してください。
| 言語 | 外部データに使わない | 代わりに使う | 公式ドキュメントの注意 |
|---|---|---|---|
| Python | pickle.load / pickle.loads、marshal、yaml.load(Loader=yaml.Loader)・yaml.unsafe_load、jsonpickle.decode | json.loads、yaml.safe_load | pickleは「安全ではない」。改ざん検知が必要なら hmac で署名 |
| PHP | unserialize()(allowed_classes を指定しても不可) | json_decode() / json_encode() | allowed_classes の値にかかわらず、信頼できない入力を渡さない |
| Ruby | Marshal.load | JSON など、基本の型だけを読む形式 | 信頼できない元から読むとリモートコード実行に至り得る |
| Java | 外部データへの ObjectInputStream#readObject、XMLDecoder、XStream#fromXML | JSONを自分のクラス(DTO)に詰め替える | どうしても必要なら ObjectInputFilter で許可するクラスを限定 |
| .NET | BinaryFormatter、SoapFormatter、NetDataContractSerializer、LosFormatter、ObjectStateFormatter、Json.NETの TypeNameHandling(None 以外) | System.Text.Json、XmlSerializer、DataContractSerializer | BinaryFormatter は .NET 9 以降、使うと例外になる |
違いを1か所にまとめると、次のようになります。危険な書き方は「受け取ったものをそのままオブジェクトに戻す」、安全な書き方は「値として読み、必要な項目だけを自分の型に移す」です。
# 避ける:受け取ったバイト列をそのままオブジェクトに戻す
order = pickle.loads(request_body)
# 使う:値として読み、必要な項目だけを自分の型に移す
data = json.loads(request_body)
order = Order(id=int(data["id"]), quantity=int(data["quantity"]))安全な側では、どのクラスを作るかを決めているのは自分のコード(Order)です。受け取ったデータが決められるのは id と quantity の値だけで、その値にはふつうの入力検証が効きます。
自分のコードで確認すること
外部データが最初に触れる場所を、全部書き出す
リクエストボディ、クッキー、セッション、キュー、ファイルアップロード、Webhook、キャッシュ——「どこかで保存して、後で戻している」場所をすべて洗い出します。セッションやキャッシュを見落としやすい(自分が書いたデータのつもりでも、経路によっては外部が触れます)。
そのうち「型まで復元する形式」を使っている箇所を特定する
上の言語別リストにある関数・クラスを検索します。ここが最優先で直す対象です。設定では守れない機構(BinaryFormatter等)を使っている場合、優先度は最上位です。移行以外の解決がありません。
純粋なデータ形式へ移す
JSONやXMLで受け取り、自分のコードで自分の型に詰め替える。詰め替えの際にスキーマ検証を掛ければ、想定外のキーも同時に拒否できます(プロトタイプ汚染の対策と同じ手当てです)。
移せないものは署名で守る
形式を変えられない場合は、シリアライズ時に署名し、復元の前に署名を検証します。署名が無ければ復元しない。これを既定にしてください。鍵の扱いは .envとAPIキーの基礎 のとおり、コードと同じ場所に置かないこと。
依存の中にある復元処理も数える
自分が書いていなくても、依存が復元していることがあります。CWE-502として公開されるCVEは継続的に出ているので、機械監視に載せてください(osv-scannerの導入と使い方 / CVE対応の実務手順)。
自分の環境で確認する方法
上の手順のうち「探す」部分を、具体的なコマンドと設定に落とします。どれも自分のリポジトリと自分のサーバーを読むだけの確認です。
ソースコードを文字列で検索する
まず候補を全部出します。ripgrep(rg)なら次のように検索できます(grep -rnE でも同じ正規表現が使えます)。
# Python
rg -n "pickle\.loads?\(|marshal\.loads?\(|yaml\.load\(|yaml\.unsafe_load|jsonpickle\.decode"
# PHP
rg -n "unserialize\("
# Ruby
rg -n "Marshal\.load"
# Java
rg -n "ObjectInputStream|readObject\(|XMLDecoder|fromXML\("
# .NET
rg -n "BinaryFormatter|SoapFormatter|NetDataContractSerializer|LosFormatter|ObjectStateFormatter|TypeNameHandling"見つかった行がすべて危険なわけではありません。それぞれについて「渡しているデータは、どこから来たか」を1行ずつ書き出し、リクエスト・クッキー・アップロード・共有ストレージなど外から触れる経路につながるものを最優先にします。
警告を消している箇所を探す
.NETでは BinaryFormatter の使用に SYSLIB0011 という警告やエラーが出ます。これを抑止している設定は、危険な呼び出しが残っている目印です。
rg -n "SYSLIB0011|CA23[0-3][0-9]" --glob "*.cs" --glob "*.csproj" --glob ".editorconfig" --glob "*.props"#pragma warning disable SYSLIB0011 や <NoWarn> に含まれていたら、その理由と移行予定を確認してください。
静的解析のルールを有効にする
Pythonでは Bandit の B301(pickle など)、B302(marshal)、B506(yaml.load)が該当します。bandit -r . -t B301,B302,B506 で、この3つだけを確認できます。.NETでは、コード分析ルール CA2300 番台(例:Json.NETの TypeNameHandling を検出する CA2326)が該当します。CA2326 は .NET 10 でも既定では無効なので、.editorconfig で dotnet_diagnostic.CA2326.severity = warning のように有効にします。CIで毎回走らせれば、新しく入った呼び出しも止められます。
Javaは実行時のフィルタ設定を確認する
Javaには、復元を許すクラスを実行時に絞る仕組み(シリアライズフィルタ)があります。JDK 9 で入り(JEP 290)、Java 17 で用途ごとに切り替えられるようになりました(JEP 415)。動いているプロセスの設定は jcmd <PID> VM.system_properties の出力で jdk.serialFilter を探すと確認できます。セキュリティプロパティとして java.security ファイル側に書かれている場合もあるので、両方を見ます。どちらにも無く、コードの中で ObjectInputFilter.Config.setSerialFilter も呼んでいなければ、プロセス全体のフィルタは設定されていません。
依存パッケージの既知の脆弱性を調べる
自分のコードに呼び出しが無くても、依存が復元処理を持っていることがあります。osv-scanner などで依存の既知脆弱性を一覧し、種別が CWE-502(信頼できないデータのデシリアライズ)のものを優先して更新します。
よくある間違いと直し方
| よくある間違い | なぜ問題か | 直し方 |
|---|---|---|
| 「社内のデータだから」「自分が保存したファイルだから」と信頼する | Microsoftは、保存ファイルの共有やクラウド同期、社内端末の乗っ取りなどで、開発者が気づかないうちにデータが信頼の境界を越える例を挙げている | 外から触れる経路が1つでもあれば外部データとして扱う。署名の無いものは復元しない |
| 危険なクラスを拒否リストに足していく | 拒否リストに無い、まだ知られていない組み合わせには効かない。PHPは allowed_classes を指定しても信頼できない入力を渡すなと書いている | 形式をJSONなどに変える。やむを得ないときだけ、許可するクラスを列挙する方式にする |
| 復元してから中身を検証する | 復元の途中で処理が動くため、検証の時点では遅い | 署名の検証を復元より前に置く |
| 警告を抑止して「対応済み」にする | SYSLIB0011 や CA2326 を消しても、危険な呼び出しは残る | 抑止箇所を検索し、移行の期限を決める |
| 「JSONだから安全」と思い込む | Json.NETの TypeNameHandling のように、JSONの中に型名を書かせる設定を有効にすると、型を選ばせる形式と同じになる | TypeNameHandling は None(既定値)のままにする |
PyYAMLで yaml.load を使う | yaml.load は pickle と同じくらい強力で、任意のPython関数を呼べるとPyYAMLの文書に書かれている | yaml.safe_load に置き換える |
実際の例:拡張機能の中にあった復元処理
当サイトの速報 CVE-2026-45247 は、Magento 2 用の拡張機能「Mirasvit Full Page Cache Warmer」1.11.12 未満にあったPHPオブジェクトインジェクション(CWE-502)です。認証なしでリモートコード実行に至り、CVSSは9.3、CISAの「悪用が確認された脆弱性」のカタログ(KEV)にも載りました。
この例から分かるのは、問題の復元処理がサイト運営者の書いたコードではなく、導入した拡張機能の中にあったことです。上の「依存パッケージの既知の脆弱性を調べる」が必要な理由はここにあります。対応は拡張機能の更新で、自分のコードの検索だけでは見つけられません。
当サイトの視点:入力の「値」ではなく「権限」を見る
入力検証というと、多くの人が値の妥当性(長さ、形式、範囲)を思い浮かべます。しかしこの記事の脅威は、そこに到達する前に成立します。当サイトの見方は次のとおりです。入力について本当に問うべきは「この値は正しいか」ではなく「この入力に、どこまで決めさせているか」。
値だけを決めさせているのか。構造も決めさせているのか(プロトタイプ汚染)。型も決めさせているのか(本記事)。置き場所まで決めさせているのか(ファイルアップロードの脆弱性)。外部に渡している決定権を数えると、危険な箇所は自然に見えてきます。
出典(一次情報)
- OWASP Cheat Sheet Series「Deserialization Cheat Sheet」 — cheatsheetseries.owasp.org(影響の3類型、純粋なデータ形式への切り替え、署名による整合性検証、言語別の注意点、
BinaryFormatterについての記述は、いずれもこの文書にもとづきます。2026-09-05に確認) - Python公式ドキュメント「pickle」 — docs.python.org(「安全ではない」という警告、
hmacによる署名とJSONの推奨) - PyYAML Documentation — pyyaml.org(
yaml.loadとyaml.safe_loadの違い) - PHPマニュアル「unserialize」 — php.net(
allowed_classesにかかわらず信頼できない入力を渡さない、JSONの推奨) - Ruby公式ドキュメント「Marshal」 — docs.ruby-lang.org
- Microsoft Learn「BinaryFormatter と関連する型の使用における逆シリアル化のリスク」 — learn.microsoft.com(同様に危険な型、推奨される代替、.NET 9 以降の挙動、信頼の境界を越える例)
- Microsoft Learn「SYSLIB0011」「CA2326」 — SYSLIB0011 / CA2326
- OpenJDK「JEP 290: Filter Incoming Serialization Data」「JEP 415: Context-Specific Deserialization Filters」 — JEP 290 / JEP 415
- Bandit ドキュメント(B301・B302・B506) — bandit.readthedocs.io
- 言語別の対応表、検索の手順、よくある間違いは、2026-10-06 に上記の文書と照合しました
次に読む
- 同じ構図:プロトタイプ汚染(Node.js)(外部データが「構造」を決める) / ファイルアップロードの脆弱性(外部データが「置き場所」を決める)
- 用語:リモートコード実行(RCE)とは
- 実例:CVE-2026-45247(Magento拡張のPHPオブジェクトインジェクション)
- フレームワーク別:ASP.NET Core / Spring Boot / Django のセキュリティ対策
- 依存:osv-scannerの導入と使い方 / CVE対応の実務手順
よくある質問
Qデシリアライズの何がそんなに危険なのですか? ただのデータ変換では?
形式によっては、データ変換ではなく『オブジェクトの再構築』だからです。どのクラスのインスタンスを作るかという情報まで外部データに含まれる形式では、攻撃者は『何を作らせるか』を選べます。OWASPは、デシリアライザに対する攻撃がサービス停止、アクセス制御の迂回、そしてリモートコード実行を許すことが判明していると述べています。値の妥当性を検証しても、値を受け取る前の段階で攻撃が成立してしまう点が厄介です。
Qいちばん確実な対策は何ですか?
外部から来るデータを、型を選べない形式に限定することです。OWASPは『JSONやXMLのような純粋なデータ形式に切り替えることで、独自のデシリアライズ処理が悪意ある目的に転用される可能性を減らせる』としています。そのうえで、どうしても独自形式が必要なら『シリアライズの過程で署名し、認証された署名を持たないメッセージはデシリアライズしないという選択ができる』とも書かれています。形式を変える、それが無理なら署名で守る、という順番です。
Q設定を固めれば安全に使えますか?
仕組みによります。OWASPは .NET の BinaryFormatter について『この型は危険であり、安全にすることはできない』と明言しています。つまり一部の機構は、設定や検証の追加では安全にならず、使用をやめる以外の解決がありません。設定で守れるものと、そもそも守れないものを分けて考えてください。『正しく使えば大丈夫』が成り立たない道具は実在します。
Q自分の言語では何に気をつければいいですか?
OWASPが名指ししているものだけでも、PHPでは unserialize() を避けてJSONを使うこと、Pythonでは pickle / c_pickle、PyYAML の load、jsonpickle が危険であること、JavaではObjectInputStreamのresolveClassをオーバーライドして復元できるクラスを制限すること、.NETではBinaryFormatterを使わないことが挙げられています。共通しているのは『言語の便利な永続化機構ほど、外部データに向けてはいけない』という点です。
Q自分のコードに危険なデシリアライズがあるか、どう探せばいいですか?
3段階で探します。まず ripgrep や grep で、pickle.loads・unserialize・Marshal.load・ObjectInputStream・BinaryFormatter などの呼び出しを文字列検索します。次に、静的解析のルールを有効にします(PythonはBanditのB301・B302・B506、.NETはコード分析のCA2300番台)。最後に、依存パッケージの既知の脆弱性を osv-scanner などで調べ、CWE-502のものを優先して更新します。見つかった呼び出しは、渡しているデータがどこから来たかを1件ずつ確認し、外から触れる経路につながるものから直します。
QJSONを使っていれば必ず安全ですか?
形式がJSONでも、型名をデータの中に書かせる設定を有効にすると安全ではなくなります。代表例は .NET の Json.NET にある TypeNameHandling で、Microsoftのコード分析ルールCA2326は『None 以外の値を使わない』ことを求めています。JSONの利点は、値だけを運び、どのクラスを作るかを受け取る側のコードが決められる点にあります。その利点を消す設定を使っていないかを確認してください。