在 ASP.NET Core 中上传文件Upload files in ASP.NET Core

作者:Luke LathamSteve SmithRutger StormBy Luke Latham, Steve Smith, and Rutger Storm

ASP.NET Core 支持使用缓冲的模型绑定(针对较小文件)和无缓冲的流式传输(针对较大文件)上传一个或多个文件。ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

查看或下载示例代码如何下载View or download sample code (how to download)

安全注意事项Security considerations

向用户提供向服务器上传文件的功能时,必须格外小心。Use caution when providing users with the ability to upload files to a server. 攻击者可能会尝试执行以下操作:Attackers may attempt to:

  • 执行拒绝服务攻击。Execute denial of service attacks.
  • 上传病毒或恶意软件。Upload viruses or malware.
  • 以其他方式破坏网络和服务器。Compromise networks and servers in other ways.

降低成功攻击可能性的安全措施如下:Security steps that reduce the likelihood of a successful attack are:

  • 将文件上传到专用文件上传区域,最好是非系统驱动器。Upload files to a dedicated file upload area, preferably to a non-system drive. 使用专用位置便于对上传的文件实施安全限制。A dedicated location makes it easier to impose security restrictions on uploaded files. 禁用对文件上传位置的执行权限。†Disable execute permissions on the file upload location.†
  • 请勿将上传的文件保存在与应用相同的目录树中 。†Do not persist uploaded files in the same directory tree as the app.†
  • 使用应用确定的安全的文件名。Use a safe file name determined by the app. 请勿使用用户提供的文件名,或上传的文件的不受信任的文件名。†显示时,HTML 对不受信任的文件名进行编码。Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. 例如,在 UI 中记录文件名或显示(Razor 自动对输出进行 HTML 编码)。For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
  • 仅允许应用设计规范的已批准文件扩展名。†Allow only approved file extensions for the app's design specification.†
  • 验证是否在服务器上执行了客户端检查。†客户端检查很容易规避。Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
  • 检查已上传文件的大小。Check the size of an uploaded file. 设置大小上限以防止上传大型文件。†Set a maximum size limit to prevent large uploads.†
  • 文件不应该被具有相同名称的上传文件覆盖时,先在数据库或物理存储上检查文件名,然后再上传文件。When files shouldn't be overwritten by an uploaded file with the same name, check the file name against the database or physical storage before uploading the file.
  • 先对上传的内容运行病毒/恶意软件扫描程序,然后再存储文件。Run a virus/malware scanner on uploaded content before the file is stored.

†示例应用演示了符合条件的方法。†The sample app demonstrates an approach that meets the criteria.

警告

将恶意代码上传到系统通常是执行代码的第一步,这些代码可以:Uploading malicious code to a system is frequently the first step to executing code that can:

  • 完全获得对系统的控制权限。Completely gain control of a system.
  • 重载系统,导致系统崩溃。Overload a system with the result that the system crashes.
  • 泄露用户或系统数据。Compromise user or system data.
  • 将涂鸦应用于公共 UI。Apply graffiti to a public UI.

有关在接受用户文件时减少攻击外围应用的信息,请参阅以下资源:For information on reducing the attack surface area when accepting files from users, see the following resources:

有关实现安全措施(包括示例应用中的示例)的详细信息,请参阅验证部分。For more information on implementing security measures, including examples from the sample app, see the Validation section.

存储方案Storage scenarios

常见的文件存储选项有:Common storage options for files include:

  • 数据库Database

    • 对于小型文件上传,数据库通常快于物理存储(文件系统或网络共享)选项。For small file uploads, a database is often faster than physical storage (file system or network share) options.
    • 相对于物理存储选项,数据库通常更为便利,因为检索数据库记录来获取用户数据可同时提供文件内容(如头像图像)。A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
    • 相对于使用数据存储服务,数据库的成本可能更低。A database is potentially less expensive than using a data storage service.
  • 物理存储(文件系统或网络共享)Physical storage (file system or network share)

    • 对于大型文件上传:For large file uploads:
      • 数据库限制可能会限制上传的大小。Database limits may restrict the size of the upload.
      • 相对于数据库存储,物理存储通常成本更高。Physical storage is often less economical than storage in a database.
    • 相对于使用数据存储服务,物理存储的成本可能更低。Physical storage is potentially less expensive than using a data storage service.
    • 应用的进程必须具有存储位置的读写权限。The app's process must have read and write permissions to the storage location. 切勿授予执行权限。 Never grant execute permission.
  • 数据存储服务(例如,Azure Blob 存储Data storage service (for example, Azure Blob Storage)

    • 服务通常通过本地解决方案提供提升的可伸缩性和复原能力,而它们往往受单一故障点的影响。Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
    • 在大型存储基础结构方案中,服务的成本可能更低。Services are potentially lower cost in large storage infrastructure scenarios.

    有关详细信息,请参阅快速入门:使用 .NET 在对象存储中创建 BlobFor more information, see Quickstart: Use .NET to create a blob in object storage. 此主题说明了 UploadFromFileAsync,但在处理 Stream 时,可以使用 UploadFromStreamAsyncFileStream 保存到 blob 存储。The topic demonstrates UploadFromFileAsync, but UploadFromStreamAsync can be used to save a FileStream to blob storage when working with a Stream.

文件上传方案File upload scenarios

缓冲和流式传输是上传文件的两种常见方法。Two general approaches for uploading files are buffering and streaming.

缓冲Buffering

整个文件读入 IFormFile,它是文件的 C# 表示形式,用于处理或保存文件。The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.

文件上传所用的资源(磁盘、内存)取决于并发文件上传的数量和大小。The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. 如果应用尝试缓冲过多上传,站点就会在内存或磁盘空间不足时崩溃。If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. 如果文件上传的大小或频率会消耗应用资源,请使用流式传输。If the size or frequency of file uploads is exhausting app resources, use streaming.

备注

会将大于 64 KB 的所有单个缓冲文件从内存移到磁盘的临时文件。Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

本主题的以下部分介绍了如何缓冲小型文件:Buffering small files is covered in the following sections of this topic:

流式处理Streaming

从多部分请求收到文件,然后应用直接处理或保存它。The file is received from a multipart request and directly processed or saved by the app. 流式传输无法显著提高性能。Streaming doesn't improve performance significantly. 流式传输可降低上传文件时对内存或磁盘空间的需求。Streaming reduces the demands for memory or disk space when uploading files.

通过流式传输上传大型文件部分介绍了如何流式传输大型文件。Streaming large files is covered in the Upload large files with streaming section.

通过缓冲的模型绑定将小型文件上传到物理存储Upload small files with buffered model binding to physical storage

要上传小文件,请使用多部分窗体或使用 JavaScript 构造 POST 请求。To upload small files, use a multipart form or construct a POST request using JavaScript.

下面的示例演示了使用 Razor Pages 窗体上传单个文件(示例应用中的 Pages/BufferedSingleFileUploadPhysical.cshtml ):The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml in the sample app):

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

