アセンブリパスインジェクション

信頼できないパスからのC#アセンブリのロード

説明

外部入力がアセンブリのファイルを選ぶと、攻撃者が配置・選択したマネージドコードを、アプリケーション自身の権限でロードする可能性があります。Assembly.LoadFrom、Assembly.LoadFile、Assembly.UnsafeLoadFrom、現行.NETのAssemblyLoadContext.LoadFromAssemblyPathは、ファイルの信頼性を保証しません。

Path.GetFullPath、Path.GetFileName、.dllの拡張子確認では、ファイルの出所や発行元を認証できません。AssemblyLoadContextはバージョンや依存関係を分離する機能であり、セキュリティ用サンドボックスではありません。Microsoftも、信頼した.NETプロセス内に信頼できないコードを安全にロードすることはできないと説明しています。

パストラバーサルと重なる場合がありますが、対象の境界が異なります。パストラバーサルは意図したディレクトリ外のファイルアクセスを扱い、アセンブリパスインジェクションはロード・起動・実行するコードを入力で選ばれる問題です。

想定される影響

  • アプリケーションのアカウント権限で任意のマネージドコードを実行される可能性があります。
  • アプリケーションがアクセスできるファイル、認証情報、データベース、内部ネットワークが露出する可能性があります。
  • 悪意のあるプラグインや依存コードが、侵入状態の維持や追加の攻撃に使われる可能性があります。

対処方法

  1. ファイルパスやアセンブリ名ではなく、限定したプラグイン識別子だけを受け取ってください。
  2. サーバー側で識別子を確認済みの固定ファイル名や絶対パスに対応付けてください。ディレクトリとマニフェストを信頼できない利用者が変更できないよう、デプロイ権限を制限します。
  3. 現行.NETでは、承認したパスだけをAssemblyLoadContextでロードしてください。プラグインごとの依存関係解決が必要なら、入力値ではなく承認済みのパスでAssemblyDependencyResolverを設定します。
  4. 外部のプラグインを許可する場合は、明示的な信頼方針に基づき、発行元の署名や署名済みマニフェストのハッシュを検証してください。依存コードも対象です。攻撃者がファイルと一緒に渡したハッシュだけでは信頼性を確認できません。
  5. 厳密名はアセンブリの識別用で、コードの信頼性を保証しません。.NET Coreと.NET 5以降は、実行時に厳密名の署名を検証しません。発行元の認証が必要なら、プラットフォームに合ったAuthenticodeなどの署名方針を使ってください。
  6. 信頼できないプラグインを実行する必要があれば、同じプロセスにロードせず、別のOSプロセス、コンテナー、仮想マシンで最小権限に隔離してください。

新しいコードではAssembly.UnsafeLoadFromや、パスを受け取る従来のAppDomain・Activator APIを避けてください。現行APIへの置換だけでは十分ではなく、対象の選択と信頼性を先に制限する必要があります。

例

変更前

csharp
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

[ApiController]
[Route("plugins")]
public sealed class PluginController : ControllerBase
{
    [HttpPost("load")]
    public Assembly Load([FromQuery] string assemblyPath)
    {
        // 絶対パスに正規化しても、ファイルの信頼性は確認できません。
        string normalized = Path.GetFullPath(assemblyPath);
        return AssemblyLoadContext.Default.LoadFromAssemblyPath(normalized);
    }
}

変更後

csharp
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

static class ApprovedPlugins
{
    // このディレクトリに書き込めるのはデプロイ用アカウントだけにします。
    private const string PluginRoot = "/opt/example/plugins";

    public static Assembly Load(string pluginId)
    {
        string fileName = pluginId switch
        {
            "reporting" => "ReportingPlugin.dll",
            "billing" => "BillingPlugin.dll",
            _ => throw new ArgumentOutOfRangeException(nameof(pluginId))
        };

        string trustedPath = Path.GetFullPath(Path.Combine(PluginRoot, fileName));
        return AssemblyLoadContext.Default.LoadFromAssemblyPath(trustedPath);
    }
}

[ApiController]
[Route("plugins")]
public sealed class PluginController : ControllerBase
{
    [HttpPost("load")]
    public IActionResult Load([FromQuery] string pluginId)
    {
        Assembly plugin = ApprovedPlugins.Load(pluginId);
        return Ok(plugin.GetName().Name);
    }
}

変更前は入力されたパスを正規化するだけで、アセンブリの出所や権限を検証しません。変更後は限定した識別子を固定ファイル名に対応付けます。ディレクトリと配置するファイルを、信頼したデプロイ担当だけが変更できることが前提です。

発行元や内容の検証に失敗したら、ロードを中止してください。検証方針、信頼する鍵の保存先、プラグインの利用権限も別途保護してください。

参考資料