在 ASP.NET Core 中上傳檔案

作者: Steve SmithRutger Storm

ASP.NET Core支援針對較小的檔案使用緩衝模型系結上傳一或多個檔案,以及針對較大型檔案使用未緩衝的串流。

檢視或下載範例程式碼 \(英文\) (如何下載)

安全性考量

提供使用者將檔案上傳至伺服器的能力時,請小心。 攻擊者可能會嘗試:

  • 執行 拒絕服務 攻擊。
  • 上傳病毒或惡意程式碼。
  • 以其他方式危害網路和伺服器。

降低成功攻擊可能性的安全性步驟如下:

  • 將檔案上傳至專用的檔案上傳區域,最好是非系統磁片磁碟機。 專用位置可讓您更輕鬆地對上傳的檔案施加安全性限制。 停用檔案上傳位置的執行許可權。†
  • 請勿將上傳的檔案保存在與 app.† 相同的目錄樹狀目錄中
  • 使用應用程式所決定的安全檔案名。 在顯示檔案時,請勿使用使用者提供的檔案名或上傳檔案的不受信任檔案名。† HTML 會將不受信任的檔案名編碼。 例如,記錄檔案名或在 UI 中顯示 (Razor 自動 HTML 編碼輸出) 。
  • 只允許應用程式設計規格的已核准副檔名。†
  • 確認用戶端檢查是在 server 上執行。†用戶端檢查很容易規避。
  • 檢查已上傳檔案的大小。 設定大小上限以防止大型上傳。†
  • 當檔案不應以相同名稱上傳的檔案覆寫時,請在上傳檔案之前,先檢查資料庫或實體儲存體的檔案名。
  • 在檔案儲存之前,在上傳的內容上執行病毒/惡意程式碼掃描器。

†範例應用程式示範符合準則的方法。

警告

將惡意程式碼上傳至系統經常是執行程式碼的第一步,該程式碼可能:

  • 完全掌控系統。
  • 多載系統,並產生系統當機的結果。
  • 洩漏使用者或系統資料。
  • 將 graffiti 套用至公用 UI。

如需在接受來自使用者的檔案時減少攻擊介面區的資訊,請參閱下列資源:

如需實作安全性措施的詳細資訊,包括範例應用程式中的範例,請參閱 驗證 一節。

儲存體案例

檔案的常見儲存選項包括:

  • 資料庫

    • 對於小型檔案上傳,資料庫通常比實體儲存體 (檔案系統或網路共用) 選項更快。
    • 資料庫通常比實體儲存體選項更方便,因為擷取使用者資料的資料庫記錄可以同時提供檔案內容 (例如虛擬人偶影像) 。
    • 資料庫的成本可能比使用雲端資料儲存體服務低。
  • 實體儲存體 (檔案系統或網路共用)

    • 針對大型檔案上傳:
      • 資料庫限制可能會限制上傳的大小。
      • 實體儲存體通常比資料庫中的儲存體低。
    • 實體儲存體的成本可能比使用雲端資料儲存體服務低。
    • 應用程式的進程必須具有儲存體位置的讀取和寫入權限。 永不授與執行許可權。
  • 例如,雲端資料儲存體服務Azure Blob 儲存體

    • 服務通常會針對通常受限於單一失敗點的內部部署解決方案,提供改善的延展性和復原能力。
    • 在大型儲存體基礎結構案例中,服務的成本可能較低。

    如需詳細資訊,請參閱 快速入門:使用 .NET 在物件儲存體中建立 Blob

檔案上傳案例

上傳檔案的兩種一般方法是緩衝和串流。

緩衝處理

整個檔案會讀入 IFormFileIFormFile 是用來處理或儲存檔案之檔案的 C# 標記法。

檔案上傳所使用的磁片和記憶體取決於並行檔案上傳的數目和大小。 如果應用程式嘗試緩衝太多上傳,網站會在記憶體或磁碟空間不足時損毀。 如果檔案上傳的大小或頻率耗盡應用程式資源,請使用串流。

任何超過 64 KB 的單一緩衝檔會從記憶體移至磁片上的暫存檔案。

