デシリアライズ

Deserialization Cheat Sheet

原典
Deserialization Cheat Sheet(OWASP Cheat Sheet Series)
原文
GitHub 上の Markdown
底本
bac04fb5(2026-07-29 時点)
ライセンス
CC BY-SA 4.0(原典と同一。訳文も同ライセンスで再配布できます)

はじめに

この記事は、アプリケーションで信頼できないデータを安全にデシリアライズするための、明確で実行可能な指針を示すことに主眼を置いている。

デシリアライズとは何か

シリアライズは、何らかのオブジェクトを、後で復元できるデータ形式へ変換する処理である。 保存のためにオブジェクトを取っておく、あるいは通信の一部として送るために、しばしばシリアライズが行われる。

デシリアライズはその逆の処理であり、何らかの形式で構造化されたデータを取り、それをオブジェクトへ組み立て直す。 今日、データのシリアライズに最も広く使われる形式は JSON である。 それ以前は XML であった。

しかし多くのプログラミング言語は、オブジェクトをシリアライズする独自の方法を持つ。 こうした独自の形式は通常、シリアライズ処理の作り替えを含め、JSON や XML より多くの機能を提供する。

残念ながら、こうした独自のデシリアライズ機構の機能は、信頼できないデータを扱うときに悪意ある目的へ転用されうる。 デシリアライザに対する攻撃は、サービス妨害、アクセス制御の回避、リモートコード実行(RCE)を可能にすることがわかっている。

オブジェクトを安全にデシリアライズするための指針

以下の言語ごとの指針は、信頼できないデータをデシリアライズするための安全な方法を列挙しようとするものである。

PHP

内部が見える精査

unserialize() 関数の使用箇所を確認し、外部のパラメータをどう受け取っているかを精査する。 シリアライズしたデータを利用者へ渡す必要がある場合は、JSON(json_decode()json_encode() 経由)のような安全な標準のデータ交換形式を使う。

内部が見えない精査

通信データの末尾にドット . の記号が含まれていれば、そのデータはシリアライズされて送られた可能性が高い。 これはデータが Base64 や 16 進の方式でエンコードされていない場合にのみ成り立つ。 エンコードされている場合は、パラメータ値の先頭の文字を見てシリアライズが起きている可能性を確かめるのがよい。 たとえばデータが Base64 でエンコードされていれば、多くの場合 gASV で始まる。

内部が見える精査

Python の次の API はシリアライズに関する攻撃に対して脆弱である。 以下のパターンをコード中から探す。

  1. pickle/c_pickle/_pickleload/loads とともに使っている箇所
import pickle
data = """ cos.system(S'dir')tR. """
pickle.loads(data)
  1. PyYAMLload とともに使っている箇所
import yaml
document = "!!python/object/apply:os.system ['ipconfig']"
print(yaml.load(document))
  1. jsonpickleencodestore のメソッドとともに使っている箇所

Java

以下の手法はいずれも、Java の Serializable 形式に対するデシリアライズ攻撃の防止に有効である。

実装上の助言

  • コード内で ObjectInputStream#resolveClass() メソッドを上書きし、任意のクラスがデシリアライズされることを防ぐ。この安全な振る舞いは SerialKiller のようなライブラリに包める。
  • 汎用の readObject() メソッドを安全な代替に置き換える。入力の長さとデシリアライズされるオブジェクトの数を確認することで、billion laughs 型の攻撃にも対処できる点に注意する。

内部が見える精査

シリアライズの脆弱性の可能性について、次の Java の API の使用に注意する。

  1. 外部の利用者が定義したパラメータを伴う XMLdecoder
  2. fromXML メソッドを伴う XStream(xstream のバージョン 1.4.6 以下はシリアライズの問題に対して脆弱である)
  3. readObject を伴う ObjectInputStream
  4. readObjectreadObjectNoDatareadResolvereadExternal の使用
  5. ObjectInputStream.readUnshared
  6. Serializable

