Partilhar via


Carregar arquivos no ASP.NET Core

Por Rutger Storm

O ASP.NET Core oferece suporte ao upload de um ou mais arquivos usando vinculação de modelo com buffer para arquivos menores e streaming sem buffer para arquivos maiores.

Exibir ou baixar código de exemplo (como baixar)

Considerações sobre segurança

Tenha cuidado ao fornecer aos usuários a capacidade de fazer upload de arquivos para um servidor. Os ciberinvasores podem tentar:

  • Executar ataques de negação de serviço.
  • Carregar vírus ou malware.
  • Comprometer redes e servidores de outras maneiras.

As etapas de segurança que reduzem a probabilidade de um ataque bem-sucedido são:

  • Carregue arquivos em uma área de carregamento de arquivo dedicada, preferencialmente para uma unidade que não seja do sistema. Um local dedicado facilita a imposição de restrições de segurança em arquivos carregados. Desabilitar permissões de execução no local de upload do arquivo.†
  • Não mantenha os arquivos carregados na mesma árvore de diretórios do aplicativo.†
  • Use um nome de arquivo seguro determinado pelo aplicativo. Não use um nome de arquivo fornecido pelo usuário ou o nome de arquivo não confiável do arquivo carregado.† HTML codifica o nome do arquivo não confiável ao exibi-lo. Por exemplo, registrar o nome do arquivo ou exibi-lo na interface do usuário (Razor codifica automaticamente a saída html).
  • Permitir somente extensões de arquivo aprovadas para a especificação de design do aplicativo.†
  • Verifique se as verificações do lado do cliente são executadas no servidor.† as verificações do lado do cliente são fáceis de contornar.
  • Verifique o tamanho de um arquivo carregado. Defina um limite de tamanho máximo para evitar uploads grandes.†
  • Quando os arquivos não devem ser substituídos por um arquivo carregado com o mesmo nome, marcar o nome do arquivo no banco de dados ou no armazenamento físico antes de carregar o arquivo.
  • Execute um scanner de vírus/malware no conteúdo carregado antes que o arquivo seja armazenado.

†O aplicativo de exemplo demonstra uma abordagem que atende aos critérios.

Aviso

Carregar códigos mal-intencionados em um sistema é frequentemente a primeira etapa para executar o código que pode:

  • Obtenha completamente o controle de um sistema.
  • Sobrecarregar um sistema, resultando em seu travamento.
  • Comprometer dados do sistema ou de usuários.
  • Aplicar grafite em uma UI pública.

Para obter informações de como reduzir as vulnerabilidades ao aceitar arquivos de usuários, consulte os seguintes recursos:

Para obter mais informações sobre como implementar medidas de segurança, incluindo exemplos do aplicativo de exemplo, consulte a seção Validação.

Cenários de armazenamento

As opções comuns de armazenamento para arquivos incluem:

  • Banco de dados

    • Para carregamentos de arquivos pequenos, um banco de dados geralmente é mais rápido do que as opções de armazenamento físico (sistema de arquivos ou compartilhamento de rede).
    • Um banco de dados geralmente é mais conveniente do que as opções de armazenamento físico porque a recuperação de um registro de banco de dados para dados do usuário pode fornecer simultaneamente o conteúdo do arquivo (por exemplo, uma imagem de avatar).
    • Um banco de dados é potencialmente menos caro do que usar um serviço de armazenamento de dados em nuvem.
  • Armazenamento físico (sistema de arquivos ou compartilhamento de rede)

    • Para carregamentos de arquivos grandes:
      • Os limites de banco de dados podem restringir o tamanho do upload.
      • O armazenamento físico geralmente é menos econômico do que o armazenamento em um banco de dados.
    • O armazenamento físico é potencialmente menos caro do que usar um serviço de armazenamento de dados em nuvem.
    • O processo do aplicativo deve ter permissões de leitura e gravação no local de armazenamento. Nunca conceda permissão de execução.
  • O serviço de armazenamento de dados de nuvem, por exemplo, Armazenamento de Blobs do Azure.

    • Os serviços geralmente oferecem escalabilidade e resiliência aprimoradas em soluções locais que geralmente estão sujeitas a pontos únicos de falha.
    • Os serviços são um custo potencialmente menor em grandes cenários de infraestrutura de armazenamento.

    Para obter mais informações, confira Início Rápido: usar o .NET para criar um blob no armazenamento de objetos.

Arquivos pequenos e grandes

A definição de arquivos pequenos e grandes depende dos recursos de computação disponíveis. Os aplicativos devem comparar a abordagem de armazenamento usada para garantir que ela possa lidar com os tamanhos esperados. Memória de parâmetro de comparação, CPU, disco e desempenho do banco de dados.

Embora limites específicos não possam ser fornecidos no que é pequeno versus grande para sua implantação, aqui estão alguns dos padrões relacionados do ASP.NET Core de FormOptions (documentação da API):

  • Por padrão, HttpRequest.Form não armazena em buffer todo o corpo da solicitação (BufferBody), mas armazena em buffer todos os arquivos de formulário de várias partes incluídos.
  • MultipartBodyLengthLimit é o tamanho máximo dos arquivos de formulário armazenados em buffer (padrão: 128 MB).
  • MemoryBufferThreshold indica o limite de armazenamento em buffer na memória antes de fazer a transição para um arquivo de buffer no disco (padrão: 64 KB). MemoryBufferThreshold atua como um limite entre arquivos pequenos e grandes, que são aumentados ou reduzidos dependendo dos recursos e cenários dos aplicativos.

Para mais informações sobre FormOptions, consulte a classe FormOptions na fonte de referência do ASP.NET Core.

Observação

Os links de documentação para a fonte de referência do .NET geralmente carregam o branch padrão do repositório, que representa o desenvolvimento atual da próxima versão do .NET. Para selecionar uma marca para uma versão específica, use a lista suspensa para Alternar branches ou marcas. Para saber mais, confira Como selecionar uma marca de versão do código-fonte do ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Cenários de carregamento de arquivo

Duas abordagens gerais para carregar arquivos são buffer e streaming.

de resposta

O arquivo inteiro é lido em um IFormFile. IFormFile é uma representação C# do arquivo usado para processar ou salvar o arquivo.

O disco e a memória usados pelos uploads de arquivos dependem do número e do tamanho dos uploads de arquivos simultâneos. Se um aplicativo tentar armazenar em buffer muitos uploads, o site será bloqueado quando ficar sem memória ou espaço em disco. Se o tamanho ou a frequência de uploads de arquivos estiver esgotando os recursos do aplicativo, use streaming.

Qualquer arquivo armazenado em buffer que exceda 64 KB é movido da memória para um arquivo temporário no disco.

Arquivos temporários para solicitações maiores são gravados no local nomeado na variável de ASPNETCORE_TEMP ambiente. Se ASPNETCORE_TEMP não estiver definido, os arquivos serão gravados na pasta temporária do usuário atual.

O buffer de arquivos pequenos é abordado nas seções a seguir deste tópico:

Streaming

O arquivo é recebido de uma solicitação de várias partes e processado ou salvo diretamente pelo aplicativo. O streaming não melhora significativamente o desempenho. O streaming reduz as demandas por memória ou espaço em disco ao carregar arquivos.

O streaming de arquivos grandes é abordado na seção Carregar arquivos grandes com streaming.

Carregar arquivos pequenos com associação de modelo em buffer para armazenamento físico

Para fazer upload de arquivos pequenos, use um formulário com várias partes ou crie uma solicitação POST usando JavaScript.

O exemplo a seguir demonstra o uso de um Razor formulário Páginas para carregar um único arquivo (Pages/BufferedSingleFileUploadPhysical.cshtml no aplicativo de exemplo):

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

O exemplo a seguir é análogo ao exemplo anterior, exceto que:

  • A API do JavaScript (Fetch API) é usada para enviar os dados do formulário.
  • Não há validação.
<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>

Para executar o formulário POST em JavaScript para clientes que não são compatíveis com a Fetch API, use uma das seguintes abordagens:

  • Use um Polyfill fetch (por exemplo, polyfill window.fetch (github/fetch)).

  • Use XMLHttpRequest. Por exemplo:

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

Para oferecer suporte a uploads de arquivos, os formulários HTML devem especificar um tipo de codificação (enctype) de multipart/form-data.

