ASP.NET Core でファイルをアップロードする

作成者: Rutger Storm

ASP.NET Core では、小さいファイルの場合はバッファー モデル バインドを使用し、大きいファイルの場合は非バッファー ストリーミングを使用して、1 つ以上のファイルのアップロードがサポートされています。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

セキュリティに関する考慮事項

サーバーにファイルをアップロードする機能をユーザーに提供するときは、十分に注意してください。 攻撃者が次のようなことを試みる可能性があります。

  • サービス拒否攻撃を実行する。
  • ウイルスまたはマルウェアをアップロードする。
  • ネットワークやサーバーを他の方法で侵害する。

攻撃の成功の可能性を少なくするセキュリティ手順は、次のとおりです。

  • 専用のファイル アップロード領域 (できれば、システム ドライブ以外) にファイルをアップロードします。 専用の場所を使用すると、アップロードされるファイルにセキュリティ制限を適用しやすくなります。 ファイルのアップロード場所に対する実行アクセス許可を無効にします。†
  • アプリと同じディレクトリ ツリーに、アップロードしたファイルを保持しないでください。†
  • アプリによって決められた安全なファイル名を使用します。 ユーザーによって指定されたファイル名や、アップロードされるファイルの信頼されていないファイル名は、使用しないでください。† 信頼されていないファイル名を表示時に HTML エンコードします。 たとえば、ファイル名をログに記録したり、UI に表示したりします (Razor では、出力が自動的に HTML エンコードされます)。
  • アプリの設計仕様に対して承認されているファイル拡張子のみを許可します。†
  • クライアント側のチェックがサーバーで実行されることを確認します。† クライアント側のチェックは簡単に回避できます。
  • アップロードされたファイルのサイズをチェックします。 サイズの大きなアップロードを防ぐために、最大サイズ制限を設定します。†
  • 同じ名前でアップロードされたファイルによってファイルが上書きされないようにする必要があるときは、ファイルをアップロードする前に、データベースまたは物理ストレージに対してファイル名を確認します。
  • ファイルを格納する前に、アップロードされる内容に対してウイルス/マルウェア スキャナーを実行します。

† サンプル アプリで、条件を満たす方法が示されています。

警告

システムへの悪意のあるコードのアップロードは、頻繁に次のような内容のコードの実行するための足がかりとなります。

  • システムの制御を完全に掌握する。
  • システムがクラッシュする結果で、システムを過負荷状態にする。
  • ユーザーまたはシステムのデータを破壊する。
  • パブリック UI に落書きする。

ユーザーからファイルを受け入れる際の外部アクセスによる攻撃を減らす方法については、次の資料を参照してください。

サンプル アプリの例など、セキュリティ対策の実装の詳細については、「検証」セクションを参照してください。

ストレージのシナリオ

ファイルの一般的なストレージ オプションには次のようなものがあります。

  • データベース

    • 小さいファイルをアップロードする場合、物理ストレージ (ファイル システムまたはネットワーク共有) のオプションよりデータベースの方が速いことがよくあります。
    • 多くの場合、ユーザー データに対するデータベース レコードの取得でファイルの内容を同時に提供できるため (たとえば、アバター イメージ)、データベースの方が物理的なストレージ オプションより便利です。
    • データベースは、クラウド データ ストレージ サービスを使用するよりコストが低くなる可能性があります。
  • 物理ストレージ (ファイル システムまたはネットワーク共有)

    • 大きいファイルのアップロードの場合:
      • データベースの制限によって、アップロードのサイズが制限される場合があります。
      • 多くの場合、物理ストレージはデータベース内のストレージより高コストです。
    • 物理ストレージは、クラウド データ ストレージ サービスを使用するよりコストが低くなる可能性があります。
    • アプリのプロセスには、ストレージの場所に対する読み取りと書き込みのアクセス許可が必要です。 実行アクセス許可は付与しないでください。
  • クラウド データ ストレージ サービス (例: Azure Blob Storage)。

    • 通常、サービスでは、大抵の場合に単一障害点となるオンプレミス ソリューションより高いスケーラビリティと回復性が提供されます。
    • 大規模なストレージ インフラストラクチャのシナリオでは、サービスのコストが低下する可能性があります。

    詳しくは、.NET を使用してオブジェクト ストレージに BLOB を作成するクイックスタートに関する記事をご覧ください。

小さいおよび大きいファイル

小さいおよび大きいファイルの定義は、使用可能なコンピューティング リソースによって異なります。 アプリでは、予想されるサイズを処理できるように、使用されるストレージ アプローチのベンチマークを実行する必要があります。 メモリ、CPU、ディスク、データベースのパフォーマンスのベンチマークを実行します。

デプロイに対しては小さいものと大きいものに具体的な境界を提供することはできませんが、AspNetCore の FormOptions に関連する既定値の一部を次に示します。

  • 既定では、HttpRequest.Form は要求本文全体 (BufferBody) をバッファリングしませんが、含まれるマルチパート フォーム ファイルはバッファリングします。
  • MultipartBodyLengthLimit はバッファリングされたフォーム ファイルの最大サイズで、既定値は 128 MB です。
  • MemoryBufferThreshold は、ディスク上のバッファー ファイルに移行する前にメモリ内でファイルをバッファリングする量を示します。既定値は 64 KB です。 MemoryBufferThreshold は、アプリのリソースとシナリオに応じて増減される、小さいおよび大きいファイルの間の境界として機能します。

FormOptions の詳細については、ソース コードを参照してください。

ファイル アップロードのシナリオ

ファイルをアップロードするための一般的な 2 つの方法は、バッファーリングとストリーミングです。

バッファリング

ファイル全体が IFormFile に読み込まれます。 IFormFile は、ファイルの処理または保存に使用されるファイルの C# 表現です。

ファイルのアップロードで使用されるディスクとメモリは、同時ファイル アップロードの数とサイズによって異なります。 アプリであまり多くのアップロードをバッファーに格納しようとすると、メモリまたはディスク領域が不足したときにサイトがクラッシュします。 ファイルのアップロードのサイズまたは頻度によりアプリのリソースが不足する場合は、ストリーミングを使用します。

1 つで 64 KB を超えるバッファー ファイルは、メモリからディスク上の一時ファイルに移動されます。

より大きな要求の一時ファイルは、ASPNETCORE_TEMP 環境変数で指定された場所に書き込まれます。 ASPNETCORE_TEMP が定義されていない場合、ファイルは現在のユーザーの一時フォルダーに書き込まれます。

小さいファイルのバッファーリングについては、後のセクションで説明します。

ストリーミング

ファイルはマルチパート要求から受信され、アプリによって直接処理または保存されます。 ストリーミングによってパフォーマンスが大幅に向上することはありません。 ストリーミングを使用すると、ファイルをアップロードするときのメモリまたはディスク領域の需要を減らすことができます。

大きいファイルのストリーミングについては、「ストリーミングを使用して大きいファイルをアップロードする」で説明します。

バッファー モデル バインドを使用して小さいファイルを物理ストレージにアップロードする

小さいファイルをアップロードするには、マルチパート形式を使用するか、または JavaScript を使用して POST 要求を作成します。

次の例では、Razor Pages フォームを使用して 1 つのファイル (サンプル アプリの Pages/BufferedSingleFileUploadPhysical.cshtml) をアップロードする方法を示します。

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
            <span asp-validation-for="FileUpload.FormFile"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>

次の例は前の例と似ていますが、以下の点が異なります。

  • JavaScript の (Fetch API) を使用して、フォームのデータを送信します。
  • 検証は行われません。
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>

Fetch API がサポートされていないクライアントに対して JavaScript でフォーム POST を実行するには、次のいずれかの方法を使用します。

  • Fetch Polyfill を使用します (例: window.fetch polyfill (github/fetch))。

  • XMLHttpRequest を使用してください。 次に例を示します。

    <script>
      "use strict";
    
      function AJAXSubmit (oFormElement) {
        var oReq = new XMLHttpRequest();
        oReq.onload = function(e) { 
        oFormElement.elements.namedItem("result").value = 
          'Result: ' + this.status + ' ' + this.statusText;
        };
        oReq.open("post", oFormElement.action);
        oReq.send(new FormData(oFormElement));
      }
    </script>
    

ファイルのアップロードをサポートするには、HTML フォームで multipart/form-data のエンコード タイプ (enctype) を指定する必要があります。

files 入力要素で複数のファイルのアップロードをサポートするには、<input> 要素で multiple 属性を指定します。

<input asp-for="FileUpload.FormFiles" type="file" multiple>

サーバーにアップロードされた個々のファイルには、IFormFile を使用してモデル バインドでアクセスできます。 サンプル アプリでは、データベースおよび物理ストレージのシナリオでの複数のバッファー ファイル アップロードが示されています。

警告

