コマンドインジェクション

C#のProcess APIにおけるコマンド・引数の注入リスクと、安全なプロセス実行の方法です。

説明

信頼できない入力を Process.Start や ProcessStartInfo に渡すと、次の2種類の危険が生じる場合があります。

  • ProcessStartInfo.FileName や Process.Start の第1引数を制御されると、意図しない実行ファイルを選択されるおそれがあります。
  • ProcessStartInfo.Arguments や Process.Start(string, string) の第2引数に入力を含めると、生のコマンドラインの引数境界やオプションを変更されるおそれがあります。実行ファイルが cmd.exe、PowerShell、/bin/sh、/bin/bash などのインタープリターなら、入力がコマンド構文として実行される可能性があります。

UseShellExecute = false は、OSのファイル関連付けに使われるシェルを使用しない設定です。アプリが cmd.exe、PowerShell、sh などのインタープリターを直接起動することは防ぎません。

ProcessStartInfo.ArgumentList と.NET 10の Process.Start(string, IEnumerable<string>) オーバーロードは引数の境界を保つため、生の Arguments 文字列より適しています。ただし、起動先のプログラムが入力をオプションとして解釈する場合があります。また、sh -c、cmd /c、PowerShellの -Command に続く値は、引き続き実行するコードです。

想定される影響

  • アプリケーションの権限での任意のコマンドやプログラムの実行
  • ファイルの読み取り・改ざん・削除、認証情報の窃取、内部ネットワークへのアクセス
  • 攻撃者が選ぶオプションによるデータ流出、権限の悪用、サービスの停止

対処方法

  1. ファイル、圧縮、ネットワーク、データ変換などの処理には、可能ならプロセスを起動せず、マネージドライブラリのAPIを使います。
  2. プロセスが必要な場合も、外部入力を実行ファイルのパスにしないでください。検証した操作識別子を、開発者が管理する固定の絶対パスに対応付けます。実行ファイルは信頼できるものを使い、非特権ユーザーが変更できない場所に置きます。
  3. UseShellExecute を false にし、コマンドインタープリターを呼び出さないでください。-c、/c、-Command など、コードを実行するオプションの後に信頼できない値を渡さないでください。
  4. ProcessStartInfo.ArgumentList や IEnumerable<string> のオーバーロードで、引数を1つずつ渡します。各値には、起動先のプログラムの構文と意味に適した許可リスト検証と長さ制限を適用します。
  5. 起動先が対応している場合は、信頼できないオペランドの前に -- などのオプション終端マーカーを置きます。この規則に対応していないプログラムでは効果がありません。
  6. 必要最小限の権限で実行し、必要なファイルシステムとネットワークへのアクセスだけを許可します。すべてのプログラムで共通に使えるコマンドラインのエスケープ関数があるとは考えないでください。

例

変更前

csharp
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

public sealed class RunController : Controller
{
    public void Run([FromQuery] string command)
    {
        var processInfo = new ProcessStartInfo
        {
            FileName = "/bin/sh",
            UseShellExecute = false
        };
        processInfo.ArgumentList.Add("-c");
        processInfo.ArgumentList.Add(command);
        Process.Start(processInfo);
    }
}

ArgumentList が引数の境界を保っても、-c の後の値はシェルが解釈するコードです。UseShellExecute = false にしても、直接起動した /bin/sh の解釈処理は無効になりません。

変更後

csharp
using System.Diagnostics;

static class Printer
{
    public static void PrintUserValue(string userInput)
    {
        var processInfo = new ProcessStartInfo
        {
            FileName = "/usr/bin/printf",
            UseShellExecute = false
        };
        processInfo.ArgumentList.Add("%s\n");
        processInfo.ArgumentList.Add(userInput);
        Process.Start(processInfo);
    }
}

コマンドインタープリターを使わず、固定された実行ファイルと書式文字列を選びます。ユーザーの値は、別のデータ引数として渡します。配備先では、/usr/bin/printf の存在と信頼できる所有者を確認する必要があります。単純な出力であれば、プロセスを作らない Console.WriteLine のようなAPIが適しています。

参考資料