Para que um elemento de entrada files dê suporte ao carregamento de vários arquivos, forneça o atributo multiple no elemento <input>:

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

Os arquivos individuais carregados no servidor podem ser acessados por meio do Model Binding usando o IFormFile. O aplicativo de exemplo demonstra vários uploads de arquivos em buffer para cenários de armazenamento físico e de banco de dados.

Aviso

Não use a propriedade FileName de IFormFile que não seja para exibição e registro em log. Ao exibir ou registrar em log, o HTML codifica o nome do arquivo. Um invasor cibernético pode fornecer um nome de arquivo mal-intencionado, incluindo caminhos completos ou caminhos relativos. Os aplicativos devem:

  • Remova o caminho do nome de arquivo fornecido pelo usuário.
  • Salve o nome de arquivo codificado em HTML e removido pelo caminho para interface do usuário ou registro em log.
  • Gere um novo nome de arquivo aleatório para armazenamento.

O código a seguir remove o caminho do nome do arquivo:

string untrustedFileName = Path.GetFileName(pathName);

Os exemplos fornecidos até agora não levam em conta as considerações de segurança. Informações adicionais são fornecidas pelas seções a seguir e pelo aplicativo de exemplo:

Ao carregar arquivos usando model binding e IFormFile, o método de ação pode aceitar:

Observação

A associação corresponde aos arquivos de formulário por nome. Por exemplo, o valor HTML name em <input type="file" name="formFile"> deve corresponder ao parâmetro/propriedade C# associado (FormFile). Para obter mais informações, consulte a seção Corresponder o valor do atributo de nome ao nome do parâmetro do método POST.

O exemplo a seguir:

  • Executa um loop em um ou mais arquivos carregados.
  • Usa Path.GetTempFileName para retornar um caminho completo para um arquivo, incluindo o nome do arquivo.
  • Salva os arquivos no sistema de arquivos local usando um nome de arquivo gerado pelo aplicativo.
  • Retorna o número total e o tamanho dos arquivos carregados.
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 });
}

Use Path.GetRandomFileName para gerar um nome de arquivo sem um caminho. No exemplo a seguir, o caminho é obtido da configuração:

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

O caminho passado para o FileStream deve incluir o nome do arquivo. Se o nome do arquivo não for fornecido, será lançado um UnauthorizedAccessException no tempo de execução.

Os arquivos carregados usando a técnica IFormFile são armazenados em buffer na memória ou no disco do servidor antes do processamento. Dentro do método de ação, o conteúdo de IFormFile pode ser acessado como Stream. Além do sistema de arquivos local, os arquivos podem ser salvos em um compartilhamento de rede ou em um serviço de armazenamento de arquivos, como o Armazenamento de Blobs do Azure.

Para obter outro exemplo que executa um loop em vários arquivos para upload e usa nomes de arquivo seguros, consulte Pages/BufferedMultipleFileUploadPhysical.cshtml.cs no aplicativo de exemplo.

Aviso

Path.GetTempFileName gera um erro IOException se mais de 65.535 arquivos forem criados sem a exclusão dos arquivos temporários anteriores. O limite de 65.535 arquivos é um limite por servidor. Para obter mais informações sobre esse limite no sistema operacional Windows, consulte os comentários nos seguintes tópicos:

Carregar arquivos pequenos com a associação de modelo em buffer para um banco de dados

Para armazenar dados de arquivos binários em um banco de dados usando o Entity Framework, defina uma propriedade Byte matriz na entidade:

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

Especifique uma propriedade de modelo de página para a classe que inclui um IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Observação

IFormFile pode ser usado diretamente como um parâmetro de método de ação ou como uma propriedade de modelo vinculada. O exemplo anterior usa uma propriedade de modelo associada.

O FileUpload é usado no Razor formulário Páginas:

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

Quando o formulário for POSTed no servidor, copie o IFormFile para um fluxo e salve-o como uma matriz de bytes no banco de dados. No exemplo a seguir, _dbContext armazena o contexto de banco de dados do aplicativo:

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

O exemplo anterior é semelhante a um cenário demonstrado no aplicativo de exemplo:

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

Aviso

Tenha cuidado ao armazenar dados binários em bancos de dados relacionais, pois isso pode afetar negativamente o desempenho.

Não se baseie nem confie na propriedade FileName de IFormFile sem validação. A propriedade FileName só deve ser usada para fins de exibição e somente após a codificação HTML.

Os exemplos fornecidos não levam em conta as considerações de segurança. Informações adicionais são fornecidas pelas seções a seguir e pelo aplicativo de exemplo:

Carregue arquivos grandes com streaming

O exemplo 3.1 demonstra como usar JavaScript para transmitir um arquivo para uma ação do controlador. O token antifalsificação do arquivo é gerado usando um atributo de filtro personalizado e passado para os cabeçalhos HTTP do cliente em vez de no corpo da solicitação. Como o método de ação processa diretamente os dados carregados, a vinculação do modelo de formulário é desativada por outro filtro personalizado. Dentro da ação, o conteúdo do formulário é lido usando um MultipartReader, que lê cada MultipartSection individual, processando o arquivo ou armazenando o conteúdo conforme apropriado. Depois que as seções de várias partes são lidas, a ação executa sua própria associação de modelo.

A resposta inicial da página carrega o formulário e salva um token antifalsificação em um cookie (por meio do atributo GenerateAntiforgeryTokenCookieAttribute). O atributo usa o suporte anti-falsificação integrado do ASP.NET Core para definir um cookie com um token de solicitação:

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

O DisableFormValueModelBindingAttribute é usado para desabilitar a associação de modelo:

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

No aplicativo de exemplo, GenerateAntiforgeryTokenCookieAttribute e DisableFormValueModelBindingAttribute são aplicados como filtros para os modelos de aplicativo de página de /StreamedSingleFileUploadDb e /StreamedSingleFileUploadPhysical em Startup.ConfigureServices usando Razor convenções de Páginas:

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

Como a associação de modelo não lê o formulário, os parâmetros associados do formulário não são associados (consulta, rota e cabeçalho continuam funcionando). O método de ação funciona diretamente com a propriedade Request. Um MultipartReader é usado para ler cada seção. Os dados de chave/valor são armazenados em um KeyValueAccumulator. Depois que as seções de várias partes são lidas, o conteúdo da KeyValueAccumulator é usado para vincular os dados do formulário a um tipo de modelo.

O método completo StreamingController.UploadDatabase para streaming para um banco de dados com EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

O método completo StreamingController.UploadPhysical para streaming para um local físico:

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

No aplicativo de exemplo, as verificações de validação são tratadas por FileHelpers.ProcessStreamedFile.

Validação

A classe do aplicativo de FileHelpers exemplo demonstra várias verificações de uploads de arquivos em buffer IFormFile e transmitidos. Para processar IFormFile uploads de arquivos em buffer no aplicativo de exemplo, consulte o ProcessFormFile método no arquivo Utilities/FileHelpers.cs. Para processar arquivos transmitidos, consulte o ProcessStreamedFile método no mesmo arquivo.

Aviso

Os métodos de processamento de validação demonstrados no aplicativo de exemplo não examinam o conteúdo dos arquivos carregados. Na maioria dos cenários de produção, uma API de scanner de vírus/malware é usada no arquivo antes de disponibilizar o arquivo para usuários ou outros sistemas.

Embora o exemplo de tópico forneça um exemplo funcional de técnicas de validação, não implemente a classe FileHelpers em um aplicativo de produção, a menos que você:

  • Entenda completamente a implementação.
  • Modifique a implementação conforme apropriado para o ambiente e as especificações do aplicativo.

Nunca implemente indiscriminadamente o código de segurança em um aplicativo sem atender a esses requisitos.

Validação de conteúdo

Use uma API de verificação de vírus/malware de terceiros no conteúdo carregado.

A verificação de arquivos exige recursos do servidor em cenários de alto volume. Se o desempenho do processamento de solicitações for reduzido devido à verificação de arquivos, considere descarregar o trabalho de verificação para um serviço em segundo plano, possivelmente um serviço em execução em um servidor diferente do servidor do aplicativo. Normalmente, os arquivos carregados são mantidos em uma área em quarentena até que o verificador de vírus em segundo plano os verifique. Quando um arquivo é passado, o arquivo é movido para o local normal de armazenamento de arquivos. Essas etapas geralmente são executadas em conjunto com um registro de banco de dados que indica o status de verificação de um arquivo. Usando essa abordagem, o aplicativo e o servidor de aplicativos permanecem focados em responder às solicitações.