IFormFileFileName プロパティは、表示とログ記録の目的以外に使用しないでください。 表示またはログ記録を行うときに、ファイル名を HTML エンコードします。 攻撃者は、完全パスまたは相対パスを含む悪意のあるファイル名を提供することがあります。 アプリケーションで次の処理を行う必要があります。

  • ユーザーが指定したファイル名からパスを削除します。
  • UI またはログ記録のために、HTML エンコードされ、パスが削除されたファイル名を保存します。
  • ストレージ用に新しいランダムなファイル名を生成します。

次のコードでは、ファイル名からパスを削除します。

string untrustedFileName = Path.GetFileName(pathName);

これまでに示した例では、セキュリティ上の考慮事項については考えられていません。 以下のセクションおよびサンプル アプリで、追加の情報が提供されています。

モデル バインドと IFormFile を使用してファイルをアップロードする場合、アクション メソッドでは以下を受け入れることができます。

注意

バインドでは、名前でフォーム ファイルが照合されます。 たとえば、HTML の <input type="file" name="formFile">name の値は、バインドされた C# のパラメーター/プロパティと一致する必要があります (FormFile)。 詳細については、「name 属性の値を POST メソッドのパラメーター名に一致させる」を参照してください。

次のような例です。

  • アップロードされた 1 つ以上のファイルをループします。
  • Path.GetTempFileName 使用して、ファイル名を含むファイルの完全なパスを返します。
  • アプリによって生成されたファイル名を使用して、ローカル ファイル システムにファイルを保存します。
  • アップロードされたファイルの合計数とサイズを返します。
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

パスを除いてファイル名を生成するには、Path.GetRandomFileName を使用します。 次の例では、構成からパスを取得します。

foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}

FileStream に渡すパスには、ファイル名が含まれている "必要があります"。 ファイル名を指定しないと、実行時に UnauthorizedAccessException がスローされます。

IFormFile の方法を使用してアップロードされたファイルは、処理の前に、サーバー上のメモリまたはディスクのバッファーに格納されます。 アクション メソッド内では、IFormFile の内容には Stream としてアクセスできます。 ローカル ファイル システムに加えて、ネットワーク共有またはファイル ストレージ サービス (Azure Blob Storage など) にファイルを保存することができます。

アップロードのために複数のファイルをループし、安全なファイル名を使用する別の例については、サンプル アプリの Pages/BufferedMultipleFileUploadPhysical.cshtml.cs を参照してください。

警告

以前の一時ファイルを削除せずに、65,535 個より多くのファイルを作成すると、Path.GetTempFileNameIOException がスローされます。 65,535 ファイルの制限は、サーバーごとの制限です。 Windows OS でのこの制限の詳細については、次のトピックの「解説」を参照してください。

バッファー モデル バインドを使用して小さいファイルをデータベースにアップロードする

Entity Framework を使用してデータベースにバイナリ ファイル データを格納するには、エンティティで Byte 配列プロパティを定義します。

public class AppFile
{
    public int Id { get; set; }
    public byte[] Content { get; set; }
}

IFormFile が含まれるクラスに対してページ モデル プロパティを指定します。

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

    [BindProperty]
    public BufferedSingleFileUploadDb FileUpload { get; set; }

    ...
}

public class BufferedSingleFileUploadDb
{
    [Required]
    [Display(Name="File")]
    public IFormFile FormFile { get; set; }
}

注意

IFormFile は、アクション メソッドのパラメーターとして直接、またはバインドされたモデル プロパティとして、使用することができます。 前の例では、バインドされたモデル プロパティが使用されています。

FileUpload は、Razor Pages フォームで使用されます。

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

フォームがサーバーに POST されるときに、IFormFile をストリームにコピーし、バイト配列としてデータベースに保存します。 次の例では、_dbContext によってアプリのデータベース コンテキストが格納されます。

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}

前の例は、次のサンプル アプリで示されているシナリオに似ています。

  • Pages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.cs

警告

パフォーマンスに悪影響を与える可能性があるため、リレーショナル データベースにバイナリ データを格納する場合は注意してください。

検証を行わずに IFormFileFileName プロパティに依存したり、信頼したりしないでください。 FileName プロパティは、表示目的でのみ、HTML エンコードした後でだけ、使用する必要があります。

示した例では、セキュリティ上の考慮事項については考えられていません。 以下のセクションおよびサンプル アプリで、追加の情報が提供されています。

ストリーミングを使用して大きいファイルをアップロードする

3.1 の例では、JavaScript を使用してファイルをコントローラーのアクションにストリーミングする方法を示します。 ファイルの偽造防止トークンは、カスタム フィルター属性を使用して生成され、要求本文ではなくクライアント HTTP ヘッダーに渡されます。 アクション メソッドではアップロードされたデータが直接処理されるため、フォーム モデル バインドは別のカスタム フィルターでは無効になります。 アクション内では、フォームのコンテンツが MultipartReader を使用して読み取られます。その場合、各 MultipartSection が読み取られ、必要に応じて、ファイルが処理されるかコンテンツが格納されます。 マルチパート セクションが読み取られた後、アクションで独自のモデル バインドが実行されます。

最初のページ応答ではフォームが読み込まれ、cookie に偽造防止トークンが保存されます (GenerateAntiforgeryTokenCookieAttribute 属性を使用)。 その属性では、ASP.NET Core の組み込みの偽造防止サポートを使用して、要求トークンで cookie が設定されます。

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

DisableFormValueModelBindingAttribute は、モデル バインドを無効にするために使用されます。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

サンプル アプリでは、GenerateAntiforgeryTokenCookieAttribute および DisableFormValueModelBindingAttribute は、Razor Pages の規則を使用して、Startup.ConfigureServices/StreamedSingleFileUploadDb および /StreamedSingleFileUploadPhysical のページ アプリケーション モデルにフィルターとして適用されます。

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
});

モデル バインドではフォームが読み取られないため、フォームからバインドされているパラメーターはバインドされません (クエリ、ルート、ヘッダーは引き続き機能します)。 アクション メソッドでは、Request プロパティが直接操作されます。 MultipartReader は各セクションを読み取るために使用されます。 キー/値データは KeyValueAccumulator に格納されます。 マルチパート セクションが読み取られた後、KeyValueAccumulator の内容を使用して、フォーム データがモデル タイプにバインドされます。

EF Core でデータベースにストリーミングするための完全な StreamingController.UploadDatabase メソッド:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}

物理的な場所にストリーミングするための完全な StreamingController.UploadPhysical メソッド:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}

サンプル アプリでは、検証チェックは FileHelpers.ProcessStreamedFile によって処理されます。

検証

サンプル アプリの FileHelpers クラスでは、バッファーリングされた IFormFile とストリーミングされたファイルのアップロードに関するいくつかのチェックが示されています。 サンプル アプリでのバッファーリングされたファイルのアップロード IFormFile の処理については、Utilities/FileHelpers.cs ファイルの ProcessFormFile メソッドを参照してください。 ストリーミングされたファイルの処理については、同じファイルの ProcessStreamedFile メソッドを参照してください。

警告

サンプル アプリで示されている検証処理メソッドでは、アップロードされたファイルの内容はスキャンされません。 ほとんどの運用シナリオでは、ファイルをユーザーまたは他のシステムで使用できるようにする前に、ウイルス/マルウェア スキャナー API が使用されます。

このトピックのサンプルでは検証技法の実際の例が示されていますが、次の場合を除き、運用アプリでは FileHelpers クラスを実装しないでください。

  • 実装を完全に理解している。
  • アプリの環境と仕様に合わせて実装が適切に変更されている。

これらの要件に対応することなく、アプリにセキュリティ コードをむやみに実装しないでください。

コンテンツの検証

アップロードされた内容に対してサードパーティ製のウイルス/マルウェア スキャン API を使用します。

大容量のシナリオでは、ファイルのスキャンに大量のサーバー リソースが必要です。 ファイルのスキャンによって要求の処理パフォーマンスが低下する場合は、スキャン処理をバックグラウンド サービスにオフロードすることを検討してください (たとえば、アプリのサーバーとは異なるサーバーで実行されているサービス)。 通常、アップロードされたファイルは、バックグラウンドのウイルス検索プログラムによってチェックされるまで、検疫された領域に保持されます。 合格したファイルは、通常のファイル ストレージの場所に移動されます。 これらの手順は、通常、ファイルのスキャン状態を示すデータベース レコードと共に実行されます。 このような方法を使用することで、アプリとアプリ サーバーは要求への応答に集中できます。

ファイル拡張子の検証

アップロードされたファイルの拡張子を、許可されている拡張子のリストで確認する必要があります。 次に例を示します。

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

ファイルのシグネチャの検証

ファイルのシグネチャは、ファイルの先頭にある最初の数バイトによって決定されます。 これらのバイトを使用して、拡張子がファイルの内容と一致するかどうかを示すことができます。 サンプル アプリでは、いくつかの一般的なファイルの種類についてファイルのシグネチャがチェックされています。 次の例では、JPEG イメージのファイルのシグネチャが、ファイルに対してチェックされています。

private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}