大型要求的暫存檔案會寫入環境變數中 ASPNETCORE_TEMP 名為 的位置。 如果未 ASPNETCORE_TEMP 定義 ,檔案會寫入目前使用者的暫存資料夾。

本主題的下列各節涵蓋緩衝處理小型檔案:

串流

檔案會從多部分要求接收,並由應用程式直接處理或儲存。 串流不會大幅改善效能。 串流可減少上傳檔案時的記憶體或磁碟空間需求。

串流大型檔案涵蓋在 使用串流上傳大型檔案 一節中。

將具有緩衝模型系結的小型檔案上傳至實體儲存體

若要上傳小型檔案,請使用多部分表單,或使用 JavaScript 建構 POST 要求。

下列範例示範如何使用 Razor Pages 表單在範例應用程式中上傳單一檔案 (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 的 (擷取 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 表單必須指定編碼類型 (enctype) multipart/form-data

若要讓 files 輸入元素支援上傳多個檔案,請在 multiple 元素上 <input> 提供 屬性:

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

您可以使用 透過 模型系結 來存取 IFormFile 上傳至伺服器的個別檔案。 範例應用程式示範資料庫和實體儲存體案例的多個緩衝檔上傳。

警告

請勿使用 FileName 的 屬性 IFormFile 來顯示和記錄。 顯示或記錄時,HTML 會將檔案名編碼。 攻擊者可以提供惡意檔案名,包括完整路徑或相對路徑。 應用程式應該:

  • 從使用者提供的檔案名中移除路徑。
  • 儲存 HTML 編碼、路徑移除的檔案名以進行 UI 或記錄。
  • 為儲存體產生新的隨機檔案名。

下列程式碼會從檔案名中移除路徑:

string untrustedFileName = Path.GetFileName(pathName);

到目前為止提供的範例不會考慮安全性考慮。 下列各節和 範例應用程式會提供其他資訊:

使用模型系結和 IFormFile 上傳檔案時,動作方法可以接受:

注意

系結會依名稱比對表單檔案。 例如,中的 <input type="file" name="formFile"> HTML name 值必須符合 C# 參數/屬性系結 (FormFile) 。 如需詳細資訊,請參閱 將 name 屬性值與 POST 方法的參數名稱相符 一節。

下列範例將:

  • 迴圈處理一或多個上傳的檔案。
  • 使用 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 儲存體

如需迴圈處理多個檔案以上傳和使用安全檔案名的另一個範例,請參閱 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs 範例應用程式中的 。

警告

Path.GetTempFileName 會在建立超過 65,535 個檔案時擲回 , IOException 而不刪除先前的暫存檔案。 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>

當表單是 POSTed 到伺服器時,請將 複製到 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

警告

將二進位資料儲存至關聯式資料庫時請小心,因為它可能會對效能造成不良影響。

請勿依賴 或信任 FileNameIFormFile 屬性,而不需驗證。 屬性 FileName 只應該用於顯示用途,而且只在 HTML 編碼之後使用。

提供的範例不會考慮安全性考慮。 下列各節和 範例應用程式會提供其他資訊:

使用串流上傳大型檔案

3.1 範例示範如何使用 JavaScript 將檔案串流至控制器動作。 檔案的反分叉權杖是使用自訂篩選屬性產生,並傳遞至用戶端 HTTP 標頭,而不是在要求本文中傳遞。 因為動作方法會直接處理上傳的資料,所以表單模型系結會由另一個自訂篩選停用。 在動作內,會使用 MultipartReader 來讀取表單內容,以讀取每個個別 MultipartSection、處理檔案,或視需要儲存內容。 讀取多部分區段之後,動作會執行自己的模型系結。

初始頁面回應會載入表單,並透過 GenerateAntiforgeryTokenCookieAttribute 屬性) 將反分叉權杖儲存在 cookie (中。 屬性會使用 ASP.NET Core的內建反forgery 支援,以要求權杖設定 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 並使用Pages 慣例,將 和 套用為 頁面應用程式模型的 Razor/StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysicalStartup.ConfigureServices 篩選: DisableFormValueModelBindingAttribute

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());
            });
});

