在 ASP.NET Core 中上傳檔案

作者:Rutger Storm

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

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

安全性考慮

為使用者提供將檔案上傳到伺服器的能力時,請特別注意。 網路攻擊者可能會嘗試:

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

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

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

範例應用程式展示了一種符合準則的方法。

Warning

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

  • 完全取得對系統的控制權。
  • 讓系統過載,進而造成系統當機的結果。
  • 洩漏使用者或系統資料。
  • 在公用 UI 上塗鴉。

如需在接受來自使用者的檔案時減少弱點的資訊,請參閱下列資源:

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

儲存情境

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

  • Database

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

    • 大檔案上傳:
      • 資料庫限制可能會限制上傳的大小。
      • 實體儲存體通常比資料庫儲存體較貴。
    • 實體儲存體可能比使用雲端資料儲存體服務較便宜。
    • 應用程式的處理序必須具有對儲存體位置的讀取和寫入權限。 永不授與執行權限。
  • 雲端資料儲存體服務 (例如 Azure Blob 儲存體)。

    • 與通常容易出現單一失敗點的內部部署解決方案相比,這種服務通常可提供更高的可擴縮性和復原能力。
    • 在大型儲存體基礎結構案例中,這種服務的成本可能較低。

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

小型和大型檔案

小型和大型檔案的定義取決於可用的運算資源。 應用程式應對所使用的儲存方法進行基準測試,以確保它能夠處理預期的大小。 對記憶體、CPU、磁碟和資料庫效能進行基準測試。

雖然無法為您的部署提供小型與大型的具體界限,但以下是一些 ASP.NET Core 的 FormOptions (API 文件) 相關預設值:

  • 依預設,HttpRequest.Form 不會緩衝處理整個要求本文 (BufferBody),但會緩衝處理所包含的任何多部分表單檔案。
  • MultipartBodyLengthLimit 是緩衝處理表單檔案的大小上限 (預設:128MB)。
  • MemoryBufferThreshold 表示在記憶體中的緩衝區閾值達到前,會轉移至磁碟上的緩衝區檔案(預設:64 KB)。 MemoryBufferThreshold 作為區分小型和大型檔案之間的界限,此界限可以根據應用程式的資源和案例來提高或降低。

如需 FormOptions 的詳細資訊,請參閱 ASP.NET Core 參考來源中的 FormOptions 類別

Note

.NET 參考來源的文件連結通常會載入存放庫的預設分支,這代表著 .NET 下一個版本的當前開發狀況。 若要選取特定版本的標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤

檔案上傳案例

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

緩衝處理

整個檔案會被讀入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 的 (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 表單必須指定 enctype 的編碼類型 (multipart/form-data)。

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

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

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

Warning

除了用於顯示和記錄之外,請勿使用 FileNameIFormFile 屬性。 顯示或記錄時,HTML 會對檔名進行編碼。 網路攻擊者會提供惡意的檔名 (包括完整路徑或相對路徑)。 應用程式應該:

  • 從使用者提供的檔名中移除路徑。
  • 儲存已進行 HTML 編碼且移除了路徑的檔名,用於 UI 或記錄。
  • 產生一個新的隨機檔名以用於儲存。

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

string untrustedFileName = Path.GetFileName(pathName);

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

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

Note

綁定會依照名稱匹配表單檔案。 例如,name 中的 HTML <input type="file" name="formFile"> 值必須符合 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

Warning

在 .NET 7 或舊版中, 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; }
}

Note

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

Warning

在將二進位資料儲存在關聯式資料庫時請特別小心,因為這可能會對效能產生負面影響。

請勿在未經驗證的情況下依賴或信任 FileNameIFormFile 屬性。 FileName 屬性只能用於顯示用途,而且只能在 HTML 編碼之後使用。

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

使用串流傳輸上傳大型檔案

針對需要大型檔案上傳的案例,串流上傳可讓您直接處理傳入的多部分窗體數據,而不需要透過模型系結緩衝整個檔案在記憶體中或磁碟上。 這項技術對於可能超過伺服器或架構緩衝閾值的檔案來說特別重要。

9.x 的範例應用程式示範伺服器如何接收檔案,並將數據直接串流至磁碟,支援透過 HTTP 要求的取消令牌進行強固的取消。

MultipartReader 是 ASP.NET Core 公用程式,可從傳入要求讀取檔案。 下列代碼段示範如何處理要求,並將檔案串流至 outputStream (例如 , FileStream):