追加のファイル シグネチャを取得するには、「ファイル シグネチャ データベース (Google 検索結果)」および公式なファイルの仕様を使用してください。 公式なファイルの仕様を参照すると、選択したシグネチャが有効であることを確認できます。

ファイル名のセキュリティ

ファイルを物理ストレージに保存する場合は、クライアント指定のファイル名を使用しないでください。 Path.GetRandomFileName または Path.GetTempFileName を使用して、ファイルの安全なファイル名を作成し、一時ストレージの完全なパス (ファイル名を含む) を作成します。

Razor では、プロパティ値が表示用に自動的に HTML エンコードされます。 次のコードは安全に使用できます。

@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}

Razor の外部では、ユーザーの要求からのファイル名の内容を常に HtmlEncode でエンコードします。

多くの実装には、ファイルが存在するかどうかのチェックを含める必要があります。そうしないと、同じ名前のファイルによってファイルが上書きされます。 アプリの仕様を満たす追加のロジックを提供します。

サイズの検証

アップロードされるファイルのサイズを制限します。

サンプル アプリでは、ファイルのサイズは 2 MB (バイト単位) に制限されています。 その制限は、 appsettings.json ファイルの Configuration によって提供されます。

{
  "FileSizeLimit": 2097152
}

FileSizeLimitPageModel クラスに挿入されます。

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

    public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
    {
        _fileSizeLimit = config.GetValue<long>("FileSizeLimit");
    }

    ...
}

ファイルのサイズが制限を超えると、ファイルは拒否されます。

if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}

name 属性の値を POST メソッドのパラメーター名に一致させる

フォーム データを POST する、または JavaScript の FormData を直接使用する、Razor 以外のフォームでは、フォームの要素または FormData で指定されている名前が、コントローラーのアクションのパラメーターの name と一致している必要があります。

次の例では

  • <input> 要素を使用すると、name 属性には値 battlePlans が設定されます。

    <input type="file" name="battlePlans" multiple>
    
  • JavaScript で FormData を使用すると、name には値 battlePlans が設定されます。

    var formData = new FormData();
    
    for (var file in files) {
      formData.append("battlePlans", file, file.name);
    }
    

C# メソッドのパラメーターに一致する名前を使用します (battlePlans)。

  • Upload という名前の Razor Pages ページ ハンドラー メソッドの場合:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • MVC POST コントローラー アクション メソッドの場合:

    public async Task<IActionResult> Post(List<IFormFile> battlePlans)
    

サーバーとアプリの構成

マルチパート本文の長さの制限

MultipartBodyLengthLimit では、各マルチパート本文の長さの制限が設定されます。 この制限を超えるフォーム セクションでは、解析時に InvalidDataException がスローされます。 既定値は 134,217,728 (128 MB) です。 制限をカスタマイズするには、Startup.ConfigureServices の設定 MultipartBodyLengthLimit を使用します。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}

単一ページまたはアクションに対する MultipartBodyLengthLimit を設定するには、RequestFormLimitsAttribute を使用します。

Razor Pages アプリでは、Startup.ConfigureServicesconvention を使用してフィルターを適用します。

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model.Filters.Add(
                new RequestFormLimitsAttribute()
                {
                    // Set the limit to 256 MB
                    MultipartBodyLengthLimit = 268435456
                });
});

Razor Pages アプリまたは MVC アプリでは、ページ モデルまたはアクション メソッドにフィルターを適用します。

// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Kestrel の要求本文の最大サイズ

Kestrel によってホストされるアプリの場合、既定の要求本文の最大サイズは、30,000,000 バイトです。これは約 28.6 MB になります。 制限をカスタマイズするには、MaxRequestBodySizeKestrel サーバー オプションを使用します。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel((context, options) =>
            {
                // Handle requests up to 50 MB
                options.Limits.MaxRequestBodySize = 52428800;
            })
            .UseStartup<Startup>();
        });

単一ページまたはアクションに対する MaxRequestBodySize を設定するには、RequestSizeLimitAttribute を使用します。

Razor Pages アプリでは、Startup.ConfigureServicesconvention を使用してフィルターを適用します。

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model =>
            {
                // Handle requests up to 50 MB
                model.Filters.Add(
                    new RequestSizeLimitAttribute(52428800));
            });
});

Razor Pages アプリまたは MVC アプリでは、ページ ハンドラー クラスまたはアクション メソッドにフィルターを適用します。

// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

RequestSizeLimitAttribute は、@attributeRazor ディレクティブを使用して適用することもできます。

@attribute [RequestSizeLimitAttribute(52428800)]

Kestrel に関するその他の制限

Kestrel によってホストされるアプリには、Kestrel に関するその他の制限が適用される場合があります。

IIS

既定の要求の制限 (maxAllowedContentLength) は、30,000,000 バイトです。これは約 28.6 MB になります。 web.config ファイルで制限をカスタマイズします。 次の例では、制限が 50 MB (52,428,800 バイト) に設定されています。

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>

maxAllowedContentLength の設定は IIS にのみ適用されます。 詳細については、「要求制限 <requestLimits>」を参照してください。

トラブルシューティング

ファイルのアップロード時に発生する一般的ないくつかの問題と、考えられる解決策を以下に示します。

IIS サーバーに展開したときの "見つかりません" エラー

次のエラーは、アップロードされるファイルがサーバーの構成されている内容の長さを超えていることを示します。

HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.

詳細については、「IIS」セクションを参照してください。

接続エラー

接続エラーおよびサーバー接続のリセットは、アップロードされるファイルが Kestrel の最大要求本文サイズを超えていることを示している場合があります。 詳細については、「Kestrel の要求本文の最大サイズ」セクションを参照してください。 Kestrel クライアント接続の制限の調整も必要な場合があります。

IFormFile での null 参照例外

コントローラーが IFormFile を使用してアップロードされたファイルを受け取っても、値が null の場合は、HTML フォームで multipart/form-data に値 enctype が指定されていることを確認します。 この属性が <form> 要素で設定されていない場合、ファイルはアップロードされず、バインドされた IFormFile 引数は null になります。 また、フォーム データでのアップロードの名前がアプリの名前と一致することを確認します。

ストリームが長すぎる

このトピックの例は、アップロードされたファイル コンテンツを保持するために MemoryStream に依存しています。 int.MaxValue のサイズ制限は MemoryStream です。 アプリのファイル アップロード シナリオで 50 MB を超えるファイル コンテンツを保持する必要がある場合は、アップロードされたファイルのコンテンツを 1 つの MemoryStream に依存することなく保持する別のアプローチを使用します。

ASP.NET Core では、小さいファイルの場合はバッファー モデル バインドを使用し、大きいファイルの場合は非バッファー ストリーミングを使用して、1 つ以上のファイルのアップロードがサポートされています。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

セキュリティに関する考慮事項

サーバーにファイルをアップロードする機能をユーザーに提供するときは、十分に注意してください。 攻撃者が次のようなことを試みる可能性があります。

  • サービス拒否攻撃を実行する。
  • ウイルスまたはマルウェアをアップロードする。
  • ネットワークやサーバーを他の方法で侵害する。

攻撃の成功の可能性を少なくするセキュリティ手順は、次のとおりです。

  • 専用のファイル アップロード領域 (できれば、システム ドライブ以外) にファイルをアップロードします。 専用の場所を使用すると、アップロードされるファイルにセキュリティ制限を適用しやすくなります。 ファイルのアップロード場所に対する実行アクセス許可を無効にします。†
  • アプリと同じディレクトリ ツリーに、アップロードしたファイルを保持しないでください。†
  • アプリによって決められた安全なファイル名を使用します。 ユーザーによって指定されたファイル名や、アップロードされるファイルの信頼されていないファイル名は、使用しないでください。† 信頼されていないファイル名を表示時に HTML エンコードします。 たとえば、ファイル名をログに記録したり、UI に表示したりします (Razor では、出力が自動的に HTML エンコードされます)。
  • アプリの設計仕様に対して承認されているファイル拡張子のみを許可します。†
  • クライアント側のチェックがサーバーで実行されることを確認します。† クライアント側のチェックは簡単に回避できます。
  • アップロードされたファイルのサイズをチェックします。 サイズの大きなアップロードを防ぐために、最大サイズ制限を設定します。†
  • 同じ名前でアップロードされたファイルによってファイルが上書きされないようにする必要があるときは、ファイルをアップロードする前に、データベースまたは物理ストレージに対してファイル名を確認します。
  • ファイルを格納する前に、アップロードされる内容に対してウイルス/マルウェア スキャナーを実行します。

† サンプル アプリで、条件を満たす方法が示されています。

警告

システムへの悪意のあるコードのアップロードは、頻繁に次のような内容のコードの実行するための足がかりとなります。

  • システムの制御を完全に掌握する。
  • システムがクラッシュする結果で、システムを過負荷状態にする。
  • ユーザーまたはシステムのデータを破壊する。
  • パブリック UI に落書きする。

ユーザーからファイルを受け入れる際の外部アクセスによる攻撃を減らす方法については、次の資料を参照してください。

