ASP.NET Core 中的 Upload 檔案

Steve SmithRutger 風暴

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

查看或下載範例程式碼 (如何下載)

安全性考量

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

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

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

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

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

警告

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

  • 完全掌握系統的控制權。
  • 使用系統損毀的結果來多載系統。
  • 洩漏使用者或系統資料。
  • 將刻套用至公用 UI。

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

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

儲存體案例

檔案的一般儲存選項包括:

  • 資料庫

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

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

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

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

檔案上傳案例

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

緩衝處理

系統會將整個檔案讀入 IFormFile ,這是用來處理或儲存檔案之檔案的 c # 標記法。

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

注意

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

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

串流

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

串流大型檔案包含在具有串流的 Upload 大型檔案一節中。

Upload 使用緩衝模型系結至實體儲存體的小型檔案

若要上傳小型檔案,請使用多部分形式的表單,或使用 JavaScript 來建立 POST 要求。

下列範例將示範 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 的 (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 中執行表單貼文,請使用下列其中一種方法:

  • 使用 Fetch Polyfill (例如, 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 會將檔案名編碼。 攻擊者可以提供惡意檔案名,包括完整路徑或相對路徑。 應用程式應該:

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

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

string untrustedFileName = Path.GetFileName(pathName);

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

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

注意

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

下列範例將:

  • 迴圈一或多個上傳的檔案。
  • 使用 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 來產生不含路徑的檔案名。 在下列範例中,路徑是從 configuration 取得:

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。

警告

GetTempFileName 會擲回, IOException 如果在不刪除先前的暫存檔的情況下建立65535個以上的檔案,則擲回。 65535檔案的限制為每個伺服器的限制。 如需有關 Windows OS 的這項限制的詳細資訊,請參閱下列主題中的備註:

Upload 具有緩衝模型系結至資料庫的小型檔案

若要使用 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 頁面表單中使用:

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

當表單張貼至伺服器時,將複製 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,.cs

警告

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

請勿依賴或信任 FileName IFormFile 不含驗證的屬性。 FileName屬性只能用於顯示目的,而且只適用于 HTML 編碼。

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

使用串流 Upload 大型檔案

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

初始頁面回應會載入表單,並透過屬性) 將 antiforgery token 儲存在 cookie (中 GenerateAntiforgeryTokenCookieAttribute 。 屬性使用 ASP.NET Core 的內建antiforgery 支援來設定 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/StreamedSingleFileUploadDb /StreamedSingleFileUploadPhysical Startup.ConfigureServices 使用 Razor 頁面慣例,並在和的頁面應用程式模型中套用為篩選:

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 會用來將表單資料系結至模型類型。

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 (公用程式/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 公用程式/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));
}

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

檔案名安全性

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

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
}

將名稱屬性值與 POST 方法的參數名稱相符

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