下面的示例与前面的示例类似,不同之处在于:The following example is analogous to the prior example except that:

  • 使用 JavaScript (Fetch API) 提交窗体的数据。JavaScript's (Fetch API) is used to submit the form's data.
  • 无验证。There's no validation.
<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>

若要使用 JavaScript 为不支持 Fetch API 的客户端执行窗体发布,请使用以下方法之一:To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:

  • 使用 Fetch Polyfill(例如,window.fetch polyfill (github/fetch))。Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).

  • 请使用 XMLHttpRequestUse XMLHttpRequest. 例如:For example:

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

为支持文件上传,HTML 窗体必须指定 multipart/form-data 的编码类型 (enctype)。In order to support file uploads, HTML forms must specify an encoding type (enctype) of multipart/form-data.

要使 files 输入元素支持上传多个文件,请在 <input> 元素上提供 multiple 属性:For a files input element to support uploading multiple files provide the multiple attribute on the <input> element:

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

上传到服务器的单个文件可使用 IFormFile 接口通过模型绑定进行访问。The individual files uploaded to the server can be accessed through Model Binding using IFormFile. 示例应用演示了数据库和物理存储方案的多个缓冲文件上传。The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

警告

除了显示和日志记录用途外,请勿使用 IFormFileFileName 属性 。Do not use the FileName property of IFormFile other than for display and logging. 显示或日志记录时,HTML 对文件名进行编码。When displaying or logging, HTML encode the file name. 攻击者可以提供恶意文件名,包括完整路径或相对路径。An attacker can provide a malicious filename, including full paths or relative paths. 应用程序应:Applications should:

  • 从用户提供的文件名中删除路径。Remove the path from the user-supplied filename.
  • 为 UI 或日志记录保存经 HTML 编码、已删除路径的文件名。Save the HTML-encoded, path-removed filename for UI or logging.
  • 生成新的随机文件名进行存储。Generate a new random filename for storage.

以下代码可从文件名中删除路径:The following code removes the path from the file name:

string untrustedFileName = Path.GetFileName(pathName);

目前提供的示例未考虑安全注意事项。The examples provided thus far don't take into account security considerations. 以下各节及示例应用提供了其他信息:Additional information is provided by the following sections and the sample app:

使用模型绑定和 IFormFile 上传文件时,操作方法可以接受以下内容:When uploading files using model binding and IFormFile, the action method can accept:

备注

绑定根据名称匹配窗体文件。Binding matches form files by name. 例如,<input type="file" name="formFile"> 中的 HTML name 值必须与 C# 参数/属性绑定 (FormFile) 匹配。For example, the HTML name value in <input type="file" name="formFile"> must match the C# parameter/property bound (FormFile). 有关详细信息,请参阅使名称属性值与 POST 方法的参数名匹配部分。For more information, see the Match name attribute value to parameter name of POST method section.

如下示例中:The following example:

  • 循环访问一个或多个上传的文件。Loops through one or more uploaded files.
  • 使用 Path.GetTempFileName 返回文件的完整路径,包括文件名称。Uses Path.GetTempFileName to return a full path for a file, including the file name.
  • 使用应用生成的文件名将文件保存到本地文件系统。Saves the files to the local file system using a file name generated by the app.
  • 返回上传的文件的总数量和总大小。Returns the total number and size of files uploaded.
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, filePath });
}

使用 Path.GetRandomFileName 生成文件名(不含路径)。Use Path.GetRandomFileName to generate a file name without a path. 在下面的示例中,从配置获取路径:In the following example, the path is obtained from 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 的路径必须包含文件名 。The path passed to the FileStream must include the file name. 如果未提供文件名,则会在运行时引发 UnauthorizedAccessExceptionIf the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.

使用 IFormFile 技术上传的文件在处理之前会缓冲在内存中或服务器的磁盘中。Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. 在操作方法中,IFormFile 内容可作为 Stream 访问。Inside the action method, the IFormFile contents are accessible as a Stream. 除本地文件系统之外,还可以将文件保存到网络共享或文件存储服务,如 Azure Blob 存储In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.

若要查看循环访问要上传的多个文件并且使用安全文件名的其他示例,请参阅示例应用中的 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs。 For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs in the sample app.

警告

如果在未删除先前临时文件的情况下创建了 65,535 个以上的文件,则 Path.GetTempFileName 将抛出一个 IOExceptionPath.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. 65,535 个文件限制是每个服务器的限制。The limit of 65,535 files is a per-server limit. 有关 Windows 操作系统上的此限制的详细信息,请参阅以下主题中的说明:For more information on this limit on Windows OS, see the remarks in the following topics:

使用缓冲的模型绑定将小型文件上传到数据库Upload small files with buffered model binding to a database

要使用实体框架将二进制文件数据存储在数据库中,请在实体上定义 Byte 数组属性:To store binary file data in a database using Entity Framework, define a Byte array property on the entity:

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

为包括 IFormFile 的类指定页模型属性:Specify a page model property for the class that includes an IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

备注

IFormFile 可以直接用作操作方法参数或绑定模型属性。IFormFile can be used directly as an action method parameter or as a bound model property. 前面的示例使用绑定模型属性。The prior example uses a bound model property.

在 Razor Pages 窗体中使用 FileUploadThe FileUpload is used in the Razor Pages form:

<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 复制到流,并将它作为字节数组保存在数据库中。When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. 在下面的示例中,_dbContext 存储应用的数据库上下文:In the following example, _dbContext stores the app's database context:

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

上面的示例与示例应用中演示的方案相似:The preceding example is similar to a scenario demonstrated in the sample app:

  • Pages/BufferedSingleFileUploadDb.cshtmlPages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.csPages/BufferedSingleFileUploadDb.cshtml.cs

警告

在关系数据库中存储二进制数据时要格外小心,因为它可能对性能产生不利影响。Use caution when storing binary data in relational databases, as it can adversely impact performance.

切勿依赖或信任未经验证的 IFormFileFileName 属性。Don't rely on or trust the FileName property of IFormFile without validation. 只应将 FileName 属性用于显示用途,并且只应在进行 HTML 编码后使用它。The FileName property should only be used for display purposes and only after HTML encoding.

提供的示例未考虑安全注意事项。The examples provided don't take into account security considerations. 以下各节及示例应用提供了其他信息:Additional information is provided by the following sections and the sample app:

通过流式传输上传大型文件Upload large files with streaming

以下示例演示如何使用 JavaScript 将文件流式传输到控制器操作。The following example demonstrates how to use JavaScript to stream a file to a controller action. 使用自定义筛选器属性生成文件的防伪令牌,并将其传递到客户端 HTTP 头中(而不是在请求正文中传递)。The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. 由于操作方法直接处理上传的数据,所以其他自定义筛选器会禁用窗体模型绑定。Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. 在该操作中,使用 MultipartReader 读取窗体的内容,它会读取每个单独的 MultipartSection,从而根据需要处理文件或存储内容。Within the action, the form's contents are read using a MultipartReader, which reads each individual MultipartSection, processing the file or storing the contents as appropriate. 读取多部分节后,该操作会执行自己的模型绑定。After the multipart sections are read, the action performs its own model binding.

初始页响应加载窗体并将防伪令牌保存在 Cookie 中(通过 GenerateAntiforgeryTokenCookieAttribute 属性)。The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute attribute). 该属性使用 ASP.NET Core 的内置防伪支持来设置包含请求令牌的 Cookie:The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:

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 禁用模型绑定:The DisableFormValueModelBindingAttribute is used to disable model binding:

[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)
    {
    }
}

在示例应用的 Startup.ConfigureServices 中,使用 Razor Pages 约定GenerateAntiforgeryTokenCookieAttributeDisableFormValueModelBindingAttribute 作为筛选器应用到 /StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysical 的页应用程序模型:In the sample app, GenerateAntiforgeryTokenCookieAttribute and DisableFormValueModelBindingAttribute are applied as filters to the page application models of /StreamedSingleFileUploadDb and /StreamedSingleFileUploadPhysical in Startup.ConfigureServices using Razor Pages conventions:

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

由于模型绑定不读取窗体,因此不绑定从窗体绑定的参数(查询、路由和标头继续运行)。Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). 操作方法直接使用 Request 属性。The action method works directly with the Request property. MultipartReader 用于读取每个节。A MultipartReader is used to read each section. KeyValueAccumulator 中存储键值数据。Key/value data is stored in a KeyValueAccumulator. 读取多部分节后,系统会使用 KeyValueAccumulator 的内容将窗体数据绑定到模型类型。After the multipart sections are read, the contents of the KeyValueAccumulator are used to bind the form data to a model type.

使用 EF Core 流式传输到数据库的完整 StreamingController.UploadDatabase 方法:The complete StreamingController.UploadDatabase method for streaming to a database with 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 = new byte[0];

    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): 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 方法:The complete StreamingController.UploadPhysical method for streaming to a physical location:

[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 处理验证检查。In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile.

验证Validation

示例应用的 FileHelpers 类演示对缓冲 IFormFile 和流式传输文件上传的多项检查。The sample app's FileHelpers class demonstrates a several checks for buffered IFormFile and streamed file uploads. 有关示例应用如何处理 IFormFile 缓冲文件上传的信息,请参阅 Utilities/FileHelpers.cs 文件中的 ProcessFormFile 方法。 For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile method in the Utilities/FileHelpers.cs file. 有关如何处理流式传输的文件的信息,请参阅同一个文件中的 ProcessStreamedFile 方法。For processing streamed files, see the ProcessStreamedFile method in the same file.

警告

示例应用演示的验证处理方法不扫描上传的文件的内容。The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. 在多数生产方案中,会先将病毒/恶意软件扫描程序 API 用于文件,然后再向用户或其他系统提供文件。In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

尽管主题示例提供了验证技巧工作示例,但是如果不满足以下情况,请勿在生产应用中实现 FileHelpers 类:Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers class in a production app unless you:

  • 完全理解此实现。Fully understand the implementation.
  • 根据应用的环境和规范修改实现。Modify the implementation as appropriate for the app's environment and specifications.

切勿未处理这些要求即随意在应用中实现安全代码。Never indiscriminately implement security code in an app without addressing these requirements.

内容验证Content validation

将第三方病毒/恶意软件扫描 API 用于上传的内容Use a third party virus/malware scanning API on uploaded content.

在大容量方案中,在服务器资源上扫描文件较为困难。Scanning files is demanding on server resources in high volume scenarios. 若文件扫描导致请求处理性能降低,请考虑将扫描工作卸载到后台服务,该服务可以是在应用服务器之外的服务器上运行的服务。If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. 通常会将卸载的文件保留在隔离区,直至后台病毒扫描程序检查它们。Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. 文件通过检查时,会将相应的文件移到常规的文件存储位置。When a file passes, the file is moved to the normal file storage location. 通常在执行这些步骤的同时,会提供指示文件扫描状态的数据库记录。These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. 通过此方法,应用和应用服务器可以持续以响应请求为重点。By using such an approach, the app and app server remain focused on responding to requests.

文件扩展名验证File extension validation

应在允许的扩展名列表中查找上传的文件的扩展名。The uploaded file's extension should be checked against a list of permitted extensions. 例如:For example:

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
}

文件签名验证File signature validation

文件的签名由文件开头部分中的前几个字节确定。A file's signature is determined by the first few bytes at the start of a file. 可以使用这些字节指示扩展名是否与文件内容匹配。These bytes can be used to indicate if the extension matches the content of the file. 示例应用检查一些常见文件类型的文件签名。The sample app checks file signatures for a few common file types. 在下面的示例中,在文件上检查 JPEG 图像的文件签名:In the following example, the file signature for a JPEG image is checked against the file:

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

若要获取其他文件签名,请参阅文件签名数据库和官方文件规范。To obtain additional file signatures, see the File Signatures Database and official file specifications.

文件名安全File name security

切勿使用客户端提供的文件名来将文件保存到物理存储。Never use a client-supplied file name for saving a file to physical storage. 使用 Path.GetRandomFileNamePath.GetTempFileName 为文件创建安全的文件名,以创建完整路径(包括文件名)来执行临时存储。Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.

Razor 自动对属性值执行 HTML 编码,以便于显示。Razor automatically HTML encodes property values for display. 以下代码安全可用:The following code is safe to use:

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

在 Razor 外部,始终对用户请求中的文件名内容执行 HtmlEncodeOutside of Razor, always HtmlEncode file name content from a user's request.

许多实现都必须包含关于文件是否存在的检查;否则文件会被使用相同名称的文件覆盖。Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. 提供其他逻辑以符合应用的规范。Supply additional logic to meet your app's specifications.

大小验证Size validation

限制上传的文件的大小。Limit the size of uploaded files.