由於模型系結不會讀取表單,因此從表單系結的參數不會系結 (查詢、路由和標頭繼續運作) 。 動作方法可直接與 屬性搭配運作 RequestMultipartReader 是用來讀取每個區段。 索引鍵/值資料會儲存在 中 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 範例應用程式中的緩衝檔上傳,請參閱 ProcessFormFile 檔案中的 Utilities/FileHelpers.cs 方法。 如需處理串流檔案,請參閱 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));
}

若要取得其他檔案簽章,請參閱 檔案簽章資料庫 和官方檔案規格。

檔案名安全性

請勿使用用戶端提供的檔案名將檔案儲存到實體儲存體。 使用 Path.GetRandomFileNamePath.GetTempFileName 建立檔案的安全檔案名,以建立完整路徑 (包括暫存儲存體的檔案名) 。

Razor 會自動 HTML 編碼要顯示的屬性值。 下列程式碼是安全的使用:

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

在 Razor 之外,一律 HtmlEncode 會從使用者的要求中檔案名內容。

許多實作都必須包含檔案是否存在的檢查;否則,檔案會以相同名稱的檔案覆寫。 提供其他邏輯以符合您應用程式的規格。

大小驗證

限制上傳檔案的大小。

在範例應用程式中,檔案的大小限制為 2 MB (以位元組) 表示。 此限制是透過檔案中的 appsettings.jsonConfiguration提供:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit 插入 PageModel 類別:

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 方法的參數名稱相符

Razor在 POST 表單資料或直接使用 JavaScript FormData 的非表單中,表單元素中指定的名稱,或 FormData 必須符合控制器動作中參數的名稱。

在下例中︰

  • 使用 <input> 專案時,屬性 name 會設定為 值 battlePlans

    <input type="file" name="battlePlans" multiple>
    
  • 在 JavaScript 中使用 FormData 時,名稱會設定為 值 battlePlans

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

使用 C# 方法參數的相符名稱, () battlePlans

  • Razor針對名為 Upload 的 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) 。 使用 MultipartBodyLengthLimit 中的 Startup.ConfigureServices 設定自訂限制:

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

RequestFormLimitsAttribute 用於設定 MultipartBodyLengthLimit 單一頁面或動作的 。

Razor在 Pages 應用程式中,在 中套用具有慣例Startup.ConfigureServices 篩選:

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>();
        });

RequestSizeLimitAttribute 用來設定單一頁面或動作的 MaxRequestBodySize

Razor在 Pages 應用程式中,在 中套用具有慣例Startup.ConfigureServices 篩選:

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

Razor在頁面應用程式或 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 表單正在 enctype 指定 的值 multipart/form-data 。 如果未在 <form> 專案上設定這個屬性,則不會發生檔案上傳,而且任何系結 IFormFile 引數都是 null 。 同時確認 表單資料中的上傳命名符合應用程式的命名

資料流程太長

本主題中的範例依賴 MemoryStream 保存上傳的檔案內容。 的大小限制 MemoryStreamint.MaxValue 。 如果應用程式的檔案上傳案例需要保存大於 50 MB 的檔案內容,請使用不依賴單 MemoryStream 一方法來保存上傳的檔案內容。

ASP.NET Core支援針對較小的檔案使用緩衝模型系結上傳一或多個檔案,以及較大型檔案的未緩衝串流。

檢視或下載範例程式碼 \(英文\) (如何下載)

安全性考量

為使用者提供將檔案上傳至伺服器的能力時,請小心。 攻擊者可能會嘗試:

  • 執行 拒絕服務 攻擊。
  • 上傳病毒或惡意程式碼。
  • 以其他方式危害網路和伺服器。

