安全でない逆シリアル化

C#で信頼できないデータの安全でない逆シリアル化を防ぐ

説明

逆シリアル化は、バイト列やテキストで保存したデータから実行時のオブジェクトを復元する処理です。入力ペイロードが生成する.NET型を指定できると、攻撃者はアプリケーションや依存ライブラリにあるガジェット型を選び、コンストラクター、プロパティのsetter、逆シリアル化のコールバックなどの副作用を実行させるおそれがあります。

BinaryFormatter は、このような型情報をペイロードに含む汎用のオブジェクトグラフ逆シリアル化器です。バインダーや設定によって安全にすることはできません。.NET 9以降では、ランタイムに組み込まれた実装を使用すると、常に PlatformNotSupportedException が発生します。サポート対象外の互換パッケージで機能を復活させることはできますが、脆弱性も戻るため、セキュリティ対策にはなりません。Microsoftは SoapFormatter、NetDataContractSerializer、LosFormatter、ObjectStateFormatter も、制限のない多態的な逆シリアル化が可能な危険な代替手段としています。

Json.NETの TypeNameHandling は別のケースです。既定の TypeNameHandling.None は、入力の $type メタデータを型の選択に使用しません。All、Auto、Objects、Arrays では入力が型の選択に影響するため、外部データを逆シリアル化する際は、明示的な許可リストを強制する ISerializationBinder が必要です。実際に許可リストを適用し、想定外の型を拒否する実装であることを確認してください。

想定される影響

  • アプリケーションのプロセス権限でのリモートコード実行。
  • ファイルの読み書き、ネットワークリクエスト、秘密情報の漏えい。
  • 認証や権限チェックの回避、オブジェクト状態の改ざん。
  • 過大なオブジェクトグラフ、再帰、メモリ割り当てによるサービス拒否。

対処方法

  1. BinaryFormatter と、型を制限しない従来のフォーマッターを取り除いてください。SerializationBinder の追加を理由に BinaryFormatter を使い続けないでください。
  2. 新しいJSON処理では、明示的なDTOを対象とする System.Text.Json を優先してください。ポリモーフィズムが必要な場合は、許可する派生型と識別子だけを指定してください。
  3. 既存のNRBFデータを移行する場合は、System.Formats.Nrbf.NrbfDecoder でレコードを読み取り、アプリケーションが定義した限定的な型集合に手動で対応付けてください。ペイロードが指定した型を読み込んだり、インスタンスを生成したりしないでください。
  4. Json.NETでは、可能な限り TypeNameHandling.None を維持してください。型の処理が必要な場合は、ISerializationBinder.BindToType が想定外のすべての型に対して null を返すか例外を発生させるよう実装し、逆シリアル化に使う設定に直接指定してください。
  5. シリアル化したデータが信頼境界を越える場合は、HMACやデジタル署名で完全性と送信元を確認し、鍵の更新を設計してください。リプレイ対策はnonce、連番、有効期限などで別途実装してください。これらは型の制限を補完するものであり、BinaryFormatter を安全にするものではありません。

例

変更前

リクエスト本文を BinaryFormatter に渡す

csharp
using Microsoft.AspNetCore.Mvc;
using System.Runtime.Serialization.Formatters.Binary;

public object Parse([FromBody] Stream stream)
{
    var formatter = new BinaryFormatter();
    return formatter.Deserialize(stream);
}

バインダーなしでJson.NETの型メタデータを許可する

csharp
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

public object Parse([FromBody] string json)
{
    return JsonConvert.DeserializeObject(
        json,
        new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Auto
        });
}

変更後

明示的なDTOへの逆シリアル化

csharp
using System.Text.Json;

public sealed record PaymentRequest(string AccountId, decimal Amount);

PaymentRequest request =
    JsonSerializer.Deserialize<PaymentRequest>(json)
    ?? throw new JsonException("Missing request body");

許可された派生型だけを使う System.Text.Json のポリモーフィズム

csharp
using System.Text.Json;
using System.Text.Json.Serialization;

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$kind")]
[JsonDerivedType(typeof(CardPayment), "card")]
public abstract class Payment;

public sealed class CardPayment : Payment;

Payment payment =
    JsonSerializer.Deserialize<Payment>(json)
    ?? throw new JsonException("Missing payment");

オブジェクトを生成せずに従来のNRBFペイロードを読み取る

csharp
using System.Formats.Nrbf;

SerializationRecord root = NrbfDecoder.Decode(stream);

// rootのレコードをサイズ・深さの制限と限定的な型マッピングで検証し、
// アプリケーションDTOに必要な値だけをコピーします。

NrbfDecoder の結果から任意の型を読み込んだり、汎用のオブジェクトグラフを再構築したりすると、保護の効果が失われます。

Json.NETに制限付きバインダーを適用する

csharp
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public sealed class KnownTypesBinder : ISerializationBinder
{
    public Type BindToType(string? assemblyName, string typeName) =>
        typeName == nameof(PaymentRequest)
            ? typeof(PaymentRequest)
            : throw new JsonSerializationException("Unexpected type");

    public void BindToName(
        Type serializedType,
        out string? assemblyName,
        out string? typeName)
    {
        if (serializedType != typeof(PaymentRequest))
        {
            throw new JsonSerializationException("Unexpected type");
        }

        assemblyName = null;
        typeName = nameof(PaymentRequest);
    }
}

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    SerializationBinder = new KnownTypesBinder()
};

PaymentRequest request =
    JsonConvert.DeserializeObject<PaymentRequest>(json, settings)
    ?? throw new JsonSerializationException("Missing request");

可能であれば、この互換構成よりも TypeNameHandling.None と明示的なDTOを優先してください。既定の型ローダーにフォールバックしたり、入力のアセンブリ名や型名をそのまま Type.GetType に渡したりするバインダーは、許可リストにはなりません。

分類

  • CWE-502: Deserialization of Untrusted Data
  • OWASP Top 10 2025 A08: Software or Data Integrity Failures
  • OWASP Top 10 2021 A08: Software and Data Integrity Failures
  • OWASP ASVS 5.0.0 v5.0.0-1.5.2: Safe Deserialization

参考資料