Validação de extensão de arquivo

A extensão do arquivo carregado deve ser verificada em relação a uma lista de extensões permitidas. Por exemplo:

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
}

Validação de assinatura de arquivo

A assinatura de um arquivo é determinada pelos primeiros bytes no início de um arquivo. Esses bytes podem ser usados para indicar se a extensão corresponde ao conteúdo do arquivo. O aplicativo de exemplo verifica as assinaturas de arquivo para alguns tipos de arquivo comuns. No exemplo a seguir, a assinatura de arquivo para uma imagem JPEG é verificada em relação ao arquivo :

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

Para obter assinaturas de arquivo adicionais, use um banco de dados de assinaturas de arquivo (resultado da pesquisa do Google) e especificações de arquivo oficiais. Consultar especificações de arquivo oficiais pode garantir que as assinaturas selecionadas sejam válidas.

Segurança de nome de arquivo

Nunca use um nome de arquivo fornecido pelo cliente para salvar um arquivo no armazenamento físico. Crie um nome de arquivo seguro para o arquivo usando Path.GetRandomFileName ou Path.GetTempFileName para criar um caminho completo (incluindo o nome do arquivo) para armazenamento temporário.

Razor codifica automaticamente valores de propriedade para exibição. O código a seguir é seguro de usar:

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

Fora do Razor, sempre HtmlEncode o conteúdo do nome do arquivo da solicitação de um usuário.

Muitas implementações devem incluir um marcar que o arquivo existe; caso contrário, o arquivo será substituído por um arquivo de mesmo nome. Forneça lógica adicional para atender às especificações do aplicativo.

Validação de tamanho

Limite o tamanho dos arquivos carregados.

No aplicativo de exemplo, o tamanho do arquivo é limitado a 2 MB (indicado em bytes). O limite é fornecido por meio da Configuração do arquivo appsettings.json:

{
  "FileSizeLimit": 2097152
}

O FileSizeLimit é injetado em PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Quando um tamanho de arquivo excede o limite, o arquivo é rejeitado:

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

Corresponder o valor do atributo de nome ao nome do parâmetro do método POST

Em formuláriosRazor que postam dados ou usam javaScript FormData diretamente, o nome especificado no elemento do formulário ou FormData deve corresponder ao nome do parâmetro na ação do controlador.

No exemplo a seguir:

  • Ao usar um elemento <input>, o atributo name é definido como o valor battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Ao usar FormData em JavaScript, o nome é definido como o valor battlePlans:

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

Use um nome correspondente para o parâmetro do método C# (battlePlans):

  • Para um método manipulador de Páginas de Razor página chamado Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Para um método de ação do controlador POST do MVC:

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

Configuração de servidor e aplicativo

Limite de comprimento do corpo de várias partes

MultipartBodyLengthLimit define o limite para o comprimento de cada corpo de várias partes. Seções de formulário que excedem esse limite lançam um InvalidDataException quando analisado. O padrão é 134.217.728 (128 MB). Personalize o limite usando a configuração MultipartBodyLengthLimit em Startup.ConfigureServices:

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

RequestFormLimitsAttribute é usado para definir o MultipartBodyLengthLimit para uma única página ou ação.

Em um aplicativo Páginas Razor, aplique o filtro com uma convenção de em Startup.ConfigureServices:

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

Em um aplicativo Páginas Razor ou um aplicativo MVC, aplique o filtro ao modelo de página ou ao método de ação:

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

Kestrel tamanho máximo do corpo da solicitação

Para aplicativos hospedados por Kestrel, o tamanho máximo padrão do corpo da solicitação é de 30.000.000 bytes, o que equivale a aproximadamente 28,6 MB. Personalize o limite usando a opção de servidor MaxRequestBodySizeKestrel:

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

RequestSizeLimitAttribute é usado para definir MaxRequestBodySize para uma única página ou ação.

Em um aplicativo Páginas Razor, aplique o filtro com uma convenção de em Startup.ConfigureServices:

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

Em um aplicativo Páginas Razor ou em um aplicativo MVC, aplique o filtro à classe do manipulador de página ou ao método de ação:

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

O RequestSizeLimitAttribute também pode ser aplicado usando a @attributeRazor diretiva :

@attribute [RequestSizeLimitAttribute(52428800)]

Outros Kestrel limites

Outros Kestrel limites podem ser aplicados a aplicativos hospedados por Kestrel:

IIS

O limite de solicitação padrão (maxAllowedContentLength) é de 30.000.000 bytes, o que equivale a aproximadamente 28,6 MB. Personalize o limite no arquivo web.config. No exemplo a seguir, o limite é definido como 50 MB (52.428.800 bytes):

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

A configuração maxAllowedContentLength se aplica somente ao IIS. Para obter mais informações, consulte Limites de solicitação<requestLimits>.

Solucionar problemas

Abaixo, são listados alguns problemas comuns encontrados ao trabalhar com o upload de arquivos e suas possíveis soluções.

Erro Não Encontrado quando implantado em um servidor IIS

O erro a seguir indica que o arquivo carregado excede o comprimento de conteúdo configurado do servidor:

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

Para obter mais informações, confira a seção IIS.

Falha de conexão

Um erro de conexão e uma redefinição da conexão com o servidor provavelmente indicam que o arquivo carregado excede o tamanho máximo do corpo da solicitação do Kestrel. Para obter mais informações, consulte a seção Kestrel tamanho máximo do corpo da solicitação . Kestrel os limites de conexão do cliente também podem exigir ajuste.

Exceção de referência nula com IFormFile

Se o controlador estiver aceitando arquivos carregados usando IFormFile, mas o valor for null, confirme se o formulário HTML está especificando um valor enctype de multipart/form-data. Se esse atributo não for definido no elemento <form>, o upload do arquivo não ocorrerá e todos os argumentos vinculados IFormFile serão null. Confirme também que a nomenclatura de upload nos dados do formulário corresponde à nomenclatura do aplicativo.

O fluxo era muito longo

Os exemplos neste tópico dependem MemoryStream para conter o conteúdo do arquivo carregado. O limite de tamanho de um MemoryStream é int.MaxValue. Se o cenário de upload de arquivo do aplicativo exigir a retenção de conteúdo de arquivo maior que 50 MB, use uma abordagem alternativa que não dependa de uma única MemoryStream para armazenar o conteúdo de um arquivo carregado.

O ASP.NET Core oferece suporte ao upload de um ou mais arquivos usando vinculação de modelo com buffer para arquivos menores e streaming sem buffer para arquivos maiores.

Exibir ou baixar código de exemplo (como baixar)

Considerações sobre segurança

Tenha cuidado ao fornecer aos usuários a capacidade de fazer upload de arquivos para um servidor. Os ciberinvasores podem tentar:

  • Executar ataques de negação de serviço.
  • Carregar vírus ou malware.
  • Comprometer redes e servidores de outras maneiras.

As etapas de segurança que reduzem a probabilidade de um ataque bem-sucedido são:

  • Carregue arquivos em uma área de carregamento de arquivo dedicada, preferencialmente para uma unidade que não seja do sistema. Um local dedicado facilita a imposição de restrições de segurança em arquivos carregados. Desabilitar permissões de execução no local de upload do arquivo.†
  • Não mantenha os arquivos carregados na mesma árvore de diretórios do aplicativo.†
  • Use um nome de arquivo seguro determinado pelo aplicativo. Não use um nome de arquivo fornecido pelo usuário ou o nome de arquivo não confiável do arquivo carregado.† HTML codifica o nome do arquivo não confiável ao exibi-lo. Por exemplo, registrar o nome do arquivo ou exibi-lo na interface do usuário (Razor codifica automaticamente a saída html).
  • Permitir somente extensões de arquivo aprovadas para a especificação de design do aplicativo.†
  • Verifique se as verificações do lado do cliente são executadas no servidor.† as verificações do lado do cliente são fáceis de contornar.
  • Verifique o tamanho de um arquivo carregado. Defina um limite de tamanho máximo para evitar uploads grandes.†
  • Quando os arquivos não devem ser substituídos por um arquivo carregado com o mesmo nome, marcar o nome do arquivo no banco de dados ou no armazenamento físico antes de carregar o arquivo.
  • Execute um scanner de vírus/malware no conteúdo carregado antes que o arquivo seja armazenado.