サンプル アプリの例など、セキュリティ対策の実装の詳細については、「検証」セクションを参照してください。

ストレージのシナリオ

ファイルの一般的なストレージ オプションには次のようなものがあります。

  • データベース

    • 小さいファイルをアップロードする場合、物理ストレージ (ファイル システムまたはネットワーク共有) のオプションよりデータベースの方が速いことがよくあります。
    • 多くの場合、ユーザー データに対するデータベース レコードの取得でファイルの内容を同時に提供できるため (たとえば、アバター イメージ)、データベースの方が物理的なストレージ オプションより便利です。
    • データベースは、データ ストレージ サービスを使用するよりコストが低くなる可能性があります。
  • 物理ストレージ (ファイル システムまたはネットワーク共有)

    • 大きいファイルのアップロードの場合:
      • データベースの制限によって、アップロードのサイズが制限される場合があります。
      • 多くの場合、物理ストレージはデータベース内のストレージより高コストです。
    • 物理ストレージは、データ ストレージ サービスを使用するよりコストが低くなる可能性があります。
    • アプリのプロセスには、ストレージの場所に対する読み取りと書き込みのアクセス許可が必要です。 実行アクセス許可は付与しないでください。
  • データ ストレージ サービス (例: Azure Blob Storage)

    • 通常、サービスでは、大抵の場合に単一障害点となるオンプレミス ソリューションより高いスケーラビリティと回復性が提供されます。
    • 大規模なストレージ インフラストラクチャのシナリオでは、サービスのコストが低下する可能性があります。

    詳しくは、.NET を使用してオブジェクト ストレージに BLOB を作成するクイックスタートに関する記事をご覧ください。

ファイル アップロードのシナリオ

ファイルをアップロードするための一般的な 2 つの方法は、バッファーリングとストリーミングです。

バッファリング

ファイル全体が IFormFile に読み込まれます。これは、ファイルの処理または保存に使用される C# でのファイルの表現です。

ファイルのアップロードで使用されるリソース (ディスク、メモリM) は、同時ファイル アップロードの数とサイズによって異なります。 アプリであまり多くのアップロードをバッファーに格納しようとすると、メモリまたはディスク領域が不足したときにサイトがクラッシュします。 ファイルのアップロードのサイズまたは頻度によりアプリのリソースが不足する場合は、ストリーミングを使用します。

注意

1 つで 64 KB を超えるバッファー ファイルは、メモリからディスク上の一時ファイルに移動されます。

小さいファイルのバッファーリングについては、後のセクションで説明します。

ストリーミング

ファイルはマルチパート要求から受信され、アプリによって直接処理または保存されます。 ストリーミングによってパフォーマンスが大幅に向上することはありません。 ストリーミングを使用すると、ファイルをアップロードするときのメモリまたはディスク領域の需要を減らすことができます。

大きいファイルのストリーミングについては、「ストリーミングを使用して大きいファイルをアップロードする」で説明します。

バッファー モデル バインドを使用して小さいファイルを物理ストレージにアップロードする

小さいファイルをアップロードするには、マルチパート形式を使用するか、または JavaScript を使用して POST 要求を作成します。

次の例では、Razor Pages フォームを使用して 1 つのファイル (サンプル アプリの Pages/BufferedSingleFileUploadPhysical.cshtml) をアップロードする方法を示します。

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
            <span asp-validation-for="FileUpload.FormFile"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>

次の例は前の例と似ていますが、以下の点が異なります。

  • JavaScript の (Fetch API) を使用して、フォームのデータを送信します。
  • 検証は行われません。
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>

Fetch API がサポートされていないクライアントに対して JavaScript でフォーム POST を実行するには、次のいずれかの方法を使用します。

  • Fetch Polyfill を使用します (例: window.fetch polyfill (github/fetch))。

  • XMLHttpRequest を使用してください。 次に例を示します。

    <script>
      "use strict";
    
      function AJAXSubmit (oFormElement) {
        var oReq = new XMLHttpRequest();
        oReq.onload = function(e) { 
        oFormElement.elements.namedItem("result").value = 
          'Result: ' + this.status + ' ' + this.statusText;
        };
        oReq.open("post", oFormElement.action);
        oReq.send(new FormData(oFormElement));
      }
    </script>
    

ファイルのアップロードをサポートするには、HTML フォームで multipart/form-data のエンコード タイプ (enctype) を指定する必要があります。

files 入力要素で複数のファイルのアップロードをサポートするには、<input> 要素で multiple 属性を指定します。

<input asp-for="FileUpload.FormFiles" type="file" multiple>

サーバーにアップロードされた個々のファイルには、IFormFile を使用してモデル バインドでアクセスできます。 サンプル アプリでは、データベースおよび物理ストレージのシナリオでの複数のバッファー ファイル アップロードが示されています。

警告

IFormFileFileName プロパティは、表示とログ記録の目的以外に使用しないでください。 表示またはログ記録を行うときに、ファイル名を HTML エンコードします。 攻撃者は、完全パスまたは相対パスを含む悪意のあるファイル名を提供することがあります。 アプリケーションで次の処理を行う必要があります。

  • ユーザーが指定したファイル名からパスを削除します。
  • UI またはログ記録のために、HTML エンコードされ、パスが削除されたファイル名を保存します。
  • ストレージ用に新しいランダムなファイル名を生成します。

次のコードでは、ファイル名からパスを削除します。

string untrustedFileName = Path.GetFileName(pathName);

これまでに示した例では、セキュリティ上の考慮事項については考えられていません。 以下のセクションおよびサンプル アプリで、追加の情報が提供されています。

モデル バインドと IFormFile を使用してファイルをアップロードする場合、アクション メソッドでは以下を受け入れることができます。

注意

バインドでは、名前でフォーム ファイルが照合されます。 たとえば、HTML の <input type="file" name="formFile">name の値は、バインドされた C# のパラメーター/プロパティと一致する必要があります (FormFile)。 詳細については、「name 属性の値を POST メソッドのパラメーター名に一致させる」を参照してください。

次のような例です。

  • アップロードされた 1 つ以上のファイルをループします。
  • Path.GetTempFileName 使用して、ファイル名を含むファイルの完全なパスを返します。
  • アプリによって生成されたファイル名を使用して、ローカル ファイル システムにファイルを保存します。
  • アップロードされたファイルの合計数とサイズを返します。
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

パスを除いてファイル名を生成するには、Path.GetRandomFileName を使用します。 次の例では、構成からパスを取得します。

foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}

FileStream に渡すパスには、ファイル名が含まれている "必要があります"。 ファイル名を指定しないと、実行時に UnauthorizedAccessException がスローされます。

IFormFile の方法を使用してアップロードされたファイルは、処理の前に、サーバー上のメモリまたはディスクのバッファーに格納されます。 アクション メソッド内では、IFormFile の内容には Stream としてアクセスできます。 ローカル ファイル システムに加えて、ネットワーク共有またはファイル ストレージ サービス (Azure Blob Storage など) にファイルを保存することができます。

アップロードのために複数のファイルをループし、安全なファイル名を使用する別の例については、サンプル アプリの Pages/BufferedMultipleFileUploadPhysical.cshtml.cs を参照してください。

警告

以前の一時ファイルを削除せずに、65,535 個より多くのファイルを作成すると、Path.GetTempFileNameIOException がスローされます。 65,535 ファイルの制限は、サーバーごとの制限です。 Windows OS でのこの制限の詳細については、次のトピックの「解説」を参照してください。

バッファー モデル バインドを使用して小さいファイルをデータベースにアップロードする

Entity Framework を使用してデータベースにバイナリ ファイル データを格納するには、エンティティで Byte 配列プロパティを定義します。

public class AppFile
{
    public int Id { get; set; }
    public byte[] Content { get; set; }
}

IFormFile が含まれるクラスに対してページ モデル プロパティを指定します。

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

    [BindProperty]
    public BufferedSingleFileUploadDb FileUpload { get; set; }

    ...
}

public class BufferedSingleFileUploadDb
{
    [Required]
    [Display(Name="File")]
    public IFormFile FormFile { get; set; }
}

注意

IFormFile は、アクション メソッドのパラメーターとして直接、またはバインドされたモデル プロパティとして、使用することができます。 前の例では、バインドされたモデル プロパティが使用されています。

FileUpload は、Razor Pages フォームで使用されます。

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

フォームがサーバーに POST されるときに、IFormFile をストリームにコピーし、バイト配列としてデータベースに保存します。 次の例では、_dbContext によってアプリのデータベース コンテキストが格納されます。

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}

前の例は、次のサンプル アプリで示されているシナリオに似ています。

  • Pages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.cs

警告

パフォーマンスに悪影響を与える可能性があるため、リレーショナル データベースにバイナリ データを格納する場合は注意してください。

検証を行わずに IFormFileFileName プロパティに依存したり、信頼したりしないでください。 FileName プロパティは、表示目的でのみ、HTML エンコードした後でだけ、使用する必要があります。