降低成功攻擊可能性的安全性步驟如下:

  • 將檔案上傳至專用的檔案上傳區域,最好是非系統磁片磁碟機。 專用位置可讓您更輕鬆地對上傳的檔案施加安全性限制。 停用檔案上傳位置的執行許可權。†
  • 請勿將上傳的檔案保存在與 app.†相同的目錄樹狀目錄中
  • 使用應用程式所決定的安全檔案名。 請勿使用使用者提供的檔案名或上傳檔案的不受信任檔案名。† HTML 會在顯示時編碼不受信任的檔案名。 例如,記錄檔案名或在 UI 中顯示, (Razor 自動 HTML 編碼輸出) 。
  • 只允許應用程式設計規格的已核准副檔名。†
  • 確認用戶端檢查是在 server.†用戶端檢查很容易規避。
  • 檢查上傳的檔案大小。 設定大小上限以防止大型 uploads.†
  • 當上傳的檔案不應該以相同名稱覆寫檔案時,請先檢查資料庫或實體儲存體的檔案名,再上傳檔案。
  • 在檔案儲存之前,在上傳的內容上執行病毒/惡意程式碼掃描器。

†範例應用程式示範符合準則的方法。

警告

將惡意程式碼上傳至系統經常是執行程式碼的第一步,該程式碼可能:

  • 完全掌控系統。
  • 多載系統,並產生系統當機的結果。
  • 洩漏使用者或系統資料。
  • 將 graffiti 套用至公用 UI。

如需在接受來自使用者的檔案時減少攻擊介面區的資訊,請參閱下列資源:

如需實作安全性措施的詳細資訊,包括範例應用程式的範例,請參閱 驗證 一節。

儲存體案例

檔案的常見儲存選項包括:

  • 資料庫

    • 對於小型檔案上傳,資料庫通常比實體儲存體更快, (檔案系統或網路共用) 選項。
    • 資料庫通常比實體儲存體選項更方便,因為擷取使用者資料的資料庫記錄可以同時提供檔案內容 (例如,虛擬人偶影像) 。
    • 資料庫的成本可能比使用資料儲存體服務還低。
  • 實體儲存體 (檔案系統或網路共用)

    • 針對大型檔案上傳:
      • 資料庫限制可能會限制上傳的大小。
      • 實體儲存體通常比資料庫中的儲存體低。
    • 實體儲存體的成本可能比使用資料儲存體服務低。
    • 應用程式的進程必須具有儲存體位置的讀取和寫入權限。 永不授與執行許可權。
  • 例如,資料儲存體服務 (Azure Blob 儲存體)

    • 服務通常會針對通常受限於單一失敗點的內部部署解決方案,提供改善的延展性和復原能力。
    • 在大型儲存體基礎結構案例中,服務的成本可能較低。

    如需詳細資訊,請參閱 快速入門:使用 .NET 在物件儲存體中建立 Blob

檔案上傳案例

上傳檔案的兩個一般方法是緩衝和串流。

緩衝處理

整個檔案會讀取到 IFormFile ,這是用來處理或儲存檔案之檔案的 C# 標記法。

資源 (磁片、檔案上傳所使用的記憶體) ,取決於並行檔案上傳的數目和大小。 如果應用程式嘗試緩衝太多上傳,網站會在記憶體或磁碟空間不足時損毀。 如果檔案上傳的大小或頻率耗盡應用程式資源,請使用串流。

注意

任何超過 64 KB 的單一緩衝檔案會從記憶體移至磁片上的暫存檔案。

本主題的下列各節涵蓋緩衝小型檔案:

串流

檔案會從多部分要求接收,並由應用程式直接處理或儲存。 串流不會大幅改善效能。 串流可減少上傳檔案時記憶體或磁碟空間的需求。

串流大型檔案涵蓋在 上傳具有串流的大型檔案 一節中。

將具有緩衝模型系結的小型檔案上傳至實體儲存體

若要上傳小型檔案,請使用多部分表單,或使用 JavaScript 建構 POST 要求。