†O aplicativo de exemplo demonstra uma abordagem que atende aos critérios.

Aviso

Carregar códigos mal-intencionados em um sistema é frequentemente a primeira etapa para executar o código que pode:

  • Obtenha completamente o controle de um sistema.
  • Sobrecarregar um sistema, resultando em seu travamento.
  • Comprometer dados do sistema ou de usuários.
  • Aplicar grafite em uma UI pública.

Para obter informações de como reduzir as vulnerabilidades ao aceitar arquivos de usuários, consulte os seguintes recursos:

Para obter mais informações sobre como implementar medidas de segurança, incluindo exemplos do aplicativo de exemplo, consulte a seção Validação.

Cenários de armazenamento

As opções comuns de armazenamento para arquivos incluem:

  • Banco de dados

    • Para carregamentos de arquivos pequenos, um banco de dados geralmente é mais rápido do que as opções de armazenamento físico (sistema de arquivos ou compartilhamento de rede).
    • Um banco de dados geralmente é mais conveniente do que as opções de armazenamento físico porque a recuperação de um registro de banco de dados para dados do usuário pode fornecer simultaneamente o conteúdo do arquivo (por exemplo, uma imagem de avatar).
    • Um banco de dados é potencialmente menos caro do que usar um serviço de armazenamento de dados.
  • Armazenamento físico (sistema de arquivos ou compartilhamento de rede)

    • Para carregamentos de arquivos grandes:
      • Os limites de banco de dados podem restringir o tamanho do upload.
      • O armazenamento físico geralmente é menos econômico do que o armazenamento em um banco de dados.
    • O armazenamento físico é potencialmente menos caro do que usar um serviço de armazenamento de dados.
    • O processo do aplicativo deve ter permissões de leitura e gravação no local de armazenamento. Nunca conceda permissão de execução.
  • Serviço de armazenamento de dados (por exemplo, Armazenamento de Blobs do Azure)

    • Os serviços geralmente oferecem escalabilidade e resiliência aprimoradas em soluções locais que geralmente estão sujeitas a pontos únicos de falha.
    • Os serviços são um custo potencialmente menor em grandes cenários de infraestrutura de armazenamento.

    Para obter mais informações, confira Início Rápido: usar o .NET para criar um blob no armazenamento de objetos.

Cenários de carregamento de arquivo

Duas abordagens gerais para carregar arquivos são buffer e streaming.

de resposta

O arquivo inteiro é lido em um IFormFile, que é uma representação C# do arquivo usado para processar ou salvar o arquivo.

Os recursos (disco, memória) usados pelos uploads de arquivos dependem do número e do tamanho dos uploads de arquivos simultâneos. Se um aplicativo tentar armazenar em buffer muitos uploads, o site será bloqueado quando ficar sem memória ou espaço em disco. Se o tamanho ou a frequência de uploads de arquivos estiver esgotando os recursos do aplicativo, use streaming.

Observação

Qualquer arquivo armazenado em buffer que exceda 64 KB é movido da memória para um arquivo temporário no disco.

O buffer de arquivos pequenos é abordado nas seções a seguir deste tópico:

Streaming

O arquivo é recebido de uma solicitação de várias partes e processado ou salvo diretamente pelo aplicativo. O streaming não melhora significativamente o desempenho. O streaming reduz as demandas por memória ou espaço em disco ao carregar arquivos.

O streaming de arquivos grandes é abordado na seção Carregar arquivos grandes com streaming.

Carregar arquivos pequenos com associação de modelo em buffer para armazenamento físico

Para fazer upload de arquivos pequenos, use um formulário com várias partes ou crie uma solicitação POST usando JavaScript.

O exemplo a seguir demonstra o uso de um Razor formulário Páginas para carregar um único arquivo (Pages/BufferedSingleFileUploadPhysical.cshtml no aplicativo de exemplo):

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

O exemplo a seguir é análogo ao exemplo anterior, exceto que:

  • A API do JavaScript (Fetch API) é usada para enviar os dados do formulário.
  • Não há validação.
<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>

Para executar o formulário POST em JavaScript para clientes que não são compatíveis com a Fetch API, use uma das seguintes abordagens:

  • Use um Polyfill fetch (por exemplo, polyfill window.fetch (github/fetch)).

  • Use XMLHttpRequest. Por exemplo:

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

Para oferecer suporte a uploads de arquivos, os formulários HTML devem especificar um tipo de codificação (enctype) de multipart/form-data.

Para que um elemento de entrada files dê suporte ao carregamento de vários arquivos, forneça o atributo multiple no elemento <input>:

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

Os arquivos individuais carregados no servidor podem ser acessados por meio do Model Binding usando o IFormFile. O aplicativo de exemplo demonstra vários uploads de arquivos em buffer para cenários de armazenamento físico e de banco de dados.

Aviso

Não use a propriedade FileName de IFormFile que não seja para exibição e registro em log. Ao exibir ou registrar em log, o HTML codifica o nome do arquivo. Um invasor cibernético pode fornecer um nome de arquivo mal-intencionado, incluindo caminhos completos ou caminhos relativos. Os aplicativos devem:

  • Remova o caminho do nome de arquivo fornecido pelo usuário.
  • Salve o nome de arquivo codificado em HTML e removido pelo caminho para interface do usuário ou registro em log.
  • Gere um novo nome de arquivo aleatório para armazenamento.

O código a seguir remove o caminho do nome do arquivo:

string untrustedFileName = Path.GetFileName(pathName);

Os exemplos fornecidos até agora não levam em conta as considerações de segurança. Informações adicionais são fornecidas pelas seções a seguir e pelo aplicativo de exemplo:

Ao carregar arquivos usando model binding e IFormFile, o método de ação pode aceitar:

Observação

A associação corresponde aos arquivos de formulário por nome. Por exemplo, o valor HTML name em <input type="file" name="formFile"> deve corresponder ao parâmetro/propriedade C# associado (FormFile). Para obter mais informações, consulte a seção Corresponder o valor do atributo de nome ao nome do parâmetro do método POST.

O exemplo a seguir:

  • Executa um loop em um ou mais arquivos carregados.
  • Usa Path.GetTempFileName para retornar um caminho completo para um arquivo, incluindo o nome do arquivo.
  • Salva os arquivos no sistema de arquivos local usando um nome de arquivo gerado pelo aplicativo.
  • Retorna o número total e o tamanho dos arquivos carregados.
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 });
}

Use Path.GetRandomFileName para gerar um nome de arquivo sem um caminho. No exemplo a seguir, o caminho é obtido da configuração:

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

O caminho passado para o FileStream deve incluir o nome do arquivo. Se o nome do arquivo não for fornecido, será lançado um UnauthorizedAccessException no tempo de execução.

Os arquivos carregados usando a técnica IFormFile são armazenados em buffer na memória ou no disco do servidor antes do processamento. Dentro do método de ação, o conteúdo de IFormFile pode ser acessado como Stream. Além do sistema de arquivos local, os arquivos podem ser salvos em um compartilhamento de rede ou em um serviço de armazenamento de arquivos, como o Armazenamento de Blobs do Azure.

Para obter outro exemplo que executa um loop em vários arquivos para upload e usa nomes de arquivo seguros, consulte Pages/BufferedMultipleFileUploadPhysical.cshtml.cs no aplicativo de exemplo.

Aviso

Path.GetTempFileName gera um erro IOException se mais de 65.535 arquivos forem criados sem a exclusão dos arquivos temporários anteriores. O limite de 65.535 arquivos é um limite por servidor. Para obter mais informações sobre esse limite no sistema operacional Windows, consulte os comentários nos seguintes tópicos:

Carregar arquivos pequenos com a associação de modelo em buffer para um banco de dados

Para armazenar dados de arquivos binários em um banco de dados usando o Entity Framework, defina uma propriedade Byte matriz na entidade:

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

Especifique uma propriedade de modelo de página para a classe que inclui um IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Observação

IFormFile pode ser usado diretamente como um parâmetro de método de ação ou como uma propriedade de modelo vinculada. O exemplo anterior usa uma propriedade de modelo associada.