示した例では、セキュリティ上の考慮事項については考えられていません。 以下のセクションおよびサンプル アプリで、追加の情報が提供されています。

ストリーミングを使用して大きいファイルをアップロードする

次の例では、JavaScript を使用してファイルをコントローラーのアクションにストリーミングする方法を示します。 ファイルの偽造防止トークンは、カスタム フィルター属性を使用して生成され、要求本文ではなくクライアント HTTP ヘッダーに渡されます。 アクション メソッドではアップロードされたデータが直接処理されるため、フォーム モデル バインドは別のカスタム フィルターでは無効になります。 アクション内では、フォームのコンテンツが MultipartReader を使用して読み取られます。その場合、各 MultipartSection が読み取られ、必要に応じて、ファイルが処理されるかコンテンツが格納されます。 マルチパート セクションが読み取られた後、アクションで独自のモデル バインドが実行されます。

最初のページ応答ではフォームが読み込まれ、cookie に偽造防止トークンが保存されます (GenerateAntiforgeryTokenCookieAttribute 属性を使用)。 その属性では、ASP.NET Core の組み込みの偽造防止サポートを使用して、要求トークンで cookie が設定されます。

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

DisableFormValueModelBindingAttribute は、モデル バインドを無効にするために使用されます。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

サンプル アプリでは、GenerateAntiforgeryTokenCookieAttribute および DisableFormValueModelBindingAttribute は、Razor Pages の規則を使用して、Startup.ConfigureServices/StreamedSingleFileUploadDb および /StreamedSingleFileUploadPhysical のページ アプリケーション モデルにフィルターとして適用されます。

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
});

モデル バインドではフォームが読み取られないため、フォームからバインドされているパラメーターはバインドされません (クエリ、ルート、ヘッダーは引き続き機能します)。 アクション メソッドでは、Request プロパティが直接操作されます。 MultipartReader は各セクションを読み取るために使用されます。 キー/値データは KeyValueAccumulator に格納されます。 マルチパート セクションが読み取られた後、KeyValueAccumulator の内容を使用して、フォーム データがモデル タイプにバインドされます。

EF Core でデータベースにストリーミングするための完全な StreamingController.UploadDatabase メソッド:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}

物理的な場所にストリーミングするための完全な StreamingController.UploadPhysical メソッド:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}

サンプル アプリでは、検証チェックは FileHelpers.ProcessStreamedFile によって処理されます。

検証

サンプル アプリの FileHelpers クラスでは、バッファーリングされた IFormFile とストリーミングされたファイルのアップロードに関するいくつかのチェックが示されています。 サンプル アプリでのバッファーリングされたファイルのアップロード IFormFile の処理については、Utilities/FileHelpers.cs ファイルの ProcessFormFile メソッドを参照してください。 ストリーミングされたファイルの処理については、同じファイルの ProcessStreamedFile メソッドを参照してください。

警告

サンプル アプリで示されている検証処理メソッドでは、アップロードされたファイルの内容はスキャンされません。 ほとんどの運用シナリオでは、ファイルをユーザーまたは他のシステムで使用できるようにする前に、ウイルス/マルウェア スキャナー API が使用されます。

このトピックのサンプルでは検証技法の実際の例が示されていますが、次の場合を除き、運用アプリでは FileHelpers クラスを実装しないでください。

  • 実装を完全に理解している。
  • アプリの環境と仕様に合わせて実装が適切に変更されている。

これらの要件に対応することなく、アプリにセキュリティ コードをむやみに実装しないでください。

コンテンツの検証

アップロードされた内容に対してサードパーティ製のウイルス/マルウェア スキャン API を使用します。

大容量のシナリオでは、ファイルのスキャンに大量のサーバー リソースが必要です。 ファイルのスキャンによって要求の処理パフォーマンスが低下する場合は、スキャン処理をバックグラウンド サービスにオフロードすることを検討してください (たとえば、アプリのサーバーとは異なるサーバーで実行されているサービス)。 通常、アップロードされたファイルは、バックグラウンドのウイルス検索プログラムによってチェックされるまで、検疫された領域に保持されます。 合格したファイルは、通常のファイル ストレージの場所に移動されます。 これらの手順は、通常、ファイルのスキャン状態を示すデータベース レコードと共に実行されます。 このような方法を使用することで、アプリとアプリ サーバーは要求への応答に集中できます。

ファイル拡張子の検証

アップロードされたファイルの拡張子を、許可されている拡張子のリストで確認する必要があります。 次に例を示します。

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

ファイルのシグネチャの検証

ファイルのシグネチャは、ファイルの先頭にある最初の数バイトによって決定されます。 これらのバイトを使用して、拡張子がファイルの内容と一致するかどうかを示すことができます。 サンプル アプリでは、いくつかの一般的なファイルの種類についてファイルのシグネチャがチェックされています。 次の例では、JPEG イメージのファイルのシグネチャが、ファイルに対してチェックされています。

private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}

追加のファイル シグネチャを取得するには、「ファイル シグネチャ データベース (Google 検索結果)」および公式なファイルの仕様を使用してください。 公式なファイルの仕様を参照すると、選択したシグネチャが有効であることを確認できます。

ファイル名のセキュリティ

ファイルを物理ストレージに保存する場合は、クライアント指定のファイル名を使用しないでください。 Path.GetRandomFileName または Path.GetTempFileName を使用して、ファイルの安全なファイル名を作成し、一時ストレージの完全なパス (ファイル名を含む) を作成します。

Razor では、プロパティ値が表示用に自動的に HTML エンコードされます。 次のコードは安全に使用できます。

@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}

Razor の外部では、ユーザーの要求からのファイル名の内容を常に HtmlEncode でエンコードします。

多くの実装には、ファイルが存在するかどうかのチェックを含める必要があります。そうしないと、同じ名前のファイルによってファイルが上書きされます。 アプリの仕様を満たす追加のロジックを提供します。

サイズの検証

アップロードされるファイルのサイズを制限します。

サンプル アプリでは、ファイルのサイズは 2 MB (バイト単位) に制限されています。 その制限は、 appsettings.json ファイルの Configuration によって提供されます。

{
  "FileSizeLimit": 2097152
}

FileSizeLimitPageModel クラスに挿入されます。

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

    public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
    {
        _fileSizeLimit = config.GetValue<long>("FileSizeLimit");
    }

    ...
}

ファイルのサイズが制限を超えると、ファイルは拒否されます。

if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}

name 属性の値を POST メソッドのパラメーター名に一致させる

フォーム データを POST する、または JavaScript の FormData を直接使用する、Razor 以外のフォームでは、フォームの要素または FormData で指定されている名前が、コントローラーのアクションのパラメーターの name と一致している必要があります。

次の例では

  • <input> 要素を使用すると、name 属性には値 battlePlans が設定されます。

    <input type="file" name="battlePlans" multiple>
    
  • JavaScript で FormData を使用すると、name には値 battlePlans が設定されます。

    var formData = new FormData();
    
    for (var file in files) {
      formData.append("battlePlans", file, file.name);
    }
    

C# メソッドのパラメーターに一致する名前を使用します (battlePlans)。

  • Upload という名前の Razor Pages ページ ハンドラー メソッドの場合:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • MVC POST コントローラー アクション メソッドの場合:

    public async Task<IActionResult> Post(List<IFormFile> battlePlans)
    

サーバーとアプリの構成

マルチパート本文の長さの制限

MultipartBodyLengthLimit では、各マルチパート本文の長さの制限が設定されます。 この制限を超えるフォーム セクションでは、解析時に InvalidDataException がスローされます。 既定値は 134,217,728 (128 MB) です。 制限をカスタマイズするには、Startup.ConfigureServices の設定 MultipartBodyLengthLimit を使用します。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}

単一ページまたはアクションに対する MultipartBodyLengthLimit を設定するには、RequestFormLimitsAttribute を使用します。

Razor Pages アプリでは、Startup.ConfigureServicesconvention を使用してフィルターを適用します。

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model.Filters.Add(
                new RequestFormLimitsAttribute()
                {
                    // Set the limit to 256 MB
                    MultipartBodyLengthLimit = 268435456
                });
});

Razor Pages アプリまたは MVC アプリでは、ページ モデルまたはアクション メソッドにフィルターを適用します。

// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Kestrel の要求本文の最大サイズ

Kestrel によってホストされるアプリの場合、既定の要求本文の最大サイズは、30,000,000 バイトです。これは約 28.6 MB になります。 制限をカスタマイズするには、MaxRequestBodySizeKestrel サーバー オプションを使用します。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel((context, options) =>
            {
                // Handle requests up to 50 MB
                options.Limits.MaxRequestBodySize = 52428800;
            })
            .UseStartup<Startup>();
        });

単一ページまたはアクションに対する MaxRequestBodySize を設定するには、RequestSizeLimitAttribute を使用します。

Razor Pages アプリでは、Startup.ConfigureServicesconvention を使用してフィルターを適用します。