在示例应用中,文件大小限制为 2 MB(以字节为单位)。In the sample app, the size of the file is limited to 2 MB (indicated in bytes). 通过 appsettings.json 文件中的配置提供此限制: The limit is supplied via Configuration from the appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit 注入到 PageModel 类:The FileSizeLimit is injected into PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

文件大小超出限制时,将拒绝文件:When a file size exceeds the limit, the file is rejected:

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

使名称属性值与 POST 方法的参数名称匹配Match name attribute value to parameter name of POST method

在发布窗体数据或直接使用 JavaScript FormData 的非 Razor 窗体中,窗体元素或 FormData 指定的名称必须与控制器操作的参数名称相匹配。In non-Razor forms that POST form data or use JavaScript's FormData directly, the name specified in the form's element or FormData must match the name of the parameter in the controller's action.

如下示例中:In the following example:

  • 使用 <input> 元素时,将 name 属性设置为值 battlePlansWhen using an <input> element, the name attribute is set to the value battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • 使用 JavaScript FormData 时,将名称设置为值 battlePlansWhen using FormData in JavaScript, the name is set to the value battlePlans:

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

将匹配的名称用于 C# 方法的参数 (battlePlans):Use a matching name for the parameter of the C# method (battlePlans):

  • 对于名称为 Upload 的 Razor Pages 页处理程序方法:For a Razor Pages page handler method named Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • 对于 MVC POST 控制器操作方法:For an MVC POST controller action method:

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

服务器和应用程序配置Server and app configuration

多部分正文长度限制Multipart body length limit

MultipartBodyLengthLimit 设置每个多部分正文的长度限制。MultipartBodyLengthLimit sets the limit for the length of each multipart body. 分析超出此限制的窗体部分时,会引发 InvalidDataExceptionForm sections that exceed this limit throw an InvalidDataException when parsed. 默认值为 134,217,728 (128 MB)。The default is 134,217,728 (128 MB). 使用 Startup.ConfigureServices 中的 MultipartBodyLengthLimit 设置自定义此限制:Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices:

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

使用 RequestFormLimitsAttribute 设置单个页面或操作的 MultipartBodyLengthLimitRequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.

在 Razor Pages 应用中,使用 Startup.ConfigureServices 中的约定应用筛选器:In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices:

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

在 Razor Pages 应用或 MVC 应用中,将筛选器应用到页面模型或操作方法:In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

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

Kestrel 最大请求正文大小Kestrel maximum request body size

对于 Kestrel 托管的应用,默认的最大请求正文大小为 30,000,000 个字节,约为 28.6 MB。For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. 使用 MaxRequestBodySize Kestrel 服务器选项自定义限制:Customize the limit using the MaxRequestBodySize Kestrel server option:

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

使用 RequestSizeLimitAttribute 设置单个页面或操作的 MaxRequestBodySizeRequestSizeLimitAttribute is used to set the MaxRequestBodySize for a single page or action.

在 Razor Pages 应用中,使用 Startup.ConfigureServices 中的约定应用筛选器:In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices:

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

在 Razor Pages 应用或 MVC 应用中,将筛选器应用到页面处理程序类或操作方法:In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

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

也可以使用 @attribute Razor 指令应用 RequestSizeLimitAttributeThe RequestSizeLimitAttribute can also be applied using the @attribute Razor directive:

@attribute [RequestSizeLimitAttribute(52428800)]

其他 Kestrel 限制Other Kestrel limits

其他 Kestrel 限制可能适用于 Kestrel 托管的应用:Other Kestrel limits may apply for apps hosted by Kestrel:

IIS 内容长度限制IIS content length limit

默认的请求限制 (maxAllowedContentLength) 为 30,000,000 字节,大约 28.6 MB。The default request limit (maxAllowedContentLength) is 30,000,000 bytes, which is approximately 28.6MB. 请在 web.config 文件中自定义此限制: Customize the limit in the web.config file:

<system.webServer>
  <security>
    <requestFiltering>
      <!-- Handle requests up to 50 MB -->
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>

此设置仅适用于 IIS。This setting only applies to IIS. 在 Kestrel 上托管时,默认情况下不会出现此行为。The behavior doesn't occur by default when hosting on Kestrel. 有关详细信息,请参阅请求限制 <requestLimits>For more information, see Request Limits <requestLimits>.