O FileUpload é usado no Razor formulário Páginas:

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

Quando o formulário for POSTed no servidor, copie o IFormFile para um fluxo e salve-o como uma matriz de bytes no banco de dados. No exemplo a seguir, _dbContext armazena o contexto de banco de dados do aplicativo:

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

O exemplo anterior é semelhante a um cenário demonstrado no aplicativo de exemplo:

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

Aviso

Tenha cuidado ao armazenar dados binários em bancos de dados relacionais, pois isso pode afetar negativamente o desempenho.

Não se baseie nem confie na propriedade FileName de IFormFile sem validação. A propriedade FileName só deve ser usada para fins de exibição e somente após a codificação HTML.

Os exemplos fornecidos não levam em conta as considerações de segurança. Informações adicionais são fornecidas pelas seções a seguir e pelo aplicativo de exemplo:

Carregue arquivos grandes com streaming

O exemplo a seguir demonstra como usar JavaScript para transmitir um arquivo para uma ação do controlador. O token antifalsificação do arquivo é gerado usando um atributo de filtro personalizado e passado para os cabeçalhos HTTP do cliente em vez de no corpo da solicitação. Como o método de ação processa diretamente os dados carregados, a vinculação do modelo de formulário é desativada por outro filtro personalizado. Dentro da ação, o conteúdo do formulário é lido usando um MultipartReader, que lê cada MultipartSection individual, processando o arquivo ou armazenando o conteúdo conforme apropriado. Depois que as seções de várias partes são lidas, a ação executa sua própria associação de modelo.

A resposta inicial da página carrega o formulário e salva um token antifalsificação em um cookie (por meio do atributo GenerateAntiforgeryTokenCookieAttribute). O atributo usa o suporte anti-falsificação integrado do ASP.NET Core para definir um cookie com um token de solicitação:

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

O DisableFormValueModelBindingAttribute é usado para desabilitar a associação de modelo:

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

No aplicativo de exemplo, GenerateAntiforgeryTokenCookieAttribute e DisableFormValueModelBindingAttribute são aplicados como filtros para os modelos de aplicativo de página de /StreamedSingleFileUploadDb e /StreamedSingleFileUploadPhysical em Startup.ConfigureServices usando Razor convenções de Páginas:

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

Como a associação de modelo não lê o formulário, os parâmetros associados do formulário não são associados (consulta, rota e cabeçalho continuam funcionando). O método de ação funciona diretamente com a propriedade Request. Um MultipartReader é usado para ler cada seção. Os dados de chave/valor são armazenados em um KeyValueAccumulator. Depois que as seções de várias partes são lidas, o conteúdo da KeyValueAccumulator é usado para vincular os dados do formulário a um tipo de modelo.

O método completo StreamingController.UploadDatabase para streaming para um banco de dados com EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

O método completo StreamingController.UploadPhysical para streaming para um local físico:

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

No aplicativo de exemplo, as verificações de validação são tratadas por FileHelpers.ProcessStreamedFile.

Validação

A classe do aplicativo de amostra FileHelpers demonstra várias verificações para uploads de arquivos em buffer IFormFile e em fluxo contínuo. Para processar IFormFile uploads de arquivos em buffer no aplicativo de exemplo, consulte o ProcessFormFile método no arquivo Utilities/FileHelpers.cs. Para processar arquivos transmitidos, consulte o ProcessStreamedFile método no mesmo arquivo.

Aviso

Os métodos de processamento de validação demonstrados no aplicativo de exemplo não examinam o conteúdo dos arquivos carregados. Na maioria dos cenários de produção, uma API de scanner de vírus/malware é usada no arquivo antes de disponibilizar o arquivo para usuários ou outros sistemas.

Embora o exemplo de tópico forneça um exemplo funcional de técnicas de validação, não implemente a classe FileHelpers em um aplicativo de produção, a menos que você:

  • Entenda completamente a implementação.
  • Modifique a implementação conforme apropriado para o ambiente e as especificações do aplicativo.

Nunca implemente indiscriminadamente o código de segurança em um aplicativo sem atender a esses requisitos.

Validação de conteúdo

Use uma API de verificação de vírus/malware de terceiros no conteúdo carregado.

A verificação de arquivos exige recursos do servidor em cenários de alto volume. Se o desempenho do processamento de solicitações for reduzido devido à verificação de arquivos, considere descarregar o trabalho de verificação para um serviço em segundo plano, possivelmente um serviço em execução em um servidor diferente do servidor do aplicativo. Normalmente, os arquivos carregados são mantidos em uma área em quarentena até que o verificador de vírus em segundo plano os verifique. Quando um arquivo é passado, o arquivo é movido para o local normal de armazenamento de arquivos. Essas etapas geralmente são executadas em conjunto com um registro de banco de dados que indica o status de verificação de um arquivo. Usando essa abordagem, o aplicativo e o servidor de aplicativos permanecem focados em responder às solicitações.

Validação de extensão de arquivo

A extensão do arquivo carregado deve ser verificada em relação a uma lista de extensões permitidas. Por exemplo:

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
}

Validação de assinatura de arquivo

A assinatura de um arquivo é determinada pelos primeiros bytes no início de um arquivo. Esses bytes podem ser usados para indicar se a extensão corresponde ao conteúdo do arquivo. O aplicativo de exemplo verifica as assinaturas de arquivo para alguns tipos de arquivo comuns. No exemplo a seguir, a assinatura de arquivo para uma imagem JPEG é verificada em relação ao arquivo :

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

Para obter assinaturas de arquivo adicionais, use um banco de dados de assinaturas de arquivo (resultado da pesquisa do Google) e especificações de arquivo oficiais. Consultar especificações de arquivo oficiais pode garantir que as assinaturas selecionadas sejam válidas.

Segurança de nome de arquivo

Nunca use um nome de arquivo fornecido pelo cliente para salvar um arquivo no armazenamento físico. Crie um nome de arquivo seguro para o arquivo usando Path.GetRandomFileName ou Path.GetTempFileName para criar um caminho completo (incluindo o nome do arquivo) para armazenamento temporário.

Razor codifica automaticamente valores de propriedade para exibição. O código a seguir é seguro de usar:

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

Fora do Razor, sempre HtmlEncode o conteúdo do nome do arquivo da solicitação de um usuário.

Muitas implementações devem incluir um marcar que o arquivo existe; caso contrário, o arquivo será substituído por um arquivo de mesmo nome. Forneça lógica adicional para atender às especificações do aplicativo.

Validação de tamanho

Limite o tamanho dos arquivos carregados.

No aplicativo de exemplo, o tamanho do arquivo é limitado a 2 MB (indicado em bytes). O limite é fornecido por meio da Configuração do arquivo appsettings.json:

{
  "FileSizeLimit": 2097152
}

O FileSizeLimit é injetado em PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Quando um tamanho de arquivo excede o limite, o arquivo é rejeitado:

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

Corresponder o valor do atributo de nome ao nome do parâmetro do método POST

Em formuláriosRazor que postam dados ou usam javaScript FormData diretamente, o nome especificado no elemento do formulário ou FormData deve corresponder ao nome do parâmetro na ação do controlador.

No exemplo a seguir:

  • Ao usar um elemento <input>, o atributo name é definido como o valor battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Ao usar FormData em JavaScript, o nome é definido como o valor battlePlans:

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

Use um nome correspondente para o parâmetro do método C# (battlePlans):

  • Para um método manipulador de Páginas de Razor página chamado Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Para um método de ação do controlador POST do MVC:

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

Configuração de servidor e aplicativo

Limite de comprimento do corpo de várias partes

MultipartBodyLengthLimit define o limite para o comprimento de cada corpo de várias partes. Seções de formulário que excedem esse limite lançam um InvalidDataException quando analisado. O padrão é 134.217.728 (128 MB). Personalize o limite usando a configuração MultipartBodyLengthLimit em Startup.ConfigureServices:

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

RequestFormLimitsAttribute é usado para definir o MultipartBodyLengthLimit para uma única página ou ação.

Em um aplicativo Páginas Razor, aplique o filtro com uma convenção de em Startup.ConfigureServices:

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

Em um aplicativo Páginas Razor ou um aplicativo MVC, aplique o filtro ao modelo de página ou ao método de ação:

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