// Read the boundary from the Content-Type header
var boundary = HeaderUtilities.RemoveQuotes(
    MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value;

// Use MultipartReader to stream data to a destination
var reader = new MultipartReader(boundary, Request.Body);
MultipartSection? section;

while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null)
{
    var contentDisposition = section.GetContentDispositionHeader();

    if (contentDisposition != null && contentDisposition.IsFileDisposition())
    {
        await section.Body.CopyToAsync(outputStream, cancellationToken);
    }
}

IFormFeature 是一個包裹 MultipartReader 的元件,不需要您手動撰寫請求正文解析的代碼。 您可以使用其 ReadFormAsync 方法來填入要求的表單資料,然後從內建集合存取上傳的檔案:

// Get the IFormFeature and read the form
var formFeature = Request.HttpContext.Features.GetRequiredFeature<IFormFeature>();
await formFeature.ReadFormAsync(cancellationToken);

// Access the uploaded file (example: first file)
var filePath = Request.Form.Files.First().FileName;

return Results.Ok($"Saved file at {filePath}");

針對進階的情況,可以使用 HttpRequest.BodyReader 來手動解析原始請求主體,這樣做會提供 IPipeReader 用於低階、高效能串流。 範例應用程式包含端點處理器,這些處理器在最小 API 和控制器中都使用 IPipeReader

3.1 範例示範如何使用 JavaScript 將檔案串流傳輸到控制器動作。 使用自訂篩選屬性來產生檔案的防偽權杖,然後將其添加到客戶端的 HTTP 標頭,而不是放在請求本文中。 由於動作方法直接處理上傳的資料,另一個客戶過濾器會停用表單模型綁定。 在動作內,會使用 MultipartReader 來讀取表單內容,以讀取每個個別 MultipartSection、處理檔案,或視需要儲存內容。 讀取多部分區段之後,動作會執行自己的模型繫結。

初始頁面回應會載入表單,並透過 GenerateAntiforgeryTokenCookieAttribute 屬性將防偽標記(antiforgery token)儲存在 cookie 中。 此屬性會使用 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)
    {
    }
}

在範例應用程式中,GenerateAntiforgeryTokenCookieAttributeDisableFormValueModelBindingAttribute 作為篩選器,使用 Razor 頁面約定應用到 /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 的內容會用來將表單資料繫結至模型類型。

使用 StreamingController.UploadDatabase 串流傳輸到資料庫的完整 EF Core 方法:

[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 負責驗證檢查。

Validation

範例應用程式的 FileHelpers 類別示範了對緩衝 IFormFile 和串流檔案上傳的數項檢查。 如需在範例應用程式中處理 IFormFile 緩衝檔案上傳,請參閱 ProcessFormFile 檔案中的 Utilities/FileHelpers.cs 方法。 如需處理串流檔案,請參閱相同檔案中的 ProcessStreamedFile 方法。

Warning

範例應用程式中示範的驗證處理方法不會掃描已上傳檔案的內容。 在大部分的生產案例中,在讓使用者或其他系統可以使用檔案之前,會先對檔案使用病毒/惡意程式碼掃描器 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 檔案中提供的:

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

在非 FormData 的表單中,無論是 POST 表單資料還是直接使用 JavaScript 的 Razor,表單元素或 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

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

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 Pages 應用程式或 MVC 應用程式中,將篩選器套用至頁面模型或動作方法:

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

也可以使用 @attributeRazor 指令來套用 RequestSizeLimitAttribute:

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

Troubleshoot

以下是在上傳檔案時常見的問題及其可能的解決方案。

部署至 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 表單指定了 enctypemultipart/form-data 值。 如果未在 <form> 元素上設定此屬性,則不會進行檔案上傳,且任何繫結的 IFormFile 引數都會是 null。 也要確認表單資料中的上傳命名與應用程式的命名相符

資料串流過長

本文範例依賴 MemoryStream 於儲存上傳檔案的內容。 MemoryStream 的大小限制為 int.MaxValue。 如果應用程式的檔案上傳場景需要保存大於 50 MB 的檔案內容,請使用不依賴單一 MemoryStream 來保存上傳檔案內容的替代方法。

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

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

安全性考慮

為使用者提供將檔案上傳到伺服器的能力時,請特別注意。 網路攻擊者可能會嘗試:

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

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

  • 將檔案上傳至專用的檔案上傳區,最好是非系統磁碟機。 專用的位置可讓您更輕鬆地對上傳的檔案強制實施安全性限制。 停用檔案上傳位置上的執行權限。†
  • 不要將上傳的檔案保存在與應用程式相同的目錄樹狀結構中。†
  • 使用由應用程式所決定的安全檔名。 請勿使用由使用者所提供的檔名或上傳檔案的不受信任檔名。† 顯示不受信任的檔名時,HTML 會對其進行編碼。 例如,記錄檔名或在 UI 中顯示 (Razor 會自動對輸出進行 HTML 編碼)。
  • 僅允許應用程式的設計規範中批准的檔案擴展名。†
  • 驗證用戶端檢查是否已在伺服器上執行。† 用戶端檢查很容易規避。
  • 檢查上傳的檔案大小。 設定大小上限以防止大型上傳。†
  • 當檔案不應被上傳的同名檔案覆蓋時,請在上傳檔案之前先針對資料庫或實體儲存體檢查檔名。
  • 在儲存檔案之前,先對上傳的內容執行病毒/惡意程式碼掃描器。

範例應用程式展示了一種符合準則的方法。

Warning

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

  • 完全取得對系統的控制權。
  • 讓系統過載,進而造成系統當機的結果。
  • 洩漏使用者或系統資料。
  • 在公用 UI 上塗鴉。

如需在接受來自使用者的檔案時減少弱點的資訊,請參閱下列資源:

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

儲存情境

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

  • Database

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

    • 大檔案上傳:
      • 資料庫限制量可能會限制上傳的大小。
      • 實體儲存體通常比資料庫儲存體較貴。
    • 實體儲存體可能比使用資料儲存體服務較便宜。
    • 應用程式的處理序必須具有對儲存體位置的讀取和寫入權限。 永不授與執行權限。
  • 資料儲存體服務 (例如 Azure Blob 儲存體)

    • 與通常容易出現單一失敗點的內部部署解決方案相比,這種服務通常可提供更高的可擴縮性和復原能力。
    • 在大型儲存體基礎結構案例中,這種服務的成本可能較低。

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

檔案上傳案例

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

Buffering

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

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

Note

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

本主題的下列各節介紹了緩衝處理小型檔案:

Streaming

檔案會從多部分請求中接收,並由應用程式直接處理或儲存。 串流傳輸不會顯著提高效能。 串流傳輸可減少上傳檔案時對記憶體或磁碟空間的需求。

使用串流傳輸上傳大型檔案一節中介紹了串流傳輸大型檔案。

使用緩衝模型繫結將小型檔案上傳至實體儲存體

若要上傳小型檔案,請使用多部分表單,或使用 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 的 (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 表單必須指定 enctype 的編碼類型 (multipart/form-data)。

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

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

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

Warning

除了用於顯示和記錄之外,請勿使用 FileNameIFormFile 屬性。 顯示或記錄時,HTML 會對檔名進行編碼。 網路攻擊者會提供惡意的檔名 (包括完整路徑或相對路徑)。 應用程式應該:

  • 從使用者提供的檔名中移除路徑。
  • 儲存已進行 HTML 編碼且移除了路徑的檔名,用於 UI 或記錄。
  • 產生一個新的隨機檔名以用於儲存。

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

string untrustedFileName = Path.GetFileName(pathName);

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

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

Note

綁定會依照名稱匹配表單檔案。 例如,name 中的 HTML <input type="file" name="formFile"> 值必須符合 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

Warning

如果在未刪除先前的暫存檔的情況下建立了超過 65,535 個檔案,則 Path.GetTempFileName 會擲回 IOException。 65,535 個檔案的限制是每部伺服器的限制。 有關 Windows 作業系統上此限制的詳細資訊,請參閱以下主題中的備註:

使用緩衝模型繫結將小型檔案上傳至資料庫

若要使用 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; }
}

Note

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

Warning

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

請勿在未經驗證的情況下依賴或信任 FileNameIFormFile 屬性。 FileName 屬性只能用於顯示用途,而且只能在 HTML 編碼之後使用。

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

使用串流傳輸上傳大型檔案

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

初始頁面回應會載入表單,並透過 GenerateAntiforgeryTokenCookieAttribute 屬性將防偽標記(antiforgery token)儲存在 cookie 中。 此屬性會使用 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)
    {
    }
}