ASP.NET Core 模块中的限制或 IIS 请求筛选模块的存在可能会将上传限制在 2 或 4 GB。Limitations in the ASP.NET Core Module or presence of the IIS Request Filtering Module may limit uploads to either 2 or 4 GB. 有关详细信息,请参阅无法上传大小超出 2 GB 的文件 (aspnet/AspNetCore #2711)For more information, see Unable to upload file greater than 2GB in size (aspnet/AspNetCore #2711).

疑难解答Troubleshoot

以下是上传文件时遇到的一些常见问题及其可能的解决方案。Below are some common problems encountered when working with uploading files and their possible solutions.

部署到 IIS 服务器时出现“找不到”错误Not Found error when deployed to an IIS server

以下错误表示上传的文件超过服务器配置的内容长度:The following error indicates that the uploaded file exceeds the server's configured content length:

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

有关提高此限制的详细信息,请参阅 IIS 内容长度限制部分。For more information on increasing the limit, see the IIS content length limit section.

连接失败Connection failure

连接错误和重置服务器连接可能表示上传的文件超出 Kestrel 的最大请求正文大小。A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. 有关详细信息,请参阅 Kestrel 最大请求正文大小部分。For more information, see the Kestrel maximum request body size section. 可能还需要调整 Kestrel 客户端连接限制。Kestrel client connection limits may also require adjustment.

IFormFile 的空引用异常Null Reference Exception with IFormFile

如果控制器正在接受使用 IFormFile 上传的文件,但该值为 null,请确认 HTML 窗体指定的 multipart/form-data 值是否为 enctypeIf the controller is accepting uploaded files using IFormFile but the value is null, confirm that the HTML form is specifying an enctype value of multipart/form-data. 如果未在 <form> 元素上设置此属性,则不会发生文件上传,并且任何绑定的 IFormFile 参数都为 nullIf this attribute isn't set on the <form> element, the file upload doesn't occur and any bound IFormFile arguments are null. 此外,请确认窗体数据中的上传命名是否与应用的命名相匹配Also confirm that the upload naming in form data matches the app's naming.

数据流太长Stream was too long

本主题中的示例依赖于 MemoryStream 来保存已上传的文件的内容。The examples in this topic rely upon MemoryStream to hold the uploaded file's content. MemoryStream 的大小限制为 int.MaxValueThe size limit of a MemoryStream is int.MaxValue. 如果应用的文件上传方案要求保存大于 50 MB 的文件内容,请使用另一种方法,该方法不依赖单个 MemoryStream 来保存已上传文件的内容。If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream for holding an uploaded file's content.

ASP.NET Core 支持使用缓冲的模型绑定(针对较小文件)和无缓冲的流式传输(针对较大文件)上传一个或多个文件。ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

查看或下载示例代码如何下载View or download sample code (how to download)

安全注意事项Security considerations

向用户提供向服务器上传文件的功能时,必须格外小心。Use caution when providing users with the ability to upload files to a server. 攻击者可能会尝试执行以下操作:Attackers may attempt to:

  • 执行拒绝服务攻击。Execute denial of service attacks.
  • 上传病毒或恶意软件。Upload viruses or malware.
  • 以其他方式破坏网络和服务器。Compromise networks and servers in other ways.

降低成功攻击可能性的安全措施如下:Security steps that reduce the likelihood of a successful attack are:

  • 将文件上传到专用文件上传区域,最好是非系统驱动器。Upload files to a dedicated file upload area, preferably to a non-system drive. 使用专用位置便于对上传的文件实施安全限制。A dedicated location makes it easier to impose security restrictions on uploaded files. 禁用对文件上传位置的执行权限。†Disable execute permissions on the file upload location.†
  • 请勿将上传的文件保存在与应用相同的目录树中 。†Do not persist uploaded files in the same directory tree as the app.†
  • 使用应用确定的安全的文件名。Use a safe file name determined by the app. 请勿使用用户提供的文件名,或上传的文件的不受信任的文件名。†显示时,HTML 对不受信任的文件名进行编码。Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. 例如,在 UI 中记录文件名或显示(Razor 自动对输出进行 HTML 编码)。For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
  • 仅允许应用设计规范的已批准文件扩展名。†Allow only approved file extensions for the app's design specification.†
  • 验证是否在服务器上执行了客户端检查。†客户端检查很容易规避。Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
  • 检查已上传文件的大小。Check the size of an uploaded file. 设置大小上限以防止上传大型文件。†Set a maximum size limit to prevent large uploads.†
  • 文件不应该被具有相同名称的上传文件覆盖时,先在数据库或物理存储上检查文件名,然后再上传文件。When files shouldn't be overwritten by an uploaded file with the same name, check the file name against the database or physical storage before uploading the file.
  • 先对上传的内容运行病毒/恶意软件扫描程序,然后再存储文件。Run a virus/malware scanner on uploaded content before the file is stored.

†示例应用演示了符合条件的方法。†The sample app demonstrates an approach that meets the criteria.

警告

将恶意代码上传到系统通常是执行代码的第一步,这些代码可以:Uploading malicious code to a system is frequently the first step to executing code that can:

  • 完全获得对系统的控制权限。Completely gain control of a system.
  • 重载系统,导致系统崩溃。Overload a system with the result that the system crashes.
  • 泄露用户或系统数据。Compromise user or system data.
  • 将涂鸦应用于公共 UI。Apply graffiti to a public UI.

有关在接受用户文件时减少攻击外围应用的信息,请参阅以下资源:For information on reducing the attack surface area when accepting files from users, see the following resources:

有关实现安全措施(包括示例应用中的示例)的详细信息,请参阅验证部分。For more information on implementing security measures, including examples from the sample app, see the Validation section.

存储方案Storage scenarios

常见的文件存储选项有:Common storage options for files include:

  • 数据库Database

    • 对于小型文件上传,数据库通常快于物理存储(文件系统或网络共享)选项。For small file uploads, a database is often faster than physical storage (file system or network share) options.
    • 相对于物理存储选项,数据库通常更为便利,因为检索数据库记录来获取用户数据可同时提供文件内容(如头像图像)。A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
    • 相对于使用数据存储服务,数据库的成本可能更低。A database is potentially less expensive than using a data storage service.
  • 物理存储(文件系统或网络共享)Physical storage (file system or network share)

    • 对于大型文件上传:For large file uploads:
      • 数据库限制可能会限制上传的大小。Database limits may restrict the size of the upload.
      • 相对于数据库存储,物理存储通常成本更高。Physical storage is often less economical than storage in a database.
    • 相对于使用数据存储服务,物理存储的成本可能更低。Physical storage is potentially less expensive than using a data storage service.
    • 应用的进程必须具有存储位置的读写权限。The app's process must have read and write permissions to the storage location. 切勿授予执行权限。 Never grant execute permission.
  • 数据存储服务(例如,Azure Blob 存储Data storage service (for example, Azure Blob Storage)

    • 服务通常通过本地解决方案提供提升的可伸缩性和复原能力,而它们往往受单一故障点的影响。Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
    • 在大型存储基础结构方案中,服务的成本可能更低。Services are potentially lower cost in large storage infrastructure scenarios.

    有关详细信息,请参阅快速入门:使用 .NET 在对象存储中创建 BlobFor more information, see Quickstart: Use .NET to create a blob in object storage. 此主题说明了 UploadFromFileAsync,但在处理 Stream 时,可以使用 UploadFromStreamAsyncFileStream 保存到 blob 存储。The topic demonstrates UploadFromFileAsync, but UploadFromStreamAsync can be used to save a FileStream to blob storage when working with a Stream.

文件上传方案File upload scenarios

缓冲和流式传输是上传文件的两种常见方法。Two general approaches for uploading files are buffering and streaming.

缓冲Buffering

整个文件读入 IFormFile,它是文件的 C# 表示形式,用于处理或保存文件。The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.

文件上传所用的资源(磁盘、内存)取决于并发文件上传的数量和大小。The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. 如果应用尝试缓冲过多上传,站点就会在内存或磁盘空间不足时崩溃。If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. 如果文件上传的大小或频率会消耗应用资源,请使用流式传输。If the size or frequency of file uploads is exhausting app resources, use streaming.

备注

会将大于 64 KB 的所有单个缓冲文件从内存移到磁盘的临时文件。Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

本主题的以下部分介绍了如何缓冲小型文件:Buffering small files is covered in the following sections of this topic:

流式处理Streaming

从多部分请求收到文件,然后应用直接处理或保存它。The file is received from a multipart request and directly processed or saved by the app. 流式传输无法显著提高性能。Streaming doesn't improve performance significantly. 流式传输可降低上传文件时对内存或磁盘空间的需求。Streaming reduces the demands for memory or disk space when uploading files.

通过流式传输上传大型文件部分介绍了如何流式传输大型文件。Streaming large files is covered in the Upload large files with streaming section.

通过缓冲的模型绑定将小型文件上传到物理存储Upload small files with buffered model binding to physical storage

要上传小文件,请使用多部分窗体或使用 JavaScript 构造 POST 请求。To upload small files, use a multipart form or construct a POST request using JavaScript.

下面的示例演示了使用 Razor Pages 窗体上传单个文件(示例应用中的 Pages/BufferedSingleFileUploadPhysical.cshtml ):The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml in the sample app):

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

下面的示例与前面的示例类似,不同之处在于:The following example is analogous to the prior example except that:

  • 使用 JavaScript (Fetch API) 提交窗体的数据。JavaScript's (Fetch API) is used to submit the form's data.
  • 无验证。There's no validation.
<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>

若要使用 JavaScript 为不支持 Fetch API 的客户端执行窗体发布,请使用以下方法之一:To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:

  • 使用 Fetch Polyfill(例如,window.fetch polyfill (github/fetch))。Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).

  • 请使用 XMLHttpRequestUse XMLHttpRequest. 例如:For example:

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