Kestrel tamanho máximo do corpo da solicitação

Para aplicativos hospedados por Kestrel, o tamanho máximo padrão do corpo da solicitação é de 30.000.000 bytes, o que equivale a aproximadamente 28,6 MB. Personalize o limite usando a opção de servidor MaxRequestBodySizeKestrel:

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

RequestSizeLimitAttribute é usado para definir MaxRequestBodySize para uma única página ou ação.

Em um aplicativo Páginas Razor, aplique o filtro com uma convenção de em Startup.ConfigureServices:

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

Em um aplicativo Páginas Razor ou em um aplicativo MVC, aplique o filtro à classe do manipulador de página ou ao método de ação:

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

O RequestSizeLimitAttribute também pode ser aplicado usando a @attributeRazor diretiva :

@attribute [RequestSizeLimitAttribute(52428800)]

Outros Kestrel limites

Outros Kestrel limites podem ser aplicados a aplicativos hospedados por Kestrel:

IIS

O limite de solicitação padrão (maxAllowedContentLength) é de 30.000.000 bytes, o que equivale a aproximadamente 28,6 MB. Personalize o limite no arquivo web.config. No exemplo a seguir, o limite é definido como 50 MB (52.428.800 bytes):

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

A configuração maxAllowedContentLength se aplica somente ao IIS. Para obter mais informações, consulte Limites de solicitação<requestLimits>.

Aumente o tamanho máximo do corpo da solicitação para a solicitação HTTP definindo IISServerOptions.MaxRequestBodySize em Startup.ConfigureServices. No exemplo a seguir, o limite é definido como 50 MB (52.428.800 bytes):

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

Para obter mais informações, consulte Hospedar o ASP.NET Core no Windows com o IIS.

Solucionar problemas

Abaixo, são listados alguns problemas comuns encontrados ao trabalhar com o upload de arquivos e suas possíveis soluções.

Erro Não Encontrado quando implantado em um servidor IIS

O erro a seguir indica que o arquivo carregado excede o comprimento de conteúdo configurado do servidor:

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

Para obter mais informações, confira a seção IIS.

Falha de conexão

Um erro de conexão e uma redefinição da conexão com o servidor provavelmente indicam que o arquivo carregado excede o tamanho máximo do corpo da solicitação do Kestrel. Para obter mais informações, consulte a seção Kestrel tamanho máximo do corpo da solicitação . Kestrel os limites de conexão do cliente também podem exigir ajuste.

Exceção de referência nula com IFormFile

Se o controlador estiver aceitando arquivos carregados usando IFormFile, mas o valor for null, confirme se o formulário HTML está especificando um valor enctype de multipart/form-data. Se esse atributo não for definido no elemento <form>, o upload do arquivo não ocorrerá e todos os argumentos vinculados IFormFile serão null. Confirme também que a nomenclatura de upload nos dados do formulário corresponde à nomenclatura do aplicativo.

O fluxo era muito longo

Os exemplos neste tópico dependem MemoryStream para conter o conteúdo do arquivo carregado. O limite de tamanho de um MemoryStream é int.MaxValue. Se o cenário de upload de arquivo do aplicativo exigir a retenção de conteúdo de arquivo maior que 50 MB, use uma abordagem alternativa que não dependa de uma única MemoryStream para armazenar o conteúdo de um arquivo carregado.

O ASP.NET Core oferece suporte ao upload de um ou mais arquivos usando vinculação de modelo com buffer para arquivos menores e streaming sem buffer para arquivos maiores.

Exibir ou baixar código de exemplo (como baixar)

Considerações sobre segurança

Tenha cuidado ao fornecer aos usuários a capacidade de fazer upload de arquivos para um servidor. Os ciberinvasores podem tentar:

  • Executar ataques de negação de serviço.
  • Carregar vírus ou malware.
  • Comprometer redes e servidores de outras maneiras.

As etapas de segurança que reduzem a probabilidade de um ataque bem-sucedido são:

  • Carregue arquivos em uma área de carregamento de arquivo dedicada, preferencialmente para uma unidade que não seja do sistema. Um local dedicado facilita a imposição de restrições de segurança em arquivos carregados. Desabilitar permissões de execução no local de upload do arquivo.†
  • Não mantenha os arquivos carregados na mesma árvore de diretórios do aplicativo.†
  • Use um nome de arquivo seguro determinado pelo aplicativo. Não use um nome de arquivo fornecido pelo usuário ou o nome de arquivo não confiável do arquivo carregado.† HTML codifica o nome do arquivo não confiável ao exibi-lo. Por exemplo, registrar o nome do arquivo ou exibi-lo na interface do usuário (Razor codifica automaticamente a saída html).
  • Permitir somente extensões de arquivo aprovadas para a especificação de design do aplicativo.†
  • Verifique se as verificações do lado do cliente são executadas no servidor.† as verificações do lado do cliente são fáceis de contornar.
  • Verifique o tamanho de um arquivo carregado. Defina um limite de tamanho máximo para evitar uploads grandes.†
  • Quando os arquivos não devem ser substituídos por um arquivo carregado com o mesmo nome, marcar o nome do arquivo no banco de dados ou no armazenamento físico antes de carregar o arquivo.
  • Execute um scanner de vírus/malware no conteúdo carregado antes que o arquivo seja armazenado.

†O aplicativo de exemplo demonstra uma abordagem que atende aos critérios.

Aviso

Carregar códigos mal-intencionados em um sistema é frequentemente a primeira etapa para executar o código que pode:

  • Obtenha completamente o controle de um sistema.
  • Sobrecarregar um sistema, resultando em seu travamento.
  • Comprometer dados do sistema ou de usuários.
  • Aplicar grafite em uma UI pública.

Para obter informações de como reduzir as vulnerabilidades ao aceitar arquivos de usuários, consulte os seguintes recursos:

Para obter mais informações sobre como implementar medidas de segurança, incluindo exemplos do aplicativo de exemplo, consulte a seção Validação.

Cenários de armazenamento

As opções comuns de armazenamento para arquivos incluem:

  • Banco de dados

    • Para carregamentos de arquivos pequenos, um banco de dados geralmente é mais rápido do que as opções de armazenamento físico (sistema de arquivos ou compartilhamento de rede).
    • Um banco de dados geralmente é mais conveniente do que as opções de armazenamento físico porque a recuperação de um registro de banco de dados para dados do usuário pode fornecer simultaneamente o conteúdo do arquivo (por exemplo, uma imagem de avatar).
    • Um banco de dados é potencialmente menos caro do que usar um serviço de armazenamento de dados.
  • Armazenamento físico (sistema de arquivos ou compartilhamento de rede)

    • Para carregamentos de arquivos grandes:
      • Os limites de banco de dados podem restringir o tamanho do upload.
      • O armazenamento físico geralmente é menos econômico do que o armazenamento em um banco de dados.
    • O armazenamento físico é potencialmente menos caro do que usar um serviço de armazenamento de dados.
    • O processo do aplicativo deve ter permissões de leitura e gravação no local de armazenamento. Nunca conceda permissão de execução.
  • Serviço de armazenamento de dados (por exemplo, Armazenamento de Blobs do Azure)

    • Os serviços geralmente oferecem escalabilidade e resiliência aprimoradas em soluções locais que geralmente estão sujeitas a pontos únicos de falha.
    • Os serviços são um custo potencialmente menor em grandes cenários de infraestrutura de armazenamento.

    Para obter mais informações, confira Início Rápido: usar o .NET para criar um blob no armazenamento de objetos. O tópico demonstra , mas UploadFromFileAsync pode ser usado para salvar um UploadFromStreamAsync no armazenamento de blobs ao trabalhar com um FileStream.Stream

Cenários de carregamento de arquivo

Duas abordagens gerais para carregar arquivos são buffer e streaming.

de resposta

O arquivo inteiro é lido em um IFormFile, que é uma representação C# do arquivo usado para processar ou salvar o arquivo.

Os recursos (disco, memória) usados pelos uploads de arquivos dependem do número e do tamanho dos uploads de arquivos simultâneos. Se um aplicativo tentar armazenar em buffer muitos uploads, o site será bloqueado quando ficar sem memória ou espaço em disco. Se o tamanho ou a frequência de uploads de arquivos estiver esgotando os recursos do aplicativo, use streaming.