services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model =>
            {
                // Handle requests up to 50 MB
                model.Filters.Add(
                    new RequestSizeLimitAttribute(52428800));
            });
});

Razor Pages アプリまたは MVC アプリでは、ページ ハンドラー クラスまたはアクション メソッドにフィルターを適用します。

// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

RequestSizeLimitAttribute は、@attributeRazor ディレクティブを使用して適用することもできます。

@attribute [RequestSizeLimitAttribute(52428800)]

Kestrel に関するその他の制限

Kestrel によってホストされるアプリには、Kestrel に関するその他の制限が適用される場合があります。

IIS

既定の要求の制限 (maxAllowedContentLength) は、30,000,000 バイトです。これは約 28.6 MB になります。 web.config ファイルで制限をカスタマイズします。 次の例では、制限が 50 MB (52,428,800 バイト) に設定されています。

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>

maxAllowedContentLength の設定は IIS にのみ適用されます。 詳細については、「要求制限 <requestLimits>」を参照してください。

Startup.ConfigureServicesIISServerOptions.MaxRequestBodySize を設定して、HTTP 要求の最大要求本文サイズを増やします。 次の例では、制限が 50 MB (52,428,800 バイト) に設定されています。

services.Configure<IISServerOptions>(options =>
{
    options.MaxRequestBodySize = 52428800;
});

詳細については、「IIS を使用した Windows での ASP.NET Core のホスト」を参照してください。

トラブルシューティング

ファイルのアップロード時に発生する一般的ないくつかの問題と、考えられる解決策を以下に示します。

IIS サーバーに展開したときの "見つかりません" エラー

次のエラーは、アップロードされるファイルがサーバーの構成されている内容の長さを超えていることを示します。

HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.

詳細については、「IIS」セクションを参照してください。

接続エラー

接続エラーおよびサーバー接続のリセットは、アップロードされるファイルが Kestrel の最大要求本文サイズを超えていることを示している場合があります。 詳細については、「Kestrel の要求本文の最大サイズ」セクションを参照してください。 Kestrel クライアント接続の制限の調整も必要な場合があります。

IFormFile での null 参照例外

コントローラーが IFormFile を使用してアップロードされたファイルを受け取っても、値が null の場合は、HTML フォームで multipart/form-data に値 enctype が指定されていることを確認します。 この属性が <form> 要素で設定されていない場合、ファイルはアップロードされず、バインドされた IFormFile 引数は null になります。 また、フォーム データでのアップロードの名前がアプリの名前と一致することを確認します。

ストリームが長すぎる

このトピックの例は、アップロードされたファイル コンテンツを保持するために MemoryStream に依存しています。 int.MaxValue のサイズ制限は MemoryStream です。 アプリのファイル アップロード シナリオで 50 MB を超えるファイル コンテンツを保持する必要がある場合は、アップロードされたファイルのコンテンツを 1 つの MemoryStream に依存することなく保持する別のアプローチを使用します。

ASP.NET Core では、小さいファイルの場合はバッファー モデル バインドを使用し、大きいファイルの場合は非バッファー ストリーミングを使用して、1 つ以上のファイルのアップロードがサポートされています。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

セキュリティに関する考慮事項

サーバーにファイルをアップロードする機能をユーザーに提供するときは、十分に注意してください。 攻撃者が次のようなことを試みる可能性があります。

  • サービス拒否攻撃を実行する。
  • ウイルスまたはマルウェアをアップロードする。
  • ネットワークやサーバーを他の方法で侵害する。

攻撃の成功の可能性を少なくするセキュリティ手順は、次のとおりです。

  • 専用のファイル アップロード領域 (できれば、システム ドライブ以外) にファイルをアップロードします。 専用の場所を使用すると、アップロードされるファイルにセキュリティ制限を適用しやすくなります。 ファイルのアップロード場所に対する実行アクセス許可を無効にします。†
  • アプリと同じディレクトリ ツリーに、アップロードしたファイルを保持しないでください。†
  • アプリによって決められた安全なファイル名を使用します。 ユーザーによって指定されたファイル名や、アップロードされるファイルの信頼されていないファイル名は、使用しないでください。† 信頼されていないファイル名を表示時に HTML エンコードします。 たとえば、ファイル名をログに記録したり、UI に表示したりします (Razor では、出力が自動的に HTML エンコードされます)。
  • アプリの設計仕様に対して承認されているファイル拡張子のみを許可します。†
  • クライアント側のチェックがサーバーで実行されることを確認します。† クライアント側のチェックは簡単に回避できます。
  • アップロードされたファイルのサイズをチェックします。 サイズの大きなアップロードを防ぐために、最大サイズ制限を設定します。†
  • 同じ名前でアップロードされたファイルによってファイルが上書きされないようにする必要があるときは、ファイルをアップロードする前に、データベースまたは物理ストレージに対してファイル名を確認します。
  • ファイルを格納する前に、アップロードされる内容に対してウイルス/マルウェア スキャナーを実行します。

† サンプル アプリで、条件を満たす方法が示されています。

警告

システムへの悪意のあるコードのアップロードは、頻繁に次のような内容のコードの実行するための足がかりとなります。

  • システムの制御を完全に掌握する。
  • システムがクラッシュする結果で、システムを過負荷状態にする。
  • ユーザーまたはシステムのデータを破壊する。
  • パブリック UI に落書きする。

ユーザーからファイルを受け入れる際の外部アクセスによる攻撃を減らす方法については、次の資料を参照してください。

サンプル アプリの例など、セキュリティ対策の実装の詳細については、「検証」セクションを参照してください。

ストレージのシナリオ

ファイルの一般的なストレージ オプションには次のようなものがあります。

  • データベース

    • 小さいファイルをアップロードする場合、物理ストレージ (ファイル システムまたはネットワーク共有) のオプションよりデータベースの方が速いことがよくあります。
    • 多くの場合、ユーザー データに対するデータベース レコードの取得でファイルの内容を同時に提供できるため (たとえば、アバター イメージ)、データベースの方が物理的なストレージ オプションより便利です。
    • データベースは、データ ストレージ サービスを使用するよりコストが低くなる可能性があります。
  • 物理ストレージ (ファイル システムまたはネットワーク共有)

    • 大きいファイルのアップロードの場合:
      • データベースの制限によって、アップロードのサイズが制限される場合があります。
      • 多くの場合、物理ストレージはデータベース内のストレージより高コストです。
    • 物理ストレージは、データ ストレージ サービスを使用するよりコストが低くなる可能性があります。
    • アプリのプロセスには、ストレージの場所に対する読み取りと書き込みのアクセス許可が必要です。 実行アクセス許可は付与しないでください。
  • データ ストレージ サービス (例: Azure Blob Storage)

    • 通常、サービスでは、大抵の場合に単一障害点となるオンプレミス ソリューションより高いスケーラビリティと回復性が提供されます。
    • 大規模なストレージ インフラストラクチャのシナリオでは、サービスのコストが低下する可能性があります。

    詳しくは、.NET を使用してオブジェクト ストレージに BLOB を作成するクイックスタートに関する記事をご覧ください。 そのトピックでは UploadFromFileAsync が示されていますが、Stream を使用する場合は、UploadFromStreamAsync を使用して FileStream を Blob Storage に保存することもできます。

ファイル アップロードのシナリオ

ファイルをアップロードするための一般的な 2 つの方法は、バッファーリングとストリーミングです。

バッファリング

ファイル全体が IFormFile に読み込まれます。これは、ファイルの処理または保存に使用される C# でのファイルの表現です。

ファイルのアップロードで使用されるリソース (ディスク、メモリM) は、同時ファイル アップロードの数とサイズによって異なります。 アプリであまり多くのアップロードをバッファーに格納しようとすると、メモリまたはディスク領域が不足したときにサイトがクラッシュします。 ファイルのアップロードのサイズまたは頻度によりアプリのリソースが不足する場合は、ストリーミングを使用します。

注意

1 つで 64 KB を超えるバッファー ファイルは、メモリからディスク上の一時ファイルに移動されます。

小さいファイルのバッファーリングについては、後のセクションで説明します。

ストリーミング

ファイルはマルチパート要求から受信され、アプリによって直接処理または保存されます。 ストリーミングによってパフォーマンスが大幅に向上することはありません。 ストリーミングを使用すると、ファイルをアップロードするときのメモリまたはディスク領域の需要を減らすことができます。

大きいファイルのストリーミングについては、「ストリーミングを使用して大きいファイルをアップロードする」で説明します。

バッファー モデル バインドを使用して小さいファイルを物理ストレージにアップロードする

小さいファイルをアップロードするには、マルチパート形式を使用するか、または JavaScript を使用して POST 要求を作成します。

次の例では、Razor Pages フォームを使用して 1 つのファイル (サンプル アプリの Pages/BufferedSingleFileUploadPhysical.cshtml) をアップロードする方法を示します。

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
            <span asp-validation-for="FileUpload.FormFile"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>