为支持文件上传,HTML 窗体必须指定 multipart/form-data 的编码类型 (enctype)。In order to support file uploads, HTML forms must specify an encoding type (enctype) of multipart/form-data.

要使 files 输入元素支持上传多个文件,请在 <input> 元素上提供 multiple 属性:For a files input element to support uploading multiple files provide the multiple attribute on the <input> element:

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

上传到服务器的单个文件可使用 IFormFile 接口通过模型绑定进行访问。The individual files uploaded to the server can be accessed through Model Binding using IFormFile. 示例应用演示了数据库和物理存储方案的多个缓冲文件上传。The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

警告

除了显示和日志记录用途外,请勿使用 IFormFileFileName 属性 。Do not use the FileName property of IFormFile other than for display and logging. 显示或日志记录时,HTML 对文件名进行编码。When displaying or logging, HTML encode the file name. 攻击者可以提供恶意文件名,包括完整路径或相对路径。An attacker can provide a malicious filename, including full paths or relative paths. 应用程序应:Applications should:

  • 从用户提供的文件名中删除路径。Remove the path from the user-supplied filename.
  • 为 UI 或日志记录保存经 HTML 编码、已删除路径的文件名。Save the HTML-encoded, path-removed filename for UI or logging.
  • 生成新的随机文件名进行存储。Generate a new random filename for storage.

以下代码可从文件名中删除路径:The following code removes the path from the file name:

string untrustedFileName = Path.GetFileName(pathName);

目前提供的示例未考虑安全注意事项。The examples provided thus far don't take into account security considerations. 以下各节及示例应用提供了其他信息:Additional information is provided by the following sections and the sample app:

使用模型绑定和 IFormFile 上传文件时,操作方法可以接受以下内容:When uploading files using model binding and IFormFile, the action method can accept:

备注

绑定根据名称匹配窗体文件。Binding matches form files by name. 例如,<input type="file" name="formFile"> 中的 HTML name 值必须与 C# 参数/属性绑定 (FormFile) 匹配。For example, the HTML name value in <input type="file" name="formFile"> must match the C# parameter/property bound (FormFile). 有关详细信息,请参阅使名称属性值与 POST 方法的参数名匹配部分。For more information, see the Match name attribute value to parameter name of POST method section.

如下示例中:The following example:

  • 循环访问一个或多个上传的文件。Loops through one or more uploaded files.
  • 使用 Path.GetTempFileName 返回文件的完整路径,包括文件名称。Uses Path.GetTempFileName to return a full path for a file, including the file name.
  • 使用应用生成的文件名将文件保存到本地文件系统。Saves the files to the local file system using a file name generated by the app.
  • 返回上传的文件的总数量和总大小。Returns the total number and size of files uploaded.
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, filePath });
}

使用 Path.GetRandomFileName 生成文件名(不含路径)。Use Path.GetRandomFileName to generate a file name without a path. 在下面的示例中,从配置获取路径:In the following example, the path is obtained from 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 的路径必须包含文件名 。The path passed to the FileStream must include the file name. 如果未提供文件名,则会在运行时引发 UnauthorizedAccessExceptionIf the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.

使用 IFormFile 技术上传的文件在处理之前会缓冲在内存中或服务器的磁盘中。Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. 在操作方法中,IFormFile 内容可作为 Stream 访问。Inside the action method, the IFormFile contents are accessible as a Stream. 除本地文件系统之外,还可以将文件保存到网络共享或文件存储服务,如 Azure Blob 存储In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.

若要查看循环访问要上传的多个文件并且使用安全文件名的其他示例,请参阅示例应用中的 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs。 For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs in the sample app.

警告

如果在未删除先前临时文件的情况下创建了 65,535 个以上的文件,则 Path.GetTempFileName 将抛出一个 IOExceptionPath.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. 65,535 个文件限制是每个服务器的限制。The limit of 65,535 files is a per-server limit. 有关 Windows 操作系统上的此限制的详细信息,请参阅以下主题中的说明:For more information on this limit on Windows OS, see the remarks in the following topics:

使用缓冲的模型绑定将小型文件上传到数据库Upload small files with buffered model binding to a database

要使用实体框架将二进制文件数据存储在数据库中,请在实体上定义 Byte 数组属性:To store binary file data in a database using Entity Framework, define a Byte array property on the entity:

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

为包括 IFormFile 的类指定页模型属性:Specify a page model property for the class that includes an IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

备注

IFormFile 可以直接用作操作方法参数或绑定模型属性。IFormFile can be used directly as an action method parameter or as a bound model property. 前面的示例使用绑定模型属性。The prior example uses a bound model property.

在 Razor Pages 窗体中使用 FileUploadThe FileUpload is used in the Razor Pages form:

<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 复制到流,并将它作为字节数组保存在数据库中。When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. 在下面的示例中,_dbContext 存储应用的数据库上下文:In the following example, _dbContext stores the app's database context:

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

上面的示例与示例应用中演示的方案相似:The preceding example is similar to a scenario demonstrated in the sample app:

  • Pages/BufferedSingleFileUploadDb.cshtmlPages/BufferedSingleFileUploadDb.cshtml
  • Pages/BufferedSingleFileUploadDb.cshtml.csPages/BufferedSingleFileUploadDb.cshtml.cs

警告

在关系数据库中存储二进制数据时要格外小心,因为它可能对性能产生不利影响。Use caution when storing binary data in relational databases, as it can adversely impact performance.

切勿依赖或信任未经验证的 IFormFileFileName 属性。Don't rely on or trust the FileName property of IFormFile without validation. 只应将 FileName 属性用于显示用途,并且只应在进行 HTML 编码后使用它。The FileName property should only be used for display purposes and only after HTML encoding.