Observação

Qualquer arquivo armazenado em buffer que exceda 64 KB é movido da memória para um arquivo temporário no disco.

O buffer de arquivos pequenos é abordado nas seções a seguir deste tópico:

Streaming

O arquivo é recebido de uma solicitação de várias partes e processado ou salvo diretamente pelo aplicativo. O streaming não melhora significativamente o desempenho. O streaming reduz as demandas por memória ou espaço em disco ao carregar arquivos.

O streaming de arquivos grandes é abordado na seção Carregar arquivos grandes com streaming.

Carregar arquivos pequenos com associação de modelo em buffer para armazenamento físico

Para fazer upload de arquivos pequenos, use um formulário com várias partes ou crie uma solicitação POST usando JavaScript.

O exemplo a seguir demonstra o uso de um Razor formulário Páginas para carregar um único arquivo (Pages/BufferedSingleFileUploadPhysical.cshtml no aplicativo de exemplo):

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

O exemplo a seguir é análogo ao exemplo anterior, exceto que:

  • A API do JavaScript (Fetch API) é usada para enviar os dados do formulário.
  • Não há validação.
<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>

Para executar o formulário POST em JavaScript para clientes que não são compatíveis com a Fetch API, use uma das seguintes abordagens:

  • Use um Polyfill fetch (por exemplo, polyfill window.fetch (github/fetch)).

  • Use XMLHttpRequest. Por exemplo:

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

Para oferecer suporte a uploads de arquivos, os formulários HTML devem especificar um tipo de codificação (enctype) de multipart/form-data.

Para que um elemento de entrada files dê suporte ao carregamento de vários arquivos, forneça o atributo multiple no elemento <input>:

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

Os arquivos individuais carregados no servidor podem ser acessados por meio do Model Binding usando o IFormFile. O aplicativo de exemplo demonstra vários uploads de arquivos em buffer para cenários de armazenamento físico e de banco de dados.

Aviso

Não use a propriedade FileName de IFormFile que não seja para exibição e registro em log. Ao exibir ou registrar em log, o HTML codifica o nome do arquivo. Um invasor cibernético pode fornecer um nome de arquivo mal-intencionado, incluindo caminhos completos ou caminhos relativos. Os aplicativos devem:

  • Remova o caminho do nome de arquivo fornecido pelo usuário.
  • Salve o nome de arquivo codificado em HTML e removido pelo caminho para interface do usuário ou registro em log.
  • Gere um novo nome de arquivo aleatório para armazenamento.

O código a seguir remove o caminho do nome do arquivo:

string untrustedFileName = Path.GetFileName(pathName);

Os exemplos fornecidos até agora não levam em conta as considerações de segurança. Informações adicionais são fornecidas pelas seções a seguir e pelo aplicativo de exemplo:

Ao carregar arquivos usando model binding e IFormFile, o método de ação pode aceitar:

Observação

A associação corresponde aos arquivos de formulário por nome. Por exemplo, o valor HTML name em <input type="file" name="formFile"> deve corresponder ao parâmetro/propriedade C# associado (FormFile). Para obter mais informações, consulte a seção Corresponder o valor do atributo de nome ao nome do parâmetro do método POST.

O exemplo a seguir:

  • Executa um loop em um ou mais arquivos carregados.
  • Usa Path.GetTempFileName para retornar um caminho completo para um arquivo, incluindo o nome do arquivo.
  • Salva os arquivos no sistema de arquivos local usando um nome de arquivo gerado pelo aplicativo.
  • Retorna o número total e o tamanho dos arquivos carregados.
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 });
}

Use Path.GetRandomFileName para gerar um nome de arquivo sem um caminho. No exemplo a seguir, o caminho é obtido da configuração:

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

O caminho passado para o FileStream deve incluir o nome do arquivo. Se o nome do arquivo não for fornecido, será lançado um UnauthorizedAccessException no tempo de execução.

Os arquivos carregados usando a técnica IFormFile são armazenados em buffer na memória ou no disco do servidor antes do processamento. Dentro do método de ação, o conteúdo de IFormFile pode ser acessado como Stream. Além do sistema de arquivos local, os arquivos podem ser salvos em um compartilhamento de rede ou em um serviço de armazenamento de arquivos, como o Armazenamento de Blobs do Azure.

Para obter outro exemplo que executa um loop em vários arquivos para upload e usa nomes de arquivo seguros, consulte Pages/BufferedMultipleFileUploadPhysical.cshtml.cs no aplicativo de exemplo.

Aviso

Path.GetTempFileName gera um erro IOException se mais de 65.535 arquivos forem criados sem a exclusão dos arquivos temporários anteriores. O limite de 65.535 arquivos é um limite por servidor. Para obter mais informações sobre esse limite no sistema operacional Windows, consulte os comentários nos seguintes tópicos:

Carregar arquivos pequenos com a associação de modelo em buffer para um banco de dados

Para armazenar dados de arquivos binários em um banco de dados usando o Entity Framework, defina uma propriedade Byte matriz na entidade:

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

Especifique uma propriedade de modelo de página para a classe que inclui um IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Observação

IFormFile pode ser usado diretamente como um parâmetro de método de ação ou como uma propriedade de modelo vinculada. O exemplo anterior usa uma propriedade de modelo associada.

O FileUpload é usado no Razor formulário Páginas:

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

Quando o formulário for POSTed no servidor, copie o IFormFile para um fluxo e salve-o como uma matriz de bytes no banco de dados. No exemplo a seguir, _dbContext armazena o contexto de banco de dados do aplicativo:

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

O exemplo anterior é semelhante a um cenário demonstrado no aplicativo de exemplo:

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

Aviso

Tenha cuidado ao armazenar dados binários em bancos de dados relacionais, pois isso pode afetar negativamente o desempenho.

Não se baseie nem confie na propriedade FileName de IFormFile sem validação. A propriedade FileName só deve ser usada para fins de exibição e somente após a codificação HTML.

Os exemplos fornecidos não levam em conta as considerações de segurança. Informações adicionais são fornecidas pelas seções a seguir e pelo aplicativo de exemplo:

Carregue arquivos grandes com streaming

O exemplo a seguir demonstra como usar JavaScript para transmitir um arquivo para uma ação do controlador. O token antifalsificação do arquivo é gerado usando um atributo de filtro personalizado e passado para os cabeçalhos HTTP do cliente em vez de no corpo da solicitação. Como o método de ação processa diretamente os dados carregados, a vinculação do modelo de formulário é desativada por outro filtro personalizado. Dentro da ação, o conteúdo do formulário é lido usando um MultipartReader, que lê cada MultipartSection individual, processando o arquivo ou armazenando o conteúdo conforme apropriado. Depois que as seções de várias partes são lidas, a ação executa sua própria associação de modelo.

A resposta inicial da página carrega o formulário e salva um token antifalsificação em um cookie (por meio do atributo GenerateAntiforgeryTokenCookieAttribute). O atributo usa o suporte anti-falsificação integrado do ASP.NET Core para definir um cookie com um token de solicitação:

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

O DisableFormValueModelBindingAttribute é usado para desabilitar a associação de modelo:

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

No aplicativo de exemplo, GenerateAntiforgeryTokenCookieAttribute e DisableFormValueModelBindingAttribute são aplicados como filtros para os modelos de aplicativo de página de /StreamedSingleFileUploadDb e /StreamedSingleFileUploadPhysical em Startup.ConfigureServices usando Razor convenções de Páginas:

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

Como a associação de modelo não lê o formulário, os parâmetros associados do formulário não são associados (consulta, rota e cabeçalho continuam funcionando). O método de ação funciona diretamente com a propriedade Request. Um MultipartReader é usado para ler cada seção. Os dados de chave/valor são armazenados em um KeyValueAccumulator. Depois que as seções de várias partes são lidas, o conteúdo da KeyValueAccumulator é usado para vincular os dados do formulário a um tipo de modelo.

O método completo StreamingController.UploadDatabase para streaming para um banco de dados com EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

O método completo StreamingController.UploadPhysical para streaming para um local físico:

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

No aplicativo de exemplo, as verificações de validação são tratadas por FileHelpers.ProcessStreamedFile.

Validação