内部が見えない精査

取得した通信データに次のパターンが含まれる場合、そのデータが Java のシリアライズのストリームで送られたことを示唆しうる。

  • 16 進で AC ED 00 05
  • Base64 で rO0
  • HTTP 応答の Content-type ヘッダが application/x-java-serialized-object に設定されている

データの漏出と信頼された項目の上書きを防ぐ

デシリアライズの際にエンドユーザーが決して制御すべきでない、あるいはシリアライズの際に利用者へ露出すべきでないオブジェクトのデータメンバがある場合、それらは transient キーワードProtecting Sensitive Information の節)で宣言するべきである。

Serializable として定義されたクラスでは、機微な情報の変数は private transient として宣言するべきである。

たとえば myAccount クラスでは、変数 profitmargin がシリアライズされないよう transient として宣言されている。

public class myAccount implements Serializable
{
    private transient double profit; // declared transient

    private transient double margin; // declared transient
    ....

ドメインオブジェクトのデシリアライズを防ぐ

アプリケーションのオブジェクトの一部は、その階層のために Serializable の実装を強いられることがある。 アプリケーションのオブジェクトがデシリアライズされないことを保証するには、常に例外を投げる readObject() メソッドを(final 修飾子付きで)宣言するべきである。

private final void readObject(ObjectInputStream in) throws java.io.IOException {
    throw new java.io.IOException("Cannot be deserialized");
}

自前の java.io.ObjectInputStream を堅牢にする

java.io.ObjectInputStream クラスはオブジェクトのデシリアライズに使われる。 これを継承することで、その振る舞いを堅牢にできる。 これは次の条件を満たす場合に最良の解決策である。

  • デシリアライズを行うコードを変更できる
  • どのクラスがデシリアライズされることを想定しているのかがわかっている

基本的な考え方は、デシリアライズを許すクラスを制限するために ObjectInputStream.resolveClass() を上書きすることである。

この呼び出しは readObject() が呼ばれる前に起こるので、許可した型でないかぎりデシリアライズの動作が起こらないことを確実にできる。

単純な例を示す。 LookAheadObjectInputStream クラスは、Bicycle クラス以外のいかなる型もデシリアライズしないことが保証される。

public class LookAheadObjectInputStream extends ObjectInputStream {

    public LookAheadObjectInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    /**
    * Only deserialize instances of our expected Bicycle class
    */
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        if (!desc.getName().equals(Bicycle.class.getName())) {
            throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
        }
        return super.resolveClass(desc);
    }
}

この方法のより完全な実装が、コミュニティのさまざまな人々によって提案されている。

  • NibbleSec。デシリアライズを許すクラスの一覧を作れるライブラリ。
  • IBM。最も破壊的な悪用の筋書きが想定される何年も前に書かれた、先駆的な保護。
  • Apache Commons IO のクラス

エージェントによってすべての java.io.ObjectInputStream の使用を堅牢にする

前述のとおり java.io.ObjectInputStream クラスはオブジェクトのデシリアライズに使われ、継承によってその振る舞いを堅牢にできる。 しかしそのコードを自分が所有していない場合、あるいはパッチを待てない場合は、エージェントを使って java.io.ObjectInputStream に堅牢化を織り込むのが最良の解決策である。

ObjectInputStream を大域的に変えることが安全なのは、既知の悪意ある型を拒否リストに入れる場合に限られる。 すべてのアプリケーションについて、デシリアライズが想定されるクラスを知ることはできないからである。 幸い、今日知られているすべての攻撃経路から守るために拒否リストへ入れる必要のあるクラスは、ごくわずかである。

濫用されうる「ガジェット」のクラスがさらに発見されることは避けられない。 しかし今日、修正を必要とする膨大な量の脆弱なソフトウェアが露出している。 場合によっては、脆弱性の「修正」がメッセージングの仕組みの再設計と後方互換性の破棄を伴うこともある。 開発者はシリアライズされたオブジェクトを受け付けない方向へ進んでいる。