提供的示例未考虑安全注意事项。The examples provided don't take into account security considerations. 以下各节及示例应用提供了其他信息:Additional information is provided by the following sections and the sample app:

通过流式传输上传大型文件Upload large files with streaming

以下示例演示如何使用 JavaScript 将文件流式传输到控制器操作。The following example demonstrates how to use JavaScript to stream a file to a controller action. 使用自定义筛选器属性生成文件的防伪令牌,并将其传递到客户端 HTTP 头中(而不是在请求正文中传递)。The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. 由于操作方法直接处理上传的数据,所以其他自定义筛选器会禁用窗体模型绑定。Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. 在该操作中,使用 MultipartReader 读取窗体的内容,它会读取每个单独的 MultipartSection,从而根据需要处理文件或存储内容。Within the action, the form's contents are read using a MultipartReader, which reads each individual MultipartSection, processing the file or storing the contents as appropriate. 读取多部分节后,该操作会执行自己的模型绑定。After the multipart sections are read, the action performs its own model binding.

初始页响应加载窗体并将防伪令牌保存在 Cookie 中(通过 GenerateAntiforgeryTokenCookieAttribute 属性)。The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute attribute). 该属性使用 ASP.NET Core 的内置防伪支持来设置包含请求令牌的 Cookie:The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:

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 禁用模型绑定:The DisableFormValueModelBindingAttribute is used to disable model binding:

[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)
    {
    }
}

在示例应用的 Startup.ConfigureServices 中,使用 Razor Pages 约定GenerateAntiforgeryTokenCookieAttributeDisableFormValueModelBindingAttribute 作为筛选器应用到 /StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysical 的页应用程序模型:In the sample app, GenerateAntiforgeryTokenCookieAttribute and DisableFormValueModelBindingAttribute are applied as filters to the page application models of /StreamedSingleFileUploadDb and /StreamedSingleFileUploadPhysical in Startup.ConfigureServices using Razor Pages conventions:

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

由于模型绑定不读取窗体,因此不绑定从窗体绑定的参数(查询、路由和标头继续运行)。Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). 操作方法直接使用 Request 属性。The action method works directly with the Request property. MultipartReader 用于读取每个节。A MultipartReader is used to read each section. KeyValueAccumulator 中存储键值数据。Key/value data is stored in a KeyValueAccumulator. 读取多部分节后,系统会使用 KeyValueAccumulator 的内容将窗体数据绑定到模型类型。After the multipart sections are read, the contents of the KeyValueAccumulator are used to bind the form data to a model type.

使用 EF Core 流式传输到数据库的完整 StreamingController.UploadDatabase 方法:The complete StreamingController.UploadDatabase method for streaming to a database with 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 = new byte[0];

    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): 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 方法:The complete StreamingController.UploadPhysical method for streaming to a physical location:

[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 处理验证检查。In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile.

验证Validation

示例应用的 FileHelpers 类演示对缓冲 IFormFile 和流式传输文件上传的多项检查。The sample app's FileHelpers class demonstrates a several checks for buffered IFormFile and streamed file uploads. 有关示例应用如何处理 IFormFile 缓冲文件上传的信息,请参阅 Utilities/FileHelpers.cs 文件中的 ProcessFormFile 方法。 For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile method in the Utilities/FileHelpers.cs file. 有关如何处理流式传输的文件的信息,请参阅同一个文件中的 ProcessStreamedFile 方法。For processing streamed files, see the ProcessStreamedFile method in the same file.

警告

示例应用演示的验证处理方法不扫描上传的文件的内容。The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. 在多数生产方案中,会先将病毒/恶意软件扫描程序 API 用于文件,然后再向用户或其他系统提供文件。In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

尽管主题示例提供了验证技巧工作示例,但是如果不满足以下情况,请勿在生产应用中实现 FileHelpers 类:Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers class in a production app unless you:

  • 完全理解此实现。Fully understand the implementation.
  • 根据应用的环境和规范修改实现。Modify the implementation as appropriate for the app's environment and specifications.

切勿未处理这些要求即随意在应用中实现安全代码。Never indiscriminately implement security code in an app without addressing these requirements.

内容验证Content validation

将第三方病毒/恶意软件扫描 API 用于上传的内容Use a third party virus/malware scanning API on uploaded content.

在大容量方案中,在服务器资源上扫描文件较为困难。Scanning files is demanding on server resources in high volume scenarios. 若文件扫描导致请求处理性能降低,请考虑将扫描工作卸载到后台服务,该服务可以是在应用服务器之外的服务器上运行的服务。If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. 通常会将卸载的文件保留在隔离区,直至后台病毒扫描程序检查它们。Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. 文件通过检查时,会将相应的文件移到常规的文件存储位置。When a file passes, the file is moved to the normal file storage location. 通常在执行这些步骤的同时,会提供指示文件扫描状态的数据库记录。These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. 通过此方法,应用和应用服务器可以持续以响应请求为重点。By using such an approach, the app and app server remain focused on responding to requests.

文件扩展名验证File extension validation

应在允许的扩展名列表中查找上传的文件的扩展名。The uploaded file's extension should be checked against a list of permitted extensions. 例如:For example:

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
}

文件签名验证File signature validation

文件的签名由文件开头部分中的前几个字节确定。A file's signature is determined by the first few bytes at the start of a file. 可以使用这些字节指示扩展名是否与文件内容匹配。These bytes can be used to indicate if the extension matches the content of the file. 示例应用检查一些常见文件类型的文件签名。The sample app checks file signatures for a few common file types. 在下面的示例中,在文件上检查 JPEG 图像的文件签名:In the following example, the file signature for a JPEG image is checked against the file:

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

若要获取其他文件签名,请参阅文件签名数据库和官方文件规范。To obtain additional file signatures, see the File Signatures Database and official file specifications.

文件名安全File name security

切勿使用客户端提供的文件名来将文件保存到物理存储。Never use a client-supplied file name for saving a file to physical storage. 使用 Path.GetRandomFileNamePath.GetTempFileName 为文件创建安全的文件名,以创建完整路径(包括文件名)来执行临时存储。Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.

Razor 自动对属性值执行 HTML 编码,以便于显示。Razor automatically HTML encodes property values for display. 以下代码安全可用:The following code is safe to use:

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

在 Razor 外部,始终对用户请求中的文件名内容执行 HtmlEncodeOutside of Razor, always HtmlEncode file name content from a user's request.

许多实现都必须包含关于文件是否存在的检查;否则文件会被使用相同名称的文件覆盖。Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. 提供其他逻辑以符合应用的规范。Supply additional logic to meet your app's specifications.

大小验证Size validation

限制上传的文件的大小。Limit the size of uploaded files.