次の例は前の例と似ていますが、以下の点が異なります。

  • JavaScript の (Fetch API) を使用して、フォームのデータを送信します。
  • 検証は行われません。
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>

Fetch API がサポートされていないクライアントに対して JavaScript でフォーム POST を実行するには、次のいずれかの方法を使用します。

  • Fetch Polyfill を使用します (例: window.fetch polyfill (github/fetch))。

  • XMLHttpRequest を使用してください。 次に例を示します。

    <script>
      "use strict";
    
      function AJAXSubmit (oFormElement) {
        var oReq = new XMLHttpRequest();
        oReq.onload = function(e) { 
        oFormElement.elements.namedItem("result").value = 
          'Result: ' + this.status + ' ' + this.statusText;
        };
        oReq.open("post", oFormElement.action);
        oReq.send(new FormData(oFormElement));
      }
    </script>
    

ファイルのアップロードをサポートするには、HTML フォームで multipart/form-data のエンコード タイプ (enctype) を指定する必要があります。

files 入力要素で複数のファイルのアップロードをサポートするには、<input> 要素で multiple 属性を指定します。

<input asp-for="FileUpload.FormFiles" type="file" multiple>

サーバーにアップロードされた個々のファイルには、IFormFile を使用してモデル バインドでアクセスできます。 サンプル アプリでは、データベースおよび物理ストレージのシナリオでの複数のバッファー ファイル アップロードが示されています。

警告

IFormFileFileName プロパティは、表示とログ記録の目的以外に使用しないでください。 表示またはログ記録を行うときに、ファイル名を HTML エンコードします。 攻撃者は、完全パスまたは相対パスを含む悪意のあるファイル名を提供することがあります。 アプリケーションで次の処理を行う必要があります。

  • ユーザーが指定したファイル名からパスを削除します。
  • UI またはログ記録のために、HTML エンコードされ、パスが削除されたファイル名を保存します。
  • ストレージ用に新しいランダムなファイル名を生成します。

次のコードでは、ファイル名からパスを削除します。

string untrustedFileName = Path.GetFileName(pathName);

これまでに示した例では、セキュリティ上の考慮事項については考えられていません。 以下のセクションおよびサンプル アプリで、追加の情報が提供されています。

モデル バインドと IFormFile を使用してファイルをアップロードする場合、アクション メソッドでは以下を受け入れることができます。

注意

バインドでは、名前でフォーム ファイルが照合されます。 たとえば、HTML の <input type="file" name="formFile">name の値は、バインドされた C# のパラメーター/プロパティと一致する必要があります (FormFile)。 詳細については、「name 属性の値を POST メソッドのパラメーター名に一致させる」を参照してください。

次のような例です。

  • アップロードされた 1 つ以上のファイルをループします。
  • Path.GetTempFileName 使用して、ファイル名を含むファイルの完全なパスを返します。
  • アプリによって生成されたファイル名を使用して、ローカル ファイル システムにファイルを保存します。
  • アップロードされたファイルの合計数とサイズを返します。
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

パスを除いてファイル名を生成するには、Path.GetRandomFileName を使用します。 次の例では、構成からパスを取得します。

foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}

FileStream に渡すパスには、ファイル名が含まれている "必要があります"。 ファイル名を指定しないと、実行時に UnauthorizedAccessException がスローされます。

IFormFile の方法を使用してアップロードされたファイルは、処理の前に、サーバー上のメモリまたはディスクのバッファーに格納されます。 アクション メソッド内では、IFormFile の内容には Stream としてアクセスできます。 ローカル ファイル システムに加えて、ネットワーク共有またはファイル ストレージ サービス (Azure Blob Storage など) にファイルを保存することができます。

アップロードのために複数のファイルをループし、安全なファイル名を使用する別の例については、サンプル アプリの Pages/BufferedMultipleFileUploadPhysical.cshtml.cs を参照してください。

警告

以前の一時ファイルを削除せずに、65,535 個より多くのファイルを作成すると、Path.GetTempFileNameIOException がスローされます。 65,535 ファイルの制限は、サーバーごとの制限です。 Windows OS でのこの制限の詳細については、次のトピックの「解説」を参照してください。

バッファー モデル バインドを使用して小さいファイルをデータベースにアップロードする

Entity Framework を使用してデータベースにバイナリ ファイル データを格納するには、エンティティで Byte 配列プロパティを定義します。

public class AppFile
{
    public int Id { get; set; }
    public byte[] Content { get; set; }
}

IFormFile が含まれるクラスに対してページ モデル プロパティを指定します。

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

    [BindProperty]
    public BufferedSingleFileUploadDb FileUpload { get; set; }

    ...
}

public class BufferedSingleFileUploadDb
{
    [Required]
    [Display(Name="File")]
    public IFormFile FormFile { get; set; }
}

注意

IFormFile は、アクション メソッドのパラメーターとして直接、またはバインドされたモデル プロパティとして、使用することができます。 前の例では、バインドされたモデル プロパティが使用されています。

FileUpload は、Razor Pages フォームで使用されます。

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

フォームがサーバーに POST されるときに、IFormFile をストリームにコピーし、バイト配列としてデータベースに保存します。 次の例では、_dbContext によってアプリのデータベース コンテキストが格納されます。

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}

前の例は、次のサンプル アプリで示されているシナリオに似ています。

  • Pages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.cs

警告

パフォーマンスに悪影響を与える可能性があるため、リレーショナル データベースにバイナリ データを格納する場合は注意してください。

検証を行わずに IFormFileFileName プロパティに依存したり、信頼したりしないでください。 FileName プロパティは、表示目的でのみ、HTML エンコードした後でだけ、使用する必要があります。

示した例では、セキュリティ上の考慮事項については考えられていません。 以下のセクションおよびサンプル アプリで、追加の情報が提供されています。

ストリーミングを使用して大きいファイルをアップロードする

次の例では、JavaScript を使用してファイルをコントローラーのアクションにストリーミングする方法を示します。 ファイルの偽造防止トークンは、カスタム フィルター属性を使用して生成され、要求本文ではなくクライアント HTTP ヘッダーに渡されます。 アクション メソッドではアップロードされたデータが直接処理されるため、フォーム モデル バインドは別のカスタム フィルターでは無効になります。 アクション内では、フォームのコンテンツが MultipartReader を使用して読み取られます。その場合、各 MultipartSection が読み取られ、必要に応じて、ファイルが処理されるかコンテンツが格納されます。 マルチパート セクションが読み取られた後、アクションで独自のモデル バインドが実行されます。

最初のページ応答ではフォームが読み込まれ、cookie に偽造防止トークンが保存されます (GenerateAntiforgeryTokenCookieAttribute 属性を使用)。 その属性では、ASP.NET Core の組み込みの偽造防止サポートを使用して、要求トークンで cookie が設定されます。

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

DisableFormValueModelBindingAttribute は、モデル バインドを無効にするために使用されます。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

サンプル アプリでは、GenerateAntiforgeryTokenCookieAttribute および DisableFormValueModelBindingAttribute は、Razor Pages の規則を使用して、Startup.ConfigureServices/StreamedSingleFileUploadDb および /StreamedSingleFileUploadPhysical のページ アプリケーション モデルにフィルターとして適用されます。

services.AddMvc()
    .AddRazorPagesOptions(options =>
        {
            options.Conventions
                .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
                    model =>
                    {
                        model.Filters.Add(
                            new GenerateAntiforgeryTokenCookieAttribute());
                        model.Filters.Add(
                            new DisableFormValueModelBindingAttribute());
                    });
            options.Conventions
                .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
                    model =>
                    {
                        model.Filters.Add(
                            new GenerateAntiforgeryTokenCookieAttribute());
                        model.Filters.Add(
                            new DisableFormValueModelBindingAttribute());
                    });
        })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

モデル バインドではフォームが読み取られないため、フォームからバインドされているパラメーターはバインドされません (クエリ、ルート、ヘッダーは引き続き機能します)。 アクション メソッドでは、Request プロパティが直接操作されます。 MultipartReader は各セクションを読み取るために使用されます。 キー/値データは KeyValueAccumulator に格納されます。 マルチパート セクションが読み取られた後、KeyValueAccumulator の内容を使用して、フォーム データがモデル タイプにバインドされます。

EF Core でデータベースにストリーミングするための完全な StreamingController.UploadDatabase メソッド:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}

物理的な場所にストリーミングするための完全な StreamingController.UploadPhysical メソッド:

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}

サンプル アプリでは、検証チェックは FileHelpers.ProcessStreamedFile によって処理されます。

検証

サンプル アプリの FileHelpers クラスでは、バッファーリングされた IFormFile とストリーミングされたファイルのアップロードに関するいくつかのチェックが示されています。 サンプル アプリでのバッファーリングされたファイルのアップロード IFormFile の処理については、Utilities/FileHelpers.cs ファイルの ProcessFormFile メソッドを参照してください。 ストリーミングされたファイルの処理については、同じファイルの ProcessStreamedFile メソッドを参照してください。

警告