下列範例示範如何使用 Pages 表單在範例應用程式中上傳單一 Razor 檔案 (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 的 (擷取 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 表單必須指定編碼類型 () enctypemultipart/form-data

files若要讓輸入專案支援上傳多個檔案,請在 multiple<input> 元素上提供 屬性:

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

您可以使用 透過 模型系結IFormFile 來存取上傳至伺服器的個別檔案。 範例應用程式示範資料庫和實體儲存體案例的多個緩衝檔上傳。

警告

請勿使用 FileName 的 屬性 IFormFile 來顯示和記錄。 顯示或記錄時,HTML 會將檔案名編碼。 攻擊者可以提供惡意檔案名,包括完整路徑或相對路徑。 應用程式應該:

  • 從使用者提供的檔案名中移除路徑。
  • 儲存 UI 或記錄的 HTML 編碼路徑移除檔案名。
  • 產生儲存體的新隨機檔案名。

下列程式碼會從檔案名中移除路徑:

string untrustedFileName = Path.GetFileName(pathName);

到目前為止提供的範例不會考慮安全性考慮。 下列各節和 範例應用程式會提供其他資訊:

使用模型系結和 IFormFile 上傳檔案時,動作方法可以接受:

注意

系結會依名稱比對表單檔案。 例如,中的 <input type="file" name="formFile"> HTML name 值必須符合 C# 參數/屬性系結 (FormFile) 。 如需詳細資訊,請參閱 將 name 屬性值與 POST 方法的參數名稱相符 一節。

下列範例將:

  • 迴圈流覽一或多個上傳的檔案。
  • 使用 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 儲存體

如需迴圈處理多個檔案以上傳和使用安全檔案名的另一個範例,請參閱 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs 範例應用程式中的 。

警告

Path.GetTempFileName 如果建立超過 65,535 個檔案,而不會刪除先前的暫存檔案,則會擲 IOException 回 。 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用於 Pages 表單: Razor

<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>

當表單是 POSTed 到伺服器時,請將 複製到 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、處理檔案,或視需要儲存內容。 讀取多部分區段之後,動作會執行自己的模型系結。

初始頁面回應會載入表單,並透過 GenerateAntiforgeryTokenCookieAttribute 屬性) 將反分叉權杖儲存在 cookie (中。 屬性會使用 ASP.NET Core的內建反forgery 支援,以要求權杖設定 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 並使用Pages 慣例將 和 套用為 頁面應用程式模型的 Razor/StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysicalStartup.ConfigureServices 篩選: DisableFormValueModelBindingAttribute

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());
            });
});

因為模型系結不會讀取表單,所以從表單系結的參數不會系結 (查詢、路由和標頭繼續運作) 。 動作方法可直接與 屬性搭配使用 RequestMultipartReader 是用來讀取每個區段。 索引鍵/值資料會儲存在 中 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 緩衝檔案上傳,請參閱 ProcessFormFile 檔案中的 Utilities/FileHelpers.cs 方法。 如需處理串流檔案,請參閱 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));
}

若要取得其他檔案簽章,請參閱 檔案簽章資料庫 和官方檔案規格。

檔案名安全性

絕對不要使用用戶端提供的檔案名,將檔案儲存至實體儲存體。 使用 Path.GetRandomFileNamePath.GetTempFileName 建立檔案的安全檔案名,以建立完整路徑 (包括暫存儲存體的檔案名) 。

Razor 會自動將屬性值編碼為顯示。 下列程式碼是安全的:

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

Razor在 外部,一律 HtmlEncode 會從使用者的要求中檔案名內容。

許多實作都必須包含檔案是否存在的檢查;否則,檔案會以相同名稱的檔案覆寫。 提供其他邏輯以符合您應用程式的規格。

大小驗證

限制上傳檔案的大小。

在範例應用程式中,檔案的大小限制為 2 MB (以位元組) 表示。 此限制是透過檔案中的 appsettings.jsonConfiguration提供:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit 插入 PageModel 類別:

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 方法的參數名稱相符

Razor在 POST 表單資料或直接使用 JavaScript FormData 的非表單中,表單元素中指定的名稱,或 FormData 必須符合控制器動作中參數的名稱。

在下例中︰

  • 使用 <input> 專案時,屬性 name 會設定為 值 battlePlans

    <input type="file" name="battlePlans" multiple>
    
  • 在 JavaScript 中使用 FormData 時,名稱會設定為 值 battlePlans

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

使用 C# 方法參數的相符名稱, () battlePlans

  • Razor針對名為 Upload 的 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) 。 使用 MultipartBodyLengthLimit 中的 Startup.ConfigureServices 設定自訂限制:

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