在下例中︰

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

    <input type="file" name="battlePlans" multiple>
    
  • FormData在 JavaScript 中使用時,名稱會設定為下列值 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 在剖析時擲回。 預設值為 134217728 (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 頁面應用程式中,將篩選套用至下列慣例 Startup.ConfigureServices

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

在 Razor 頁面應用程式或 MVC 應用程式中,將篩選套用至頁面模型或動作方法:

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

Kestrel 要求主體大小上限

若為所裝載的應用程式 Kestrel ,預設的要求主體大小上限為30000000個位元組,大約是 28.6 MB。 使用 >limits.maxrequestbodysize Kestrel 伺服器選項自訂限制:

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 用來設定單一頁面或動作的 >limits.maxrequestbodysize

在 Razor 頁面應用程式中,將篩選套用至下列慣例 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也可以使用指示詞套用 @attribute Razor :

@attribute [RequestSizeLimitAttribute(52428800)]

其他 Kestrel 限制

Kestrel下列應用程式可能會套用其他限制 Kestrel :

IIS

預設的要求限制 (maxAllowedContentLength) 為30000000個位元組,大約 28.6 MB。 自訂檔案中的限制 web.config 。 在下列範例中,此限制設定為 50 MB (52428800 bytes) :

<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 支援針對較小的檔案使用緩衝的模型系結上傳一或多個檔案,並針對較大的檔案上傳未緩衝

查看或下載範例程式碼 (如何下載)

安全性考量

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

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

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

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

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

警告

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

  • 完全掌握系統的控制權。
  • 使用系統損毀的結果來多載系統。
  • 洩漏使用者或系統資料。
  • 將刻套用至公用 UI。

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

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

儲存體案例

檔案的一般儲存選項包括:

  • 資料庫

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

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

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

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

檔案上傳案例

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

緩衝處理

系統會將整個檔案讀入 IFormFile ,這是用來處理或儲存檔案之檔案的 c # 標記法。

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

注意

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

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

串流

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

串流大型檔案包含在具有串流的 Upload 大型檔案一節中。

Upload 使用緩衝模型系結至實體儲存體的小型檔案

若要上傳小型檔案,請使用多部分形式的表單,或使用 JavaScript 來建立 POST 要求。

下列範例將示範 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 的 (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 中執行表單貼文,請使用下列其中一種方法:

  • 使用 Fetch Polyfill (例如, 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 會將檔案名編碼。 攻擊者可以提供惡意檔案名,包括完整路徑或相對路徑。 應用程式應該:

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

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

string untrustedFileName = Path.GetFileName(pathName);

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

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

注意

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

下列範例將:

  • 迴圈一或多個上傳的檔案。
  • 使用 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 來產生不含路徑的檔案名。 在下列範例中,路徑是從 configuration 取得:

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。

警告

GetTempFileName 會擲回, IOException 如果在不刪除先前的暫存檔的情況下建立65535個以上的檔案,則擲回。 65535檔案的限制為每個伺服器的限制。 如需有關 Windows OS 的這項限制的詳細資訊,請參閱下列主題中的備註:

Upload 具有緩衝模型系結至資料庫的小型檔案

若要使用 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 頁面表單中使用:

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

當表單張貼至伺服器時,將複製 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,.cs

警告

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

請勿依賴或信任 FileName IFormFile 不含驗證的屬性。 FileName屬性只能用於顯示目的,而且只適用于 HTML 編碼。

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

使用串流 Upload 大型檔案

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

初始頁面回應會載入表單,並透過屬性) 將 antiforgery token 儲存在 cookie (中 GenerateAntiforgeryTokenCookieAttribute 。 屬性使用 ASP.NET Core 的內建antiforgery 支援來設定 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/StreamedSingleFileUploadDb /StreamedSingleFileUploadPhysical Startup.ConfigureServices 使用 Razor 頁面慣例,並在和的頁面應用程式模型中套用為篩選:

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 會用來將表單資料系結至模型類型。

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 (公用程式/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 公用程式/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));
}

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

檔案名安全性

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

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
}

將名稱屬性值與 POST 方法的參數名稱相符

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