在範例應用程式中,GenerateAntiforgeryTokenCookieAttributeDisableFormValueModelBindingAttribute 作為篩選器,使用 Razor 頁面約定應用到 /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 的內容會用來將表單資料繫結至模型類型。

使用 StreamingController.UploadDatabase 串流傳輸到資料庫的完整 EF Core 方法:

[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 處理。

Validation

範例應用程式的 FileHelpers 類別示範了對緩衝 IFormFile 和串流檔案上傳的數項檢查。 如需在範例應用程式中處理 IFormFile 緩衝檔案上傳,請參閱 ProcessFormFile 檔案中的 Utilities/FileHelpers.cs 方法。 如需處理串流檔案,請參閱相同檔案中的 ProcessStreamedFile 方法。

Warning

範例應用程式中示範的驗證處理方法不會掃描已上傳檔案的內容。 在大部分的生產案例中,在讓使用者或其他系統可以使用檔案之前,會先對檔案使用病毒/惡意程式碼掃描器 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檔案提供的:

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

在非FormData表單中使用POST表單資料或直接使用JavaScript的Razor時,表單元素或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) 的參數使用相符的名稱:

  • 對於一個名為 Upload 的 Pages 頁面處理常式方法 Razor:

    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

Pages 應用程式中,使用約定套用篩選器:

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
{
    ...
}

可以使用 @attributeRazor 指令來應用 RequestSizeLimitAttribute

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

IISServerOptions.MaxRequestBodySize 中設定 Startup.ConfigureServices,以增加 HTTP 要求的最大要求本文大小。 在下列範例中,限制設定為 50 MB (52,428,800 個位元組):

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

如需詳細資訊,請參閱在使用 IIS 的 Windows 上裝載 ASP.NET Core

Troubleshoot

以下是使用上傳檔案和其可能解決方案時發現的一些常見問題。

部署至 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 表單指定了 enctypemultipart/form-data 值。 如果未在 <form> 元素上設定此屬性,則不會進行檔案上傳,且任何繫結的 IFormFile 引數都會是 null。 也要確認表單資料中的上傳命名與應用程式的命名相符

資料串流過長

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

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

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

安全性考慮

為使用者提供將檔案上傳到伺服器的能力時,請特別注意。 網路攻擊者可能會嘗試:

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

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

  • 將檔案上傳至專用的檔案上傳區,最好是非系統磁碟機。 專用的位置可讓您更輕鬆地對上傳的檔案強制實施安全性限制。 停用檔案上傳位置上的執行權限。†
  • 不要將上傳的檔案保存在與應用程式相同的目錄樹狀結構中。†
  • 使用由應用程式所決定的安全檔名。 請勿使用由使用者所提供的檔名或上傳檔案的不受信任檔名。† 顯示不受信任的檔名時,HTML 會對其進行編碼。 例如,記錄檔名或在 UI 中顯示 (Razor 會自動對輸出進行 HTML 編碼)。
  • 僅允許應用程式的設計規範中批准的檔案擴展名。†
  • 驗證用戶端檢查是否已在伺服器上執行。† 用戶端檢查很容易規避。
  • 檢查上傳的檔案大小。 設定大小上限以防止大型上傳。†
  • 當檔案不應被上傳的同名檔案覆蓋時,請在上傳檔案之前先針對資料庫或實體儲存體檢查檔名。
  • 在儲存檔案之前,先對上傳的內容執行病毒/惡意程式碼掃描器。

範例應用程式展示了一種符合準則的方法。

Warning

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

  • 完全取得對系統的控制權。
  • 讓系統過載,進而造成系統當機的結果。
  • 洩漏使用者或系統資料。
  • 在公用 UI 上塗鴉。

如需在接受來自使用者的檔案時減少弱點的資訊,請參閱下列資源:

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

儲存情境

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

  • Database

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

    • 大檔案上傳:
      • 資料庫限制量可能會限制上傳的大小。
      • 實體儲存體通常比資料庫儲存體較貴。
    • 實體儲存體可能比使用資料儲存體服務較便宜。
    • 應用程式的處理序必須具有對儲存體位置的讀取和寫入權限。 永不授與執行權限。
  • 資料儲存體服務 (例如 Azure Blob 儲存體)

    • 與通常容易出現單一失敗點的內部部署解決方案相比,這種服務通常可提供更高的可擴縮性和復原能力。
    • 在大型儲存體基礎結構案例中,這種服務的成本可能較低。

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