在示例应用中,文件大小限制为 2 MB(以字节为单位)。In the sample app, the size of the file is limited to 2 MB (indicated in bytes). 通过 appsettings.json 文件中的配置提供此限制: The limit is supplied via Configuration from the appsettings.json file:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit 注入到 PageModel 类:The FileSizeLimit is injected into PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

文件大小超出限制时,将拒绝文件:When a file size exceeds the limit, the file is rejected:

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

使名称属性值与 POST 方法的参数名称匹配Match name attribute value to parameter name of POST method

在发布窗体数据或直接使用 JavaScript FormData 的非 Razor 窗体中,窗体元素或 FormData 指定的名称必须与控制器操作的参数名称相匹配。In non-Razor forms that POST form data or use JavaScript's FormData directly, the name specified in the form's element or FormData must match the name of the parameter in the controller's action.

如下示例中:In the following example:

  • 使用 <input> 元素时,将 name 属性设置为值 battlePlansWhen using an <input> element, the name attribute is set to the value battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • 使用 JavaScript FormData 时,将名称设置为值 battlePlansWhen using FormData in JavaScript, the name is set to the value battlePlans:

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

将匹配的名称用于 C# 方法的参数 (battlePlans):Use a matching name for the parameter of the C# method (battlePlans):

  • 对于名称为 Upload 的 Razor Pages 页处理程序方法:For a Razor Pages page handler method named Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • 对于 MVC POST 控制器操作方法:For an MVC POST controller action method:

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

服务器和应用程序配置Server and app configuration

多部分正文长度限制Multipart body length limit

MultipartBodyLengthLimit 设置每个多部分正文的长度限制。MultipartBodyLengthLimit sets the limit for the length of each multipart body. 分析超出此限制的窗体部分时,会引发 InvalidDataExceptionForm sections that exceed this limit throw an InvalidDataException when parsed. 默认值为 134,217,728 (128 MB)。The default is 134,217,728 (128 MB). 使用 Startup.ConfigureServices 中的 MultipartBodyLengthLimit 设置自定义此限制:Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices:

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

使用 RequestFormLimitsAttribute 设置单个页面或操作的 MultipartBodyLengthLimitRequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.

在 Razor Pages 应用中,使用 Startup.ConfigureServices 中的约定应用筛选器:In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices:

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

在 Razor Pages 应用或 MVC 应用中,将筛选器应用到页面模型或操作方法:In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

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

Kestrel 最大请求正文大小Kestrel maximum request body size

对于 Kestrel 托管的应用,默认的最大请求正文大小为 30,000,000 个字节,约为 28.6 MB。For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. 使用 MaxRequestBodySize Kestrel 服务器选项自定义限制:Customize the limit using the MaxRequestBodySize Kestrel server option:

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 设置单个页面或操作的 MaxRequestBodySizeRequestSizeLimitAttribute is used to set the MaxRequestBodySize for a single page or action.

在 Razor Pages 应用中,使用 Startup.ConfigureServices 中的约定应用筛选器:In a Razor Pages app, apply the filter with a convention in 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 Pages 应用或 MVC 应用中,将筛选器应用到页面处理程序类或操作方法:In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

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

其他 Kestrel 限制Other Kestrel limits

其他 Kestrel 限制可能适用于 Kestrel 托管的应用:Other Kestrel limits may apply for apps hosted by Kestrel:

IIS 内容长度限制IIS content length limit

默认的请求限制 (maxAllowedContentLength) 为 30,000,000 字节,大约 28.6 MB。The default request limit (maxAllowedContentLength) is 30,000,000 bytes, which is approximately 28.6MB. 请在 web.config 文件中自定义此限制: Customize the limit in the web.config file:

<system.webServer>
  <security>
    <requestFiltering>
      <!-- Handle requests up to 50 MB -->
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>

此设置仅适用于 IIS。This setting only applies to IIS. 在 Kestrel 上托管时,默认情况下不会出现此行为。The behavior doesn't occur by default when hosting on Kestrel. 有关详细信息,请参阅请求限制 <requestLimits>For more information, see Request Limits <requestLimits>.

ASP.NET Core 模块中的限制或 IIS 请求筛选模块的存在可能会将上传限制在 2 或 4 GB。Limitations in the ASP.NET Core Module or presence of the IIS Request Filtering Module may limit uploads to either 2 or 4 GB. 有关详细信息,请参阅无法上传大小超出 2 GB 的文件 (aspnet/AspNetCore #2711)For more information, see Unable to upload file greater than 2GB in size (aspnet/AspNetCore #2711).

疑难解答Troubleshoot

以下是上传文件时遇到的一些常见问题及其可能的解决方案。Below are some common problems encountered when working with uploading files and their possible solutions.

部署到 IIS 服务器时出现“找不到”错误Not Found error when deployed to an IIS server

以下错误表示上传的文件超过服务器配置的内容长度:The following error indicates that the uploaded file exceeds the server's configured content length:

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

有关提高此限制的详细信息,请参阅 IIS 内容长度限制部分。For more information on increasing the limit, see the IIS content length limit section.

连接失败Connection failure

连接错误和重置服务器连接可能表示上传的文件超出 Kestrel 的最大请求正文大小。A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. 有关详细信息,请参阅 Kestrel 最大请求正文大小部分。For more information, see the Kestrel maximum request body size section. 可能还需要调整 Kestrel 客户端连接限制。Kestrel client connection limits may also require adjustment.

IFormFile 的空引用异常Null Reference Exception with IFormFile

如果控制器正在接受使用 IFormFile 上传的文件,但该值为 null,请确认 HTML 窗体指定的 multipart/form-data 值是否为 enctypeIf the controller is accepting uploaded files using IFormFile but the value is null, confirm that the HTML form is specifying an enctype value of multipart/form-data. 如果未在 <form> 元素上设置此属性,则不会发生文件上传,并且任何绑定的 IFormFile 参数都为 nullIf this attribute isn't set on the <form> element, the file upload doesn't occur and any bound IFormFile arguments are null. 此外,请确认窗体数据中的上传命名是否与应用的命名相匹配Also confirm that the upload naming in form data matches the app's naming.

数据流太长Stream was too long

本主题中的示例依赖于 MemoryStream 来保存已上传的文件的内容。The examples in this topic rely upon MemoryStream to hold the uploaded file's content. MemoryStream 的大小限制为 int.MaxValueThe size limit of a MemoryStream is int.MaxValue. 如果应用的文件上传方案要求保存大于 50 MB 的文件内容,请使用另一种方法,该方法不依赖单个 MemoryStream 来保存已上传文件的内容。If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream for holding an uploaded file's content.

其他资源Additional resources