RequestFormLimitsAttribute 用於設定 MultipartBodyLengthLimit 單一頁面或動作的 。

Razor在 Pages 應用程式中,在 中套用具有慣例Startup.ConfigureServices 篩選:

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>();
        });

RequestSizeLimitAttribute 用來設定單一頁面或動作的 MaxRequestBodySize

Razor在 Pages 應用程式中,在 中套用具有慣例Startup.ConfigureServices 篩選:

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

Razor在頁面應用程式或 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.ConfigureServices 設定 IISServerOptions.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 表單正在 enctype 指定 的值 multipart/form-data 。 如果未在 <form> 專案上設定這個屬性,則不會發生檔案上傳,而且任何系結 IFormFile 引數都是 null 。 同時確認 表單資料中的上傳命名符合應用程式的命名

資料流程太長

本主題中的範例依賴 MemoryStream 保存上傳的檔案內容。 的大小限制 MemoryStreamint.MaxValue 。 如果應用程式的檔案上傳案例需要保存大於 50 MB 的檔案內容,請使用不依賴單 MemoryStream 一方法來保存上傳的檔案內容。

ASP.NET Core支援針對較小的檔案使用緩衝模型系結上傳一或多個檔案,以及較大型檔案的未緩衝串流。

檢視或下載範例程式碼 \(英文\) (如何下載)

安全性考量

為使用者提供將檔案上傳至伺服器的能力時,請小心。 攻擊者可能會嘗試:

  • 執行 拒絕服務 攻擊。
  • 上傳病毒或惡意程式碼。
  • 以其他方式危害網路和伺服器。

降低成功攻擊可能性的安全性步驟如下:

  • 將檔案上傳至專用的檔案上傳區域,最好是非系統磁片磁碟機。 專用位置可讓您更輕鬆地對上傳的檔案施加安全性限制。 停用檔案上傳位置的執行許可權。†
  • 請勿將上傳的檔案保存在與 app.†相同的目錄樹狀目錄中
  • 使用應用程式所決定的安全檔案名。 請勿使用使用者提供的檔案名或上傳檔案的不受信任檔案名。† HTML 會在顯示時編碼不受信任的檔案名。 例如,記錄檔案名或在 UI 中顯示, (Razor 自動 HTML 編碼輸出) 。
  • 只允許應用程式設計規格的已核准副檔名。†
  • 確認用戶端檢查是在 server.†用戶端檢查很容易規避。
  • 檢查上傳的檔案大小。 設定大小上限以防止大型 uploads.†
  • 當上傳的檔案不應該以相同名稱覆寫檔案時,請先檢查資料庫或實體儲存體的檔案名,再上傳檔案。
  • 在檔案儲存之前,在上傳的內容上執行病毒/惡意程式碼掃描器。

†範例應用程式示範符合準則的方法。

警告

將惡意程式碼上傳至系統經常是執行程式碼的第一步,該程式碼可能:

  • 完全掌控系統。
  • 多載系統,並產生系統當機的結果。
  • 洩漏使用者或系統資料。
  • 將 graffiti 套用至公用 UI。

如需在接受來自使用者的檔案時減少攻擊介面區的資訊,請參閱下列資源:

如需實作安全性措施的詳細資訊,包括範例應用程式的範例,請參閱 驗證 一節。

儲存體案例

檔案的常見儲存選項包括:

  • 資料庫

    • 對於小型檔案上傳,資料庫通常比實體儲存體更快, (檔案系統或網路共用) 選項。
    • 資料庫通常比實體儲存體選項更方便,因為擷取使用者資料的資料庫記錄可以同時提供檔案內容 (例如,虛擬人偶影像) 。
    • 資料庫的成本可能比使用資料儲存體服務還低。
  • 實體儲存體 (檔案系統或網路共用)

    • 針對大型檔案上傳:
      • 資料庫限制可能會限制上傳的大小。
      • 實體儲存體通常比資料庫中的儲存體低。
    • 實體儲存體的成本可能比使用資料儲存體服務低。
    • 應用程式的進程必須具有儲存體位置的讀取和寫入權限。 永不授與執行許可權。
  • 例如,資料儲存體服務 (Azure Blob 儲存體)

    • 服務通常會針對通常受限於單一失敗點的內部部署解決方案,提供改善的延展性和復原能力。
    • 在大型儲存體基礎結構案例中,服務的成本可能較低。

    如需詳細資訊,請參閱 快速入門:使用 .NET 在物件儲存體中建立 Blob。 本主題示範 UploadFromFileAsync ,但在 UploadFromStreamAsync 使用 Stream 時,可用來將 儲存 FileStream 至 Blob 儲存體。