檔案上傳案例

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

Buffering

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

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

Note

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

本主題的下列各節介紹了緩衝處理小型檔案:

Streaming

檔案會從多部分請求中接收,並由應用程式直接處理或儲存。 串流傳輸不會顯著提高效能。 串流傳輸可減少上傳檔案時對記憶體或磁碟空間的需求。

使用串流傳輸上傳大型檔案一節中介紹了串流傳輸大型檔案。

使用緩衝模型繫結將小型檔案上傳至實體儲存體

若要上傳小型檔案,請使用多部分表單,或使用 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 的 (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 表單必須指定 enctype 的編碼類型 (multipart/form-data)。

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

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

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

Warning

除了用於顯示和記錄之外,請勿使用 FileNameIFormFile 屬性。 顯示或記錄時,HTML 會對檔名進行編碼。 網路攻擊者會提供惡意的檔名 (包括完整路徑或相對路徑)。 應用程式應該:

  • 從使用者提供的檔名中移除路徑。
  • 儲存已進行 HTML 編碼且移除了路徑的檔名,用於 UI 或記錄。
  • 產生一個新的隨機檔名以用於儲存。

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

string untrustedFileName = Path.GetFileName(pathName);

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

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

Note

綁定會依照名稱匹配表單檔案。 例如,name 中的 HTML <input type="file" name="formFile"> 值必須符合 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

Warning

如果在未刪除先前的暫存檔的情況下建立了超過 65,535 個檔案,則 Path.GetTempFileName 會擲回 IOException。 65,535 個檔案的限制是每部伺服器的限制。 有關 Windows 作業系統上此限制的詳細資訊,請參閱以下主題中的備註:

使用緩衝模型繫結將小型檔案上傳至資料庫

若要使用 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; }
}

Note

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

Warning

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

請勿在未經驗證的情況下依賴或信任 FileNameIFormFile 屬性。 FileName 屬性只能用於顯示用途,而且只能在 HTML 編碼之後使用。

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

使用串流傳輸上傳大型檔案

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

初始頁面回應會載入表單,並透過 GenerateAntiforgeryTokenCookieAttribute 屬性將防偽標記(antiforgery token)儲存在 cookie 中。 此屬性會使用 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)
    {
    }
}

在範例應用程式中,GenerateAntiforgeryTokenCookieAttributeDisableFormValueModelBindingAttribute 作為篩選器,使用 Razor 頁面約定應用到 /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 的內容會用來將表單資料繫結至模型類型。

使用 StreamingController.UploadDatabase 串流傳輸到資料庫的完整 EF Core 方法:

[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 處理。

Validation

範例應用程式的 FileHelpers 類別示範了對緩衝 IFormFile 和串流檔案上傳的數項檢查。 如需在範例應用程式中處理 IFormFile 緩衝檔案上傳,請參閱 ProcessFormFile 檔案中的 Utilities/FileHelpers.cs 方法。 如需處理串流檔案,請參閱相同檔案中的 ProcessStreamedFile 方法。

Warning

範例應用程式中示範的驗證處理方法不會掃描已上傳檔案的內容。 在大部分的生產案例中,在讓使用者或其他系統可以使用檔案之前,會先對檔案使用病毒/惡意程式碼掃描器 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 檔案中提供的:

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

在非 FormData 的表單中,無論是 POST 表單資料還是直接使用 JavaScript 的 Razor,表單元素或 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) 的參數使用相符的名稱:

  • 對於一個名為 Upload 的 Pages 頁面處理常式方法 Razor:

    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

Pages 應用程式中,使用約定套用篩選器:

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

Pages 應用程式中,使用約定套用篩選器:

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>

IISServerOptions.MaxRequestBodySize 中設置 Startup.ConfigureServices,以增加 HTTP 請求的最大正文大小。 在下列範例中,限制設定為 50 MB (52,428,800 個位元組):

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

如需詳細資訊,請參閱在使用 IIS 的 Windows 上裝載 ASP.NET Core

Troubleshoot

以下是使用上傳檔案和其可能解決方案時發現的一些常見問題。

部署至 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 表單指定了 enctypemultipart/form-data 值。 如果未在 <form> 元素上設定此屬性,則不會進行檔案上傳,且任何繫結的 IFormFile 引數都會是 null。 也要確認表單資料中的上傳命名與應用程式的命名相符

資料串流過長

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

其他資源