C#の制御されない複合書式文字列

外部入力が.NETの複合書式文字列を制御する問題

説明

.NETの複合書式は、{index[,alignment][:formatString]}という形式の項目を解釈します。渡された引数のどれを何回表示するか、配置や値ごとの書式を決めます。外部入力をString.Format、StringBuilder.AppendFormat、TextWriter.Write・WriteLine、Console.Write・WriteLineの複合書式引数に使うと、攻撃者が出力構造を制御できます。

Cのprintfの書式文字列脆弱性とは影響が異なります。マネージド.NETの複合書式には%nのようなメモリ書き込み指示がなく、呼び出しスタックの任意の値も読みません。C/C++の任意メモリの読み書きや直接的なコード実行を、そのままこの問題の影響としないでください。

想定される影響

  • インデックスの変更や繰り返しによって、同じ呼び出しに渡した他の引数を表示される可能性があります。機密の値を渡していれば漏えいにつながります。
  • 閉じていない波括弧、範囲外のインデックス、不正な書式項目はFormatExceptionを起こします。未処理ならリクエストの失敗や継続的なサービス障害につながります。
  • 極端に大きい配置幅は出力やメモリ使用量を増やします。独自のIFormatProviderやICustomFormatterを使う場合、アプリケーション固有の書式処理を選ばれる可能性もあります。
  • ログ、応答、通知を改変されると、それを使うセキュリティ判断や運用で、パーサーや担当者が誤認する可能性があります。

対処方法

  1. 複合書式の構造をコードで管理するリテラルに固定し、信頼できない文字列は値の引数として渡してください。開発者が書いた文字列補間も、表示の構造を入力値から分離できます。
  2. 生の書式文字列を受け取らず、compactやlabelledなどの識別子を変更不可のリテラル書式に対応付けてください。
  3. 生の書式が必要なら、少数の固定リテラルとの完全一致だけを許可してください。渡す値の種類と出力サイズも制限します。接頭辞や部分文字列の確認では、書式全体を制限できません。
  4. CompositeFormat.Parseは文法を検証して結果をキャッシュしますが、有効な書式の中で引数や表示方法を選ばれる問題は残ります。安全性を保証する機能として扱わないでください。
  5. Sanitizeという名前のヘルパーや、片方の波括弧だけを除去する変換では安全性を確認できません。開き・閉じ両方の波括弧を除去すれば書式構文はなくなりますが、元の文字列を壊すため一般的な対策には向きません。

例

変更前

csharp
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("format")]
public sealed class FormatController : ControllerBase
{
    [HttpGet]
    public string Render(
        [FromQuery] string format,
        [FromQuery] string displayName)
    {
        string internalReference = HttpContext.TraceIdentifier;
        return string.Format(format, displayName, internalReference);
    }
}

formatに{1}を渡すと、表示する意図のないinternalReferenceの文字列表現が含まれる可能性があります。不正な波括弧や存在しない引数のインデックスはFormatExceptionを起こします。

変更後

リテラルの書式

csharp
using System.Globalization;

return string.Format(
    CultureInfo.InvariantCulture,
    "사용자: {0}",
    displayName);

書式の構造とカルチャープロバイダーはアプリケーションが決め、入力は値としてだけ使います。Console.WriteLine(message)やTextWriter.Write(message)の単一文字列オーバーロードは、その値を複合書式として解釈しません。

定義済みの書式を選択

csharp
using System.Globalization;

static string SelectFormat(string style) => style switch
{
    "compact" => "{0}",
    "labelled" => "사용자: {0}",
    _ => "사용자: {0}"
};

string format = SelectFormat(style);
return string.Format(CultureInfo.InvariantCulture, format, displayName);

利用者が選ぶのは限定した識別子であり、書式文字列そのものではありません。すべての分岐で開発者が書いたリテラルを返します。

固定の書式でも、引数の個数・型との一致と、独自フォーマッターの動作を確認してください。.NETの解析規則CA2241とテストを併用できます。

参考資料