完全性の検証

OWASP Top 10:2025 の A08 と Deserialization Cheat Sheet を取得して読み、 理解した内容を自分の言葉で書いたもの。本文の引用はしていない。

OWASP Top 10A08 Software or Data Integrity Failures。 2025年版で「Software and Data」から「Software or Data」に変わり、 扱う範囲が絞り込まれた

A03 との線引き

先にここを決めておかないと、サプライチェーンと重複する。

対象
A03 サプライチェーン供給網という生態系の側。ベンダー管理、依存関係、ビルド基盤の安全性
A08 完全性の検証アプリケーションとデータの層での検証の欠落。より粒度が細かい

言い換えると、A03 が「信頼できる出所から取ってきたか」なら、 A08 は「取ってきたものを、使う前に検証したか」

ビルド側の完全性(プロヴェナンス、署名、SLSA のレベル)は サプライチェーンにある。 ここはその下流を扱う。

何が脆弱な状態か

自動更新が、十分な完全性の検証をせずに更新を取得して適用する
信頼できないデータをデシリアライズする
CI/CD が、検証なしに信頼できない出所からコードを引く
検証していないリポジトリや CDN からプラグイン・ライブラリを取り込む
外部のサービスや第三者のコードを、完全性の検査なしに読み込む

対応する CWE は14件。中心は CWE-502(信頼できないデータのデシリアライズ)CWE-829(信頼できない制御領域からの機能の取り込み)CWE-494(完全性の検査がないコードのダウンロード)

安全でないデシリアライズ

なぜ危険か

デシリアライズへの攻撃は、サービス拒否・アクセス制御の回避・リモートコード実行に至る。

危険なのは、言語ネイティブの直列化形式が「カスタマイズ機能」を持っていること。 JSON や XML のような純粋なデータ形式にはそれがない。 ネイティブ形式は、復元の途中で任意のコードを走らせる余地を仕様として持っている。

大原則

ネイティブの直列化形式を避ける。 これが最も効く。

純粋なデータ形式(JSON / XML)に切り替えるだけで、 独自のデシリアライズ処理が悪用される余地が大きく減る。

直列化の関心事を業務ロジックから引き剥がす(データ転送オブジェクト)。 処理すべきメッセージが事前に分かっているなら、直列化の時点で署名する。 署名が検証できないメッセージは、デシリアライズしないことを選べる。

言語ごとに危ないもの

言語使わない代わりに
PHPunserialize()json_encode() / json_decode()
Pythonpickle.load / pickle.loadsPyYAML.load()jsonpickle標準的なデータ形式。YAML は SafeConstructor
JavaObjectInputStream.readObject()XMLDecoderXStream.fromXML()(v1.4.6 以下)resolveClass() の上書き(後述)
.NETBinaryFormatterTypeNameHandlingJavaScriptTypeResolverDataContractSerializer / XmlSerializer。JSON.Net は TypeNameHandling.None

**BinaryFormatter は「用途を問わず不適」**とされている。

既定のままで安全なもの、設定が要るもの、避けるべきものが分かれている。

既定で安全   fastjson2(autotype off)、jackson-databind(多態なし)
             Kryo v5.0.0+(クラス登録あり)、XStream v1.4.17+(許可リスト)
設定が要る   fastjson v1.2.68+(safemode)、json-io(非型付け)、SnakeYAML(SafeConstructor)
避ける       fastjson <1.2.68、XMLDecoder、XStream <1.4.17、YamlBeans <1.16、Castor

通信の中から見つける

直列化されたデータには目印がある。 検知にも、レビューにも使える。

目印
AC ED 00 05 (16進) / rO0 (Base64)Java の直列化オブジェクト
Content-type: application/x-java-serialized-object同上
Base64 が gASV で始まる / 末尾に .Python の pickle
Base64 が AAEAAAD///// で始まる / $type: を含む.NET

先読みデシリアライズ

避けられない場合の本命がこれ。

Java なら ObjectInputStream.resolveClass() を上書きし、 許可した型でなければ例外を投げる。

@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException {
    if (!desc.getName().equals(Bicycle.class.getName())) {
        throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
    }
    return super.resolveClass(desc);
}

効く理由は呼ばれる順序にある。 resolveClass()readObject()に呼ばれる。 だから、許可していない型についてはデシリアライズの処理自体が始まらない。

ここが分かれ目で、デシリアライズした後に型を検査しても遅い。 オブジェクトの構築中に、既に実行されている。

決してデシリアライズさせたくないクラスには、明示的に塞ぐ手がある。

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

機微なフィールドは private transient にして、直列化の対象から外す。

許可リストにする理由

拒否リストは原理的に負ける。

新しいガジェット連鎖(既存クラスを組み合わせて任意コード実行に持ち込む経路)が 次々に見つかるので、拒否リストは常に古くなる。

許可リストは、既知の安全な型だけに絞るので、この競争から降りられる。 静的解析入力検証で「許可するものを列挙する」のと同じ構造。

環境の側で削る

ガジェットになりうるコードと、インターネットに繋がるコードを離す。

デシリアライズを行う部分は、権限を落とし、資源の上限と時間切れを設け、 失敗を記録し、ネットワークの到達範囲を絞る。

攻撃者が使える「部品」がその場に無ければ、連鎖は組み立たない。

更新の配信

署名のない更新は、それ自体が攻撃経路になる。

とくに端末のファームウェアで厄介なのは、 一度そういう仕組みで出荷すると、端末が自然に世代交代するまで脆弱なまま残ること。 サーバー側の修正のようには畳めない。

電子署名で「期待した出所から来ていて、改変されていない」ことを検証する。

ライブラリの取得元を、信頼できるパッケージマネージャに限定する
リスクの高い環境では、検証済みの内部リポジトリを持つ
非公式な配布元から、署名のないパッケージを取らない

CI/CD

パイプラインは本番インフラとして扱う。

ビルドの完全性を守る構成とアクセス制御を持ち、基盤を分離する
悪意あるコードがビルドに入り込まないよう、コードと設定の変更をレビューする

具体は秘密情報の管理サプライチェーンにある。

既存の記述との対応

既にある記述この文書との関係
サプライチェーンビルド側の完全性(署名・プロヴェナンス)。A08 はその下流
入力検証と出力エスケープ信頼できない入力を扱う姿勢。直列化データも入力
暗号と鍵の管理署名の検証に使う鍵の置き場所
秘密情報の管理CI/CD の分離とアクセス制御
依存関係取得元を絞る判断
例外条件の扱いデシリアライズの失敗をどう扱うか

関連