これらのエージェントを有効にするには、新しい JVM のパラメータを加えるだけでよい。

-javaagent:name-of-agent.jar

この方法を採るエージェントが、コミュニティのさまざまな人々によって公開されている。

似ているが拡張性の劣る方法として、JVM の ObjectInputStream を手作業でパッチしてブートストラップすることがある。 この方法の指針はこちらにある。

他のデシリアライズのライブラリと形式

上記の助言は Java の Serializable 形式に焦点を置いているが、デシリアライズに他の形式を使うライブラリも多数ある。 それらの多くも、正しく設定されなければ同様の安全上の問題を抱えうる。 この節は、そうしたライブラリの一部と、信頼できないデータをデシリアライズするときに安全上の問題を避けるための推奨設定を挙げる。

既定の設定で安全に使えるもの

次のライブラリは既定の設定で安全に使える。

安全に使うには設定が必要なもの

次のライブラリは、安全に使うために設定が必要である。

  • fastjson v1.2.68 以降(JSON)。safemode の設定を有効にしないかぎり安全に使えない。この設定は任意のクラスのデシリアライズを無効にする(文書を参照)。それより前のバージョンは安全ではない。
  • json-io(JSON)。JSON における @type プロパティの使用が任意のクラスのデシリアライズを許すため、安全に使えない。次の場合にのみ安全に使える。
    • 汎用のオブジェクトのデシリアライズを無効にする JsonReader.USE_MAPS の設定を用いた型なしモード
    • どのクラスがデシリアライズされるかを制御する独自のデシリアライザを用いる場合
  • Kryo v5.0.0 より前(独自形式)。クラスの登録を有効にしないかぎり安全に使えない。この設定は任意のクラスのデシリアライズを無効にする(文書この issueを参照)。
    • 注記:Chill のような Kryo のラッパーも存在し、背後の Kryo のバージョンに関わらずクラスの登録が既定で必須になっていないことがある。
  • SnakeYAML(YAML)。org.yaml.snakeyaml.constructor.SafeConstructor クラスを使わないかぎり安全に使えない。このクラスは任意のクラスのデシリアライズを無効にする(文書を参照)。

安全に使えないもの

次のライブラリは、保守されていないか、信頼できない入力とともに安全に使えない。

  • Castor(XML)。2016 年以降コミットがなく、放棄されているように見える。
  • fastjson v1.2.68 より前(JSON)。これらのバージョンは任意のクラスのデシリアライズを許す(文書を参照)。
  • JDK の XMLDecoder(XML)。信頼できない入力からこの形式で Java のオブジェクトを安全にデシリアライズすることは、ほぼ不可能とされている("Red Hat Defensive Coding Guide" の 2.6.5 節の末尾)。
  • XStream v1.4.17 より前(JSON と XML)。これらのバージョンは任意のクラスのデシリアライズを許す(文書を参照)。
  • YamlBeans v1.16 より前(YAML)。これらのバージョンは任意のクラスのデシリアライズを許す(この文書を参照)。

.NET と C#

内部が見える精査

ソースコードから次の語句を探す。

  1. TypeNameHandling
  2. JavaScriptTypeResolver

利用者が制御できる変数によって型が設定されるシリアライザを探す。

内部が見えない精査

次で始まる Base64 でエンコードされた内容を探す。

AAEAAAD/////

次のテキストを含む内容を探す。

  1. TypeObject
  2. $type:

一般的な注意

Microsoft は BinaryFormatter 型が危険であり安全にできないと述べている。 したがってこれを使うべきではない。 詳細は BinaryFormatter のセキュリティガイドにある。

デシリアライズ先のオブジェクトの型を、データのストリームに決めさせない。 可能なかぎり DataContractSerializerXmlSerializer を使うことで、これを防げる。

JSON.Net を使っている場合は、TypeNameHandlingNone にのみ設定されている状態にする。