A classe do aplicativo de amostra FileHelpers demonstra várias verificações para uploads de arquivos em buffer IFormFile e em fluxo contínuo. Para processar IFormFile uploads de arquivos em buffer no aplicativo de exemplo, consulte o ProcessFormFile método no arquivo Utilities/FileHelpers.cs. Para processar arquivos transmitidos, consulte o ProcessStreamedFile método no mesmo arquivo.

Aviso

Os métodos de processamento de validação demonstrados no aplicativo de exemplo não examinam o conteúdo dos arquivos carregados. Na maioria dos cenários de produção, uma API de scanner de vírus/malware é usada no arquivo antes de disponibilizar o arquivo para usuários ou outros sistemas.

Embora o exemplo de tópico forneça um exemplo funcional de técnicas de validação, não implemente a classe FileHelpers em um aplicativo de produção, a menos que você:

  • Entenda completamente a implementação.
  • Modifique a implementação conforme apropriado para o ambiente e as especificações do aplicativo.

Nunca implemente indiscriminadamente o código de segurança em um aplicativo sem atender a esses requisitos.

Validação de conteúdo

Use uma API de verificação de vírus/malware de terceiros no conteúdo carregado.

A verificação de arquivos exige recursos do servidor em cenários de alto volume. Se o desempenho do processamento de solicitações for reduzido devido à verificação de arquivos, considere descarregar o trabalho de verificação para um serviço em segundo plano, possivelmente um serviço em execução em um servidor diferente do servidor do aplicativo. Normalmente, os arquivos carregados são mantidos em uma área em quarentena até que o verificador de vírus em segundo plano os verifique. Quando um arquivo é passado, o arquivo é movido para o local normal de armazenamento de arquivos. Essas etapas geralmente são executadas em conjunto com um registro de banco de dados que indica o status de verificação de um arquivo. Usando essa abordagem, o aplicativo e o servidor de aplicativos permanecem focados em responder às solicitações.

Validação de extensão de arquivo

A extensão do arquivo carregado deve ser verificada em relação a uma lista de extensões permitidas. Por exemplo:

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
}

Validação de assinatura de arquivo

A assinatura de um arquivo é determinada pelos primeiros bytes no início de um arquivo. Esses bytes podem ser usados para indicar se a extensão corresponde ao conteúdo do arquivo. O aplicativo de exemplo verifica as assinaturas de arquivo para alguns tipos de arquivo comuns. No exemplo a seguir, a assinatura de arquivo para uma imagem JPEG é verificada em relação ao arquivo :

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

Para obter assinaturas de arquivo adicionais, use um banco de dados de assinaturas de arquivo (resultado da pesquisa do Google) e especificações de arquivo oficiais. Consultar especificações de arquivo oficiais pode garantir que as assinaturas selecionadas sejam válidas.

Segurança de nome de arquivo

Nunca use um nome de arquivo fornecido pelo cliente para salvar um arquivo no armazenamento físico. Crie um nome de arquivo seguro para o arquivo usando Path.GetRandomFileName ou Path.GetTempFileName para criar um caminho completo (incluindo o nome do arquivo) para armazenamento temporário.

Razor codifica automaticamente valores de propriedade para exibição. O código a seguir é seguro de usar:

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

Fora do Razor, sempre HtmlEncode o conteúdo do nome do arquivo da solicitação de um usuário.

Muitas implementações devem incluir um marcar que o arquivo existe; caso contrário, o arquivo será substituído por um arquivo de mesmo nome. Forneça lógica adicional para atender às especificações do aplicativo.

Validação de tamanho

Limite o tamanho dos arquivos carregados.

No aplicativo de exemplo, o tamanho do arquivo é limitado a 2 MB (indicado em bytes). O limite é fornecido por meio da Configuração do arquivo appsettings.json:

{
  "FileSizeLimit": 2097152
}

O FileSizeLimit é injetado em PageModel classes:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Quando um tamanho de arquivo excede o limite, o arquivo é rejeitado:

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

Corresponder o valor do atributo de nome ao nome do parâmetro do método POST

Em formuláriosRazor que postam dados ou usam javaScript FormData diretamente, o nome especificado no elemento do formulário ou FormData deve corresponder ao nome do parâmetro na ação do controlador.

No exemplo a seguir:

  • Ao usar um elemento <input>, o atributo name é definido como o valor battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Ao usar FormData em JavaScript, o nome é definido como o valor battlePlans:

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

Use um nome correspondente para o parâmetro do método C# (battlePlans):

  • Para um método manipulador de Páginas de Razor página chamado Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Para um método de ação do controlador POST do MVC:

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

Configuração de servidor e aplicativo

Limite de comprimento do corpo de várias partes

MultipartBodyLengthLimit define o limite para o comprimento de cada corpo de várias partes. Seções de formulário que excedem esse limite lançam um InvalidDataException quando analisado. O padrão é 134.217.728 (128 MB). Personalize o limite usando a configuração MultipartBodyLengthLimit em Startup.ConfigureServices:

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

RequestFormLimitsAttribute é usado para definir o MultipartBodyLengthLimit para uma única página ou ação.

Em um aplicativo Páginas Razor, aplique o filtro com uma convenção de em 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);

Em um aplicativo Páginas Razor ou um aplicativo MVC, aplique o filtro ao modelo de página ou ao método de ação:

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

Kestrel tamanho máximo do corpo da solicitação

Para aplicativos hospedados por Kestrel, o tamanho máximo padrão do corpo da solicitação é de 30.000.000 bytes, o que equivale a aproximadamente 28,6 MB. Personalize o limite usando a opção de servidor MaxRequestBodySizeKestrel:

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

RequestSizeLimitAttribute é usado para definir MaxRequestBodySize para uma única página ou ação.

Em um aplicativo Páginas Razor, aplique o filtro com uma convenção de em 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);

Em um aplicativo Páginas Razor ou em um aplicativo MVC, aplique o filtro à classe do manipulador de página ou ao método de ação:

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

Outros Kestrel limites

Outros Kestrel limites podem ser aplicados a aplicativos hospedados por Kestrel:

IIS

O limite de solicitação padrão (maxAllowedContentLength) é de 30.000.000 bytes, o que equivale a aproximadamente 28,6 MB. Personalize o limite no arquivo web.config. No exemplo a seguir, o limite é definido como 50 MB (52.428.800 bytes):

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

A configuração maxAllowedContentLength se aplica somente ao IIS. Para obter mais informações, consulte Limites de solicitação<requestLimits>.

Aumente o tamanho máximo do corpo da solicitação para a solicitação HTTP definindo IISServerOptions.MaxRequestBodySize em Startup.ConfigureServices. No exemplo a seguir, o limite é definido como 50 MB (52.428.800 bytes):

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

Para obter mais informações, consulte Hospedar o ASP.NET Core no Windows com o IIS.

Solucionar problemas

Abaixo, são listados alguns problemas comuns encontrados ao trabalhar com o upload de arquivos e suas possíveis soluções.

Erro Não Encontrado quando implantado em um servidor IIS

O erro a seguir indica que o arquivo carregado excede o comprimento de conteúdo configurado do servidor:

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

Para obter mais informações, confira a seção IIS.

Falha de conexão

Um erro de conexão e uma redefinição da conexão com o servidor provavelmente indicam que o arquivo carregado excede o tamanho máximo do corpo da solicitação do Kestrel. Para obter mais informações, consulte a seção Kestrel tamanho máximo do corpo da solicitação . Kestrel os limites de conexão do cliente também podem exigir ajuste.

Exceção de referência nula com IFormFile

Se o controlador estiver aceitando arquivos carregados usando IFormFile, mas o valor for null, confirme se o formulário HTML está especificando um valor enctype de multipart/form-data. Se esse atributo não for definido no elemento <form>, o upload do arquivo não ocorrerá e todos os argumentos vinculados IFormFile serão null. Confirme também que a nomenclatura de upload nos dados do formulário corresponde à nomenclatura do aplicativo.

O fluxo era muito longo

Os exemplos neste tópico dependem MemoryStream para conter o conteúdo do arquivo carregado. O limite de tamanho de um MemoryStream é int.MaxValue. Se o cenário de upload de arquivo do aplicativo exigir a retenção de conteúdo de arquivo maior que 50 MB, use uma abordagem alternativa que não dependa de uma única MemoryStream para armazenar o conteúdo de um arquivo carregado.

Recursos adicionais