在下例中︰

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

    <input type="file" name="battlePlans" multiple>
    
  • FormData在 JavaScript 中使用時,名稱會設定為下列值 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 在剖析時擲回。 預設值為 134217728 (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 頁面應用程式中,將篩選套用至下列慣例 Startup.ConfigureServices

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

在 Razor 頁面應用程式或 MVC 應用程式中,將篩選套用至頁面模型或動作方法:

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

Kestrel 要求主體大小上限

若為所裝載的應用程式 Kestrel ,預設的要求主體大小上限為30000000個位元組,大約是 28.6 MB。 使用 >limits.maxrequestbodysize Kestrel 伺服器選項自訂限制:

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 用來設定單一頁面或動作的 >limits.maxrequestbodysize

在 Razor 頁面應用程式中,將篩選套用至下列慣例 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也可以使用指示詞套用 @attribute Razor :

@attribute [RequestSizeLimitAttribute(52428800)]

其他 Kestrel 限制

Kestrel下列應用程式可能會套用其他限制 Kestrel :

IIS

預設的要求限制 (maxAllowedContentLength) 為30000000個位元組,大約 28.6 MB。 自訂檔案中的限制 web.config 。 在下列範例中,此限制設定為 50 MB (52428800 bytes) :

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

maxAllowedContentLength 設定僅適用于 IIS。 如需詳細資訊,請參閱要求限制 <requestLimits>

在中設定,以增加 HTTP 要求的要求主體大小 IISServerOptions.MaxRequestBodySize 上限 Startup.ConfigureServices 。 在下列範例中,此限制設定為 50 MB (52428800 bytes) :

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 支援針對較小的檔案使用緩衝的模型系結上傳一或多個檔案,並針對較大的檔案上傳未緩衝

查看或下載範例程式碼 (如何下載)

安全性考量

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

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

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

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

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

警告

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

  • 完全掌握系統的控制權。
  • 使用系統損毀的結果來多載系統。
  • 洩漏使用者或系統資料。
  • 將刻套用至公用 UI。

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

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

儲存體案例

檔案的一般儲存選項包括:

  • 資料庫

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

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

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

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

檔案上傳案例

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

緩衝處理

系統會將整個檔案讀入 IFormFile ,這是用來處理或儲存檔案之檔案的 c # 標記法。

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

注意

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

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

串流

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

串流大型檔案包含在具有串流的 Upload 大型檔案一節中。

Upload 使用緩衝模型系結至實體儲存體的小型檔案

若要上傳小型檔案,請使用多部分形式的表單,或使用 JavaScript 來建立 POST 要求。

下列範例將示範 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 的 (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 中執行表單貼文,請使用下列其中一種方法:

  • 使用 Fetch Polyfill (例如, 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 會將檔案名編碼。 攻擊者可以提供惡意檔案名,包括完整路徑或相對路徑。 應用程式應該:

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

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

string untrustedFileName = Path.GetFileName(pathName);

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

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

注意

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

下列範例將:

  • 迴圈一或多個上傳的檔案。
  • 使用 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 來產生不含路徑的檔案名。 在下列範例中,路徑是從 configuration 取得:

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。

警告

GetTempFileName 會擲回, IOException 如果在不刪除先前的暫存檔的情況下建立65535個以上的檔案,則擲回。 65535檔案的限制為每個伺服器的限制。 如需有關 Windows OS 的這項限制的詳細資訊,請參閱下列主題中的備註:

Upload 具有緩衝模型系結至資料庫的小型檔案

若要使用 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 頁面表單中使用:

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

當表單張貼至伺服器時,將複製 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,.cs

警告

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

請勿依賴或信任 FileName IFormFile 不含驗證的屬性。 FileName屬性只能用於顯示目的,而且只適用于 HTML 編碼。

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

使用串流 Upload 大型檔案

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

初始頁面回應會載入表單,並透過屬性) 將 antiforgery token 儲存在 cookie (中 GenerateAntiforgeryTokenCookieAttribute 。 屬性使用 ASP.NET Core 的內建antiforgery 支援來設定 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/StreamedSingleFileUploadDb /StreamedSingleFileUploadPhysical Startup.ConfigureServices 使用 Razor 頁面慣例,並在和的頁面應用程式模型中套用為篩選:

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 會用來將表單資料系結至模型類型。

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 (公用程式/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 公用程式/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));
}

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

檔案名安全性

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

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
}

將名稱屬性值與 POST 方法的參數名稱相符

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

在下例中︰

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

    <input type="file" name="battlePlans" multiple>
    
  • FormData在 JavaScript 中使用時,名稱會設定為下列值 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 在剖析時擲回。 預設值為 134217728 (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 頁面應用程式中,將篩選套用至下列慣例 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 頁面應用程式或 MVC 應用程式中,將篩選套用至頁面模型或動作方法:

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

Kestrel 要求主體大小上限

若為所裝載的應用程式 Kestrel ,預設的要求主體大小上限為30000000個位元組,大約是 28.6 MB。 使用 >limits.maxrequestbodysize Kestrel 伺服器選項自訂限制:

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 用來設定單一頁面或動作的 >limits.maxrequestbodysize

在 Razor 頁面應用程式中,將篩選套用至下列慣例 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) 為30000000個位元組,大約 28.6 MB。 自訂檔案中的限制 web.config 。 在下列範例中,此限制設定為 50 MB (52428800 bytes) :

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

maxAllowedContentLength 設定僅適用于 IIS。 如需詳細資訊,請參閱要求限制 <requestLimits>

在中設定,以增加 HTTP 要求的要求主體大小 IISServerOptions.MaxRequestBodySize 上限 Startup.ConfigureServices 。 在下列範例中,此限制設定為 50 MB (52428800 bytes) :

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 的方法來保存上傳的檔案內容。

其他資源