TypeNameHandling = TypeNameHandling.None

JavaScriptSerializer を使う場合は、JavaScriptTypeResolver とともに使わない。

自身の型を定義するデータのストリームをデシリアライズしなければならない場合は、デシリアライズを許す型を制限する。 これでもなおリスクがあることに注意する。 .NET の多くの標準の型がそれ自体で危険になりうるからである。 たとえば次のものである。

System.IO.FileInfo

サーバー上に実際に存在するファイルを参照する FileInfo のオブジェクトは、デシリアライズされたときにそのファイルの属性を変えうる。 たとえば読み取り専用にすることで、サービス妨害攻撃を成立させうる。

デシリアライズできる型を制限していても、型によってはリスクのあるプロパティを持つことを忘れない。 たとえば System.ComponentModel.DataAnnotations.ValidationExceptionObject 型の Value プロパティを持つ。 この型がデシリアライズを許された型であれば、攻撃者は Value プロパティを任意のオブジェクト型に設定できる。

攻撃者が、生成される型を誘導することを防ぐべきである。 それが可能であれば、DataContractSerializerXmlSerializer すら覆される。 たとえば次のようになる。

// Action below is dangerous if the attacker can change the data in the database
var typename = GetTransactionTypeFromDatabase();

var serializer = new DataContractJsonSerializer(Type.GetType(typename));

var obj = serializer.ReadObject(ms);

.NET の一部の型では、デシリアライズの最中に実行が起こりうる。 次のような対策を作っても効果はない。

var suspectObject = myBinaryFormatter.Deserialize(untrustedData);

//Check below is too late! Execution may have already occurred.
if (suspectObject is SomeDangerousObjectType)
{
    //generate warnings and dispose of suspectObject
}

JSON.Net では、独自の SerializationBinder を使ってより安全な許可リスト方式の対策を作れる。

既知の .NET の安全でないデシリアライズのガジェットについて最新の情報を保ち、そうした型が自分のデシリアライズ処理によって生成されうる箇所にはとくに注意を払う。 デシリアライザは、自分が知っている型しか生成できない。

ガジェットになりうるコードは、インターネットに接続するコードから切り離しておく。 たとえば WPF のアプリケーションで使われる System.Windows.Data.ObjectDataProvider は、任意のメソッドの呼び出しを許す既知のガジェットである。 信頼できないデータをデシリアライズする REST サービスのプロジェクトで、このアセンブリへの参照を持つのはリスクがある。

既知の .NET の RCE ガジェット

  • System.Configuration.Install.AssemblyInstaller
  • System.Activities.Presentation.WorkflowDesigner
  • System.Windows.ResourceDictionary
  • System.Windows.Data.ObjectDataProvider
  • System.Windows.Forms.BindingSource
  • Microsoft.Exchange.Management.SystemManager.WinForms.ExchangeSettingsProvider
  • System.Data.DataViewManager, System.Xml.XmlDocument/XmlDataDocument
  • System.Management.Automation.PSObject

言語に依存しない安全なデシリアライズの方法

別のデータ形式を使う

言語独自のシリアライズとデシリアライズの形式を避けることで、リスクは大きく下がる。 JSON や XML のような純粋なデータ形式へ切り替えれば、独自のデシリアライズのロジックが悪意ある目的へ転用される可能性が減る。

多くのアプリケーションは、データの転送を明示的な目的とする別のオブジェクトの領域を作るデータ転送オブジェクトのパターンに依拠している。 もちろん、純粋なデータオブジェクトを解析したあとにアプリケーションが安全上の誤りを犯す可能性は残る。

署名されたデータのみをデシリアライズする

処理すべきメッセージがデシリアライズの前にわかっているなら、シリアライズの処理の一部としてそれらに署名できる。 そうすればアプリケーションは、認証された署名を持たないメッセージをデシリアライズしないことを選べる。

緩和のための道具とライブラリ

検出のための道具

参考資料