サンプル アプリで示されている検証処理メソッドでは、アップロードされたファイルの内容はスキャンされません。 ほとんどの運用シナリオでは、ファイルをユーザーまたは他のシステムで使用できるようにする前に、ウイルス/マルウェア スキャナー API が使用されます。

このトピックのサンプルでは検証技法の実際の例が示されていますが、次の場合を除き、運用アプリでは FileHelpers クラスを実装しないでください。

  • 実装を完全に理解している。
  • アプリの環境と仕様に合わせて実装が適切に変更されている。

これらの要件に対応することなく、アプリにセキュリティ コードをむやみに実装しないでください。

コンテンツの検証

アップロードされた内容に対してサードパーティ製のウイルス/マルウェア スキャン API を使用します。

大容量のシナリオでは、ファイルのスキャンに大量のサーバー リソースが必要です。 ファイルのスキャンによって要求の処理パフォーマンスが低下する場合は、スキャン処理をバックグラウンド サービスにオフロードすることを検討してください (たとえば、アプリのサーバーとは異なるサーバーで実行されているサービス)。 通常、アップロードされたファイルは、バックグラウンドのウイルス検索プログラムによってチェックされるまで、検疫された領域に保持されます。 合格したファイルは、通常のファイル ストレージの場所に移動されます。 これらの手順は、通常、ファイルのスキャン状態を示すデータベース レコードと共に実行されます。 このような方法を使用することで、アプリとアプリ サーバーは要求への応答に集中できます。

ファイル拡張子の検証

アップロードされたファイルの拡張子を、許可されている拡張子のリストで確認する必要があります。 次に例を示します。

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

ファイルのシグネチャの検証

ファイルのシグネチャは、ファイルの先頭にある最初の数バイトによって決定されます。 これらのバイトを使用して、拡張子がファイルの内容と一致するかどうかを示すことができます。 サンプル アプリでは、いくつかの一般的なファイルの種類についてファイルのシグネチャがチェックされています。 次の例では、JPEG イメージのファイルのシグネチャが、ファイルに対してチェックされています。

private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}

追加のファイル シグネチャを取得するには、「ファイル シグネチャ データベース (Google 検索結果)」および公式なファイルの仕様を使用してください。 公式なファイルの仕様を参照すると、選択したシグネチャが有効であることを確認できます。

ファイル名のセキュリティ

ファイルを物理ストレージに保存する場合は、クライアント指定のファイル名を使用しないでください。 Path.GetRandomFileName または Path.GetTempFileName を使用して、ファイルの安全なファイル名を作成し、一時ストレージの完全なパス (ファイル名を含む) を作成します。

Razor では、プロパティ値が表示用に自動的に HTML エンコードされます。 次のコードは安全に使用できます。

@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}

Razor の外部では、ユーザーの要求からのファイル名の内容を常に HtmlEncode でエンコードします。

多くの実装には、ファイルが存在するかどうかのチェックを含める必要があります。そうしないと、同じ名前のファイルによってファイルが上書きされます。 アプリの仕様を満たす追加のロジックを提供します。

サイズの検証

アップロードされるファイルのサイズを制限します。

サンプル アプリでは、ファイルのサイズは 2 MB (バイト単位) に制限されています。 その制限は、 appsettings.json ファイルの Configuration によって提供されます。

{
  "FileSizeLimit": 2097152
}

FileSizeLimitPageModel クラスに挿入されます。

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

    public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
    {
        _fileSizeLimit = config.GetValue<long>("FileSizeLimit");
    }

    ...
}

ファイルのサイズが制限を超えると、ファイルは拒否されます。

if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}

name 属性の値を POST メソッドのパラメーター名に一致させる

フォーム データを POST する、または JavaScript の FormData を直接使用する、Razor 以外のフォームでは、フォームの要素または FormData で指定されている名前が、コントローラーのアクションのパラメーターの name と一致している必要があります。

次の例では

  • <input> 要素を使用すると、name 属性には値 battlePlans が設定されます。

    <input type="file" name="battlePlans" multiple>
    
  • JavaScript で FormData を使用すると、name には値 battlePlans が設定されます。

    var formData = new FormData();
    
    for (var file in files) {
      formData.append("battlePlans", file, file.name);
    }
    

C# メソッドのパラメーターに一致する名前を使用します (battlePlans)。

  • Upload という名前の Razor Pages ページ ハンドラー メソッドの場合:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • MVC POST コントローラー アクション メソッドの場合:

    public async Task<IActionResult> Post(List<IFormFile> battlePlans)
    

サーバーとアプリの構成

マルチパート本文の長さの制限

MultipartBodyLengthLimit では、各マルチパート本文の長さの制限が設定されます。 この制限を超えるフォーム セクションでは、解析時に InvalidDataException がスローされます。 既定値は 134,217,728 (128 MB) です。 制限をカスタマイズするには、Startup.ConfigureServices の設定 MultipartBodyLengthLimit を使用します。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}

単一ページまたはアクションに対する MultipartBodyLengthLimit を設定するには、RequestFormLimitsAttribute を使用します。

Razor Pages アプリでは、Startup.ConfigureServicesconvention を使用してフィルターを適用します。

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions
            .AddPageApplicationModelConvention("/FileUploadPage",
                model.Filters.Add(
                    new RequestFormLimitsAttribute()
                    {
                        // Set the limit to 256 MB
                        MultipartBodyLengthLimit = 268435456
                    });
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Razor Pages アプリまたは MVC アプリでは、ページ モデルまたはアクション メソッドにフィルターを適用します。

// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Kestrel の要求本文の最大サイズ

Kestrel によってホストされるアプリの場合、既定の要求本文の最大サイズは、30,000,000 バイトです。これは約 28.6 MB になります。 制限をカスタマイズするには、MaxRequestBodySizeKestrel サーバー オプションを使用します。

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureKestrel((context, options) =>
        {
            // Handle requests up to 50 MB
            options.Limits.MaxRequestBodySize = 52428800;
        });

単一ページまたはアクションに対する MaxRequestBodySize を設定するには、RequestSizeLimitAttribute を使用します。

Razor Pages アプリでは、Startup.ConfigureServicesconvention を使用してフィルターを適用します。

services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions
            .AddPageApplicationModelConvention("/FileUploadPage",
                model =>
                {
                    // Handle requests up to 50 MB
                    model.Filters.Add(
                        new RequestSizeLimitAttribute(52428800));
                });
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Razor Pages アプリまたは MVC アプリでは、ページ ハンドラー クラスまたはアクション メソッドにフィルターを適用します。

// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}

Kestrel に関するその他の制限

Kestrel によってホストされるアプリには、Kestrel に関するその他の制限が適用される場合があります。

IIS

既定の要求の制限 (maxAllowedContentLength) は、30,000,000 バイトです。これは約 28.6 MB になります。 web.config ファイルで制限をカスタマイズします。 次の例では、制限が 50 MB (52,428,800 バイト) に設定されています。

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>

maxAllowedContentLength の設定は IIS にのみ適用されます。 詳細については、「要求制限 <requestLimits>」を参照してください。

Startup.ConfigureServicesIISServerOptions.MaxRequestBodySize を設定して、HTTP 要求の最大要求本文サイズを増やします。 次の例では、制限が 50 MB (52,428,800 バイト) に設定されています。

services.Configure<IISServerOptions>(options =>
{
    options.MaxRequestBodySize = 52428800;
});

詳細については、「IIS を使用した Windows での ASP.NET Core のホスト」を参照してください。

トラブルシューティング

ファイルのアップロード時に発生する一般的ないくつかの問題と、考えられる解決策を以下に示します。

IIS サーバーに展開したときの "見つかりません" エラー

次のエラーは、アップロードされるファイルがサーバーの構成されている内容の長さを超えていることを示します。

HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.

詳細については、「IIS」セクションを参照してください。

接続エラー

接続エラーおよびサーバー接続のリセットは、アップロードされるファイルが Kestrel の最大要求本文サイズを超えていることを示している場合があります。 詳細については、「Kestrel の要求本文の最大サイズ」セクションを参照してください。 Kestrel クライアント接続の制限の調整も必要な場合があります。

IFormFile での null 参照例外

コントローラーが IFormFile を使用してアップロードされたファイルを受け取っても、値が null の場合は、HTML フォームで multipart/form-data に値 enctype が指定されていることを確認します。 この属性が <form> 要素で設定されていない場合、ファイルはアップロードされず、バインドされた IFormFile 引数は null になります。 また、フォーム データでのアップロードの名前がアプリの名前と一致することを確認します。

ストリームが長すぎる

このトピックの例は、アップロードされたファイル コンテンツを保持するために MemoryStream に依存しています。 int.MaxValue のサイズ制限は MemoryStream です。 アプリのファイル アップロード シナリオで 50 MB を超えるファイル コンテンツを保持する必要がある場合は、アップロードされたファイルのコンテンツを 1 つの MemoryStream に依存することなく保持する別のアプローチを使用します。

その他の技術情報