檔案上傳案例

上傳檔案的兩個一般方法是緩衝和串流。

緩衝處理

整個檔案會讀取到 IFormFile ,這是用來處理或儲存檔案之檔案的 C# 標記法。

資源 (磁片、檔案上傳所使用的記憶體) ,取決於並行檔案上傳的數目和大小。 如果應用程式嘗試緩衝太多上傳,網站會在記憶體或磁碟空間不足時損毀。 如果檔案上傳的大小或頻率耗盡應用程式資源,請使用串流。

注意

任何超過 64 KB 的單一緩衝檔案會從記憶體移至磁片上的暫存檔案。

本主題的下列各節涵蓋緩衝小型檔案:

串流

檔案會從多部分要求接收,並由應用程式直接處理或儲存。 串流不會大幅改善效能。 串流可減少上傳檔案時記憶體或磁碟空間的需求。

串流大型檔案涵蓋在 上傳具有串流的大型檔案 一節中。

將具有緩衝模型系結的小型檔案上傳至實體儲存體

若要上傳小型檔案,請使用多部分表單,或使用 JavaScript 建構 POST 要求。

下列範例示範如何使用 Pages 表單在範例應用程式中上傳單一 Razor 檔案 (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 的 (擷取 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 表單必須指定編碼類型 () enctypemultipart/form-data

files若要讓輸入專案支援上傳多個檔案,請在 multiple<input> 元素上提供 屬性:

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

您可以使用 透過 模型系結IFormFile 來存取上傳至伺服器的個別檔案。 範例應用程式示範資料庫和實體儲存體案例的多個緩衝檔上傳。

警告

請勿使用 FileName 的 屬性 IFormFile 來顯示和記錄。 顯示或記錄時,HTML 會將檔案名編碼。 攻擊者可以提供惡意檔案名,包括完整路徑或相對路徑。 應用程式應該:

  • 從使用者提供的檔案名中移除路徑。
  • 儲存 UI 或記錄的 HTML 編碼路徑移除檔案名。
  • 產生儲存體的新隨機檔案名。

下列程式碼會從檔案名中移除路徑:

string untrustedFileName = Path.GetFileName(pathName);

到目前為止提供的範例不會考慮安全性考慮。 下列各節和 範例應用程式會提供其他資訊:

使用模型系結和 IFormFile 上傳檔案時,動作方法可以接受:

注意

系結會依名稱比對表單檔案。 例如,中的 <input type="file" name="formFile"> HTML name 值必須符合 C# 參數/屬性系結 (FormFile) 。 如需詳細資訊,請參閱 將 name 屬性值與 POST 方法的參數名稱相符 一節。

下列範例將:

  • 迴圈處理一或多個上傳的檔案。
  • 使用 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 儲存體

如需迴圈處理多個檔案以上傳和使用安全檔案名的另一個範例,請參閱 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs 範例應用程式中的 。

警告

Path.GetTempFileName 會在建立超過 65,535 個檔案時擲回 , IOException 而不刪除先前的暫存檔案。 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>

當表單是 POSTed 到伺服器時,請將 複製到 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

警告

將二進位資料儲存至關聯式資料庫時請小心,因為它可能會對效能造成不良影響。

請勿依賴 或信任 FileNameIFormFile 屬性,而不需驗證。 屬性 FileName 只應該用於顯示用途,而且只在 HTML 編碼之後使用。

提供的範例不會考慮安全性考慮。 下列各節和 範例應用程式會提供其他資訊:

使用串流上傳大型檔案

下列範例示範如何使用 JavaScript 將檔案串流至控制器動作。 檔案的反分叉權杖是使用自訂篩選屬性產生,並傳遞至用戶端 HTTP 標頭,而不是在要求本文中傳遞。 因為動作方法會直接處理上傳的資料,所以表單模型系結會由另一個自訂篩選停用。 在動作內,會使用 MultipartReader 來讀取表單內容,以讀取每個個別 MultipartSection、處理檔案,或視需要儲存內容。 讀取多部分區段之後,動作會執行自己的模型系結。

初始頁面回應會載入表單,並透過 GenerateAntiforgeryTokenCookieAttribute 屬性) 將反分叉權杖儲存在 cookie (中。 屬性會使用 ASP.NET Core的內建反forgery 支援,以要求權杖設定 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 並使用Pages 慣例,將 和 套用為 頁面應用程式模型的 Razor/StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysicalStartup.ConfigureServices 篩選: DisableFormValueModelBindingAttribute

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);

由於模型系結不會讀取表單,因此從表單系結的參數不會系結 (查詢、路由和標頭繼續運作) 。 動作方法可直接與 屬性搭配運作 RequestMultipartReader 是用來讀取每個區段。 索引鍵/值資料會儲存在 中 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 範例應用程式中的緩衝檔上傳,請參閱 ProcessFormFile 檔案中的 Utilities/FileHelpers.cs 方法。 如需處理串流檔案,請參閱 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));
}

若要取得其他檔案簽章,請參閱 檔案簽章資料庫 和官方檔案規格。

檔案名安全性

請勿使用用戶端提供的檔案名將檔案儲存到實體儲存體。 使用 Path.GetRandomFileNamePath.GetTempFileName 建立檔案的安全檔案名,以建立完整路徑 (包括暫存儲存體的檔案名) 。

Razor 會自動 HTML 編碼要顯示的屬性值。 下列程式碼是安全的使用:

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

在 Razor 之外,一律 HtmlEncode 會從使用者的要求中檔案名內容。

許多實作都必須包含檔案是否存在的檢查;否則,檔案會以相同名稱的檔案覆寫。 提供其他邏輯以符合您應用程式的規格。

大小驗證

限制上傳檔案的大小。

在範例應用程式中,檔案的大小限制為 2 MB (以位元組) 表示。 此限制是透過檔案中的 appsettings.jsonConfiguration提供:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit 插入 PageModel 類別:

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 方法的參數名稱相符

Razor在 POST 表單資料或直接使用 JavaScript FormData 的非表單中,表單元素中指定的名稱,或 FormData 必須符合控制器動作中參數的名稱。

在下例中︰

  • 使用 <input> 專案時,屬性 name 會設定為 值 battlePlans

    <input type="file" name="battlePlans" multiple>
    
  • 在 JavaScript 中使用 FormData 時,名稱會設定為 值 battlePlans

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

使用 C# 方法參數的相符名稱, () battlePlans

  • Razor針對名為 Upload 的 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) 。 使用 MultipartBodyLengthLimit 中的 Startup.ConfigureServices 設定自訂限制:

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

RequestFormLimitsAttribute 用於設定 MultipartBodyLengthLimit 單一頁面或動作的 。

Razor在 Pages 應用程式中,在 中套用具有慣例Startup.ConfigureServices 篩選:

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;
        });

RequestSizeLimitAttribute 用來設定單一頁面或動作的 MaxRequestBodySize

Razor在 Pages 應用程式中,在 中套用具有慣例Startup.ConfigureServices 篩選:

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在頁面應用程式或 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.ConfigureServices 設定 IISServerOptions.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 表單正在 enctype 指定 的值 multipart/form-data 。 如果未在 <form> 專案上設定這個屬性,則不會發生檔案上傳,而且任何系結 IFormFile 引數都是 null 。 同時確認 表單資料中的上傳命名符合應用程式的命名

資料流程太長

本主題中的範例依賴 MemoryStream 保存上傳的檔案內容。 的大小限制 MemoryStreamint.MaxValue 。 如果應用程式的檔案上傳案例需要保存大於 50 MB 的檔案內容,請使用不依賴單 MemoryStream 一方法來保存上傳的檔案內容。

其他資源