Carga de archivos en ASP.NET Core

De Rutger Storm

ASP.NET Core admite la carga de uno o varios archivos mediante el enlace de modelos almacenado en búfer de archivos más pequeños y el streaming no almacenado en búfer de archivos de mayor tamaño.

Vea o descargue el código de ejemplo (cómo descargarlo)

Consideraciones sobre la seguridad

Tenga precaución al proporcionar a los usuarios la capacidad de cargar archivos en un servidor. Es posible que los atacantes intenten lo siguiente:

  • Ejecutar ataques por denegación de servicio.
  • Cargar virus o malware.
  • Poner en riesgo redes y servidores de otras maneras.

Estos son algunos de los pasos de seguridad con los que se reduce la probabilidad de sufrir ataques:

  • Cargue los archivos a un área de carga de archivos dedicada, preferiblemente una unidad que no sea de sistema. Una ubicación dedicada facilita la imposición de restricciones de seguridad en los archivos cargados. Deshabilite la ejecución de los permisos en la ubicación de carga de archivos.†
  • Los archivos cargados no se deben persistir en el mismo árbol de directorio que la aplicación.†
  • Use un nombre de archivo seguro determinado por la aplicación. No use un nombre de archivo proporcionado por el usuario ni el nombre de archivo que no sea de confianza del archivo cargado.† HTML codifica el nombre de archivo que no es de confianza al mostrarlo. Por ejemplo, al registrar el nombre de archivo o mostrarlo en la interfaz de usuario (Razor codifica de forma automática la salida HTML).
  • Permita solo las extensiones de archivo aprobadas para la especificación de diseño de la aplicación.†
  • Compruebe que las comprobaciones por parte del cliente se hacen en el servidor.† Las comprobaciones por parte del cliente son fáciles de eludir.
  • Compruebe el tamaño de un archivo cargado. Establezca un límite de tamaño máximo para evitar cargas grandes.†
  • Cuando un archivo cargado con el mismo nombre no deba sobrescribir los archivos, vuelva a comprobar el nombre de archivo en la base de datos o en el almacenamiento físico antes de cargarlo.
  • Ejecute un detector de virus o malware en el contenido cargado antes de que se almacene el archivo.

†La aplicación de ejemplo muestra un enfoque que cumple los criterios.

Advertencia

La carga de código malintencionado en un sistema suele ser el primer paso para ejecutar código que puede:

  • Obtener el control completo de un sistema.
  • Sobrecargar un sistema de manera que el sistema se bloquea.
  • Poner en peligro los datos del usuario o del sistema.
  • Aplicar grafitis a una interfaz de usuario pública.

Vea los siguientes recursos para más información sobre cómo reducir el área expuesta de ataques al aceptar archivos de los usuarios:

Para más información sobre cómo implementar medidas de seguridad, incluidos ejemplos de la aplicación de ejemplo, consulte la sección Validación.

Escenarios de almacenamiento

Las opciones de almacenamiento comunes para los archivos incluyen:

  • Base de datos

    • En el caso de cargas de archivos pequeñas, una base de datos suele ser más rápida que las opciones de almacenamiento físico (sistema de archivos o recurso compartido de red).
    • Una base de datos suele ser más conveniente que las opciones de almacenamiento físico, ya que la recuperación de un registro de base de datos para los datos de usuario puede proporcionar el contenido del archivo (por ejemplo, una imagen de avatar).
    • Una base de datos puede ser más económica que usar un servicio de almacenamiento de datos en la nube.
  • Almacenamiento físico (sistema de archivos o recurso compartido de red)

    • Para cargas de archivos de gran tamaño:
      • Los límites de base de datos pueden restringir el tamaño de la carga.
      • El almacenamiento físico suele ser menos económico que el almacenamiento en una base de datos.
    • El almacenamiento físico puede ser más económico que usar un servicio de almacenamiento de datos en la nube.
    • El proceso de la aplicación debe tener permisos de lectura y escritura en la ubicación de almacenamiento. Nunca conceda el permiso de ejecución.
  • Servicio de almacenamiento de datos en la nube (por ejemplo, Azure Blob Storage).

    • Los servicios suelen ofrecer una escalabilidad y resistencia mejoradas sobre las soluciones locales que normalmente están sujetas a únicos puntos de error.
    • Los servicios pueden tener un costo menor en escenarios de infraestructura de almacenamiento de gran tamaño.

    Para más información, consulte Inicio rápido: Uso de .NET para crear un blob en el almacenamiento de objetos.

Archivos pequeños y grandes

La definición de archivos pequeños y grandes depende de los recursos informáticos disponibles. Las aplicaciones deben hacer pruebas comparativas del enfoque de almacenamiento que se usa para asegurarse de que puede controlar los tamaños esperados. Pruebas comparativas de memoria, CPU, disco y rendimiento de la base de datos.

Aunque no se pueden proporcionar límites específicos en lo que es pequeño frente a grande para la implementación, estos son algunos de los valores predeterminados relacionados de AspNetCore para FormOptions:

  • De forma predeterminada, HttpRequest.Form no almacena en búfer todo el cuerpo de la solicitud (BufferBody), pero almacena en búfer los archivos de formulario de varias partes incluidos.
  • MultipartBodyLengthLimit es el tamaño máximo de los archivos de formulario almacenados en búfer, el valor predeterminado es de 128 MB.
  • MemoryBufferThreshold indica cuánto almacenar en búfer los archivos en memoria antes de pasar a un archivo de búfer en el disco, el valor predeterminado es de 64 KB. MemoryBufferThreshold actúa como límite entre archivos pequeños y grandes que se aumenta o reduce en función de los recursos y escenarios de las aplicaciones.

Para obtener más información sobre FormOptions, vea el código fuente.

Escenarios de carga de archivos

Dos enfoques generales para cargar archivos son el almacenamiento en búfer y el streaming.

de respuesta

Todo el archivo se lee en IFormFile. IFormFile es una representación de C# del archivo que se usa para procesar o guardar el archivo.

El disco y la memoria se usan en las cargas de archivos dependen de la cantidad y del tamaño de las cargas de archivos que se realizan simultáneamente. Si una aplicación intenta almacenar demasiadas cargas en el búfer, el sitio se bloquea cuando se queda sin memoria o sin espacio en disco. Si el tamaño o la frecuencia de las cargas de archivos está agotando los recursos de la aplicación, use el streaming.

Cualquier archivo almacenado en búfer único que supere los 64 KB se mueve de la memoria a un archivo temporal en el disco.

Los archivos temporales para solicitudes más grandes se escriben en la ubicación denominada en la variable de entorno ASPNETCORE_TEMP. Si ASPNETCORE_TEMP no está definido, los archivos se escriben en la carpeta temporal del usuario actual.

En las secciones siguientes de este tema se habla del almacenamiento en búfer de archivos pequeños:

Streaming

El archivo se recibe de una solicitud de varias partes y lo procesa o guarda directamente la aplicación. El streaming no mejora considerablemente el rendimiento. El streaming reduce las demandas de memoria o espacio en disco cuando se cargan archivos.

El streaming de archivos grandes se describe en la sección Carga de archivos de gran tamaño con streaming.

Carga de archivos pequeños con enlace de modelos almacenado en búfer al almacenamiento físico

Para cargar archivos pequeños, se puede usar un formulario de varias partes o construir una solicitud POST con JavaScript.

En el ejemplo siguiente se muestra el uso de un formulario de Razor Pages para cargar un archivo único (Pages/BufferedSingleFileUploadPhysical.cshtml en la aplicación de ejemplo):

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

El ejemplo siguiente es análogo al ejemplo anterior, salvo en que:

  • (Fetch API) de JavaScript se usa para enviar los datos del formulario.
  • No hay ninguna validación.
<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 realizar la solicitud POST en JavaScript para los clientes que no admiten Fetch API, use uno de estos enfoques:

  • Use un Fetch Polyfill (por ejemplo, window.fetch polyfill (github/fetch)).

  • Use XMLHttpRequest. Por ejemplo:

    <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 admitir las cargas de archivos, los formularios HTML deben especificar un tipo de codificación (enctype) de multipart/form-data.

Para que un elemento de entrada files admita al carga de varios archivos, proporcione el atributo multiple en el elemento <input>:

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

Es posible acceder a archivos individuales cargados en el servidor a través del enlace de modelos mediante IFormFile. La aplicación de ejemplo muestra varias cargas de archivos almacenados en búfer para escenarios de almacenamiento físico y base de datos.

Advertencia

No utilice la propiedad FileName de IFormFile para usos distintos a la presentación y el registro. Para fines de presentación y registro, codifique el nombre de archivo en HTML. Un atacante puede proporcionar un nombre de archivo malintencionado, incluidas rutas de acceso completas o relativas. Las aplicaciones deben:

  • Quitar la ruta de acceso del nombre de archivo proporcionado por el usuario.
  • Guardar el nombre de archivo codificado en HTML, sin la ruta de acceso para la interfaz de usuario o el registro.
  • Generar un nombre de archivo aleatorio nuevo para el almacenamiento.

En el código siguiente se quita la ruta de acceso del nombre del archivo:

string untrustedFileName = Path.GetFileName(pathName);

Los ejemplos proporcionados hasta ahora no tienen en cuenta las consideraciones de seguridad. Se proporciona información adicional en las secciones siguientes y en la aplicación de ejemplo:

Al cargar archivos mediante el enlace de modelos y IFormFile, el método de acción puede aceptar:

Nota

El enlace coincide con los archivos de formulario por nombre. Por ejemplo, el valor name HTML en <input type="file" name="formFile"> debe coincidir con la propiedad/el parámetro enlazado de C# (FormFile). Para más información, consulte la sección Coincidencia del valor de atributo de nombre con el nombre del parámetro del método POST.

En el ejemplo siguiente:

  • Recorre en bucle uno o más archivos cargados.
  • Usa Path.GetTempFileName para devolver una ruta de acceso completa de un archivo, incluido el nombre de archivo.
  • Guarda los archivos en el sistema de archivos local con un nombre de archivo generado por la aplicación.
  • Devuelve el número total y el tamaño de los archivos cargados.
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 generar un nombre de archivo sin una ruta de acceso. En el ejemplo siguiente, la ruta de acceso se obtiene de la configuración:

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

La ruta de acceso pasada a FileStreamdebe incluir el nombre de archivo. Si no se proporciona el nombre de archivo, se produce una UnauthorizedAccessException en tiempo de ejecución.

Los archivos que se cargan usando la técnica IFormFile se almacenan en búfer en memoria o en disco en el servidor web antes de procesarse. Dentro del método de acción, se puede tener acceso al contenido de IFormFile como Stream. Además del sistema de archivos local, los archivos se pueden guardar en un recurso compartido de red o en un servicio de almacenamiento de archivos, como Azure Blob Storage.

Para ver otro ejemplo que recorre en bucle varios archivos para cargar y usa nombres de archivo seguros, consulte Pages/BufferedMultipleFileUploadPhysical.cshtml.cs en la aplicación de ejemplo.

Advertencia

Path.GetTempFileName arroja una IOException si se crean más de 65 535 archivos sin eliminar los archivos temporales anteriores. El límite de 65 535 archivos es un límite por servidor. Para más información sobre este límite en el sistema operativo Windows, consulte las notas en los temas siguientes:

Carga de archivos pequeños con enlace de modelos almacenado en búfer a una base de datos

Para almacenar datos de archivo binario en una base de datos con Entity Framework, defina una propiedad de matriz Byte en la entidad:

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

Especifique una propiedad de modelo de página para la clase que incluya un IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Nota

IFormFile se puede usar directamente como un parámetro de método de acción o como una propiedad de modelo enlazado. En el ejemplo anterior se utiliza una propiedad de modelo enlazado.

FileUpload se usa en el formulario de Razor Pages:

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

Cuando el formulario se publique en el servidor, copie el IFormFile en un flujo y guárdelo como matriz de bytes en la base de datos. En el ejemplo siguiente, _dbContext almacena el contexto de base de datos de la aplicación:

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

El ejemplo anterior es similar a un escenario que se muestra en la aplicación de ejemplo:

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

Advertencia

Tenga cuidado al almacenar los datos binarios en bases de datos relacionales, ya que esto puede repercutir adversamente en el rendimiento.

No se base ni confíe en la propiedad FileName de IFormFile sin validarla. La propiedad FileName solo debe usarse para fines de presentación y solo después de la codificación HTML.

Los ejemplos proporcionados no tienen en cuenta las consideraciones de seguridad. Se proporciona información adicional en las secciones siguientes y en la aplicación de ejemplo:

Carga de archivos de gran tamaño con streaming

En el ejemplo 3.1 se muestra cómo usar JavaScript para transmitir un archivo a una acción de controlador. El token de antifalsificación del archivo se genera por medio de un atributo de filtro personalizado y se pasa a los encabezados HTTP del cliente, en lugar de en el cuerpo de la solicitud. Dado que el método de acción procesa los datos cargados directamente, el enlace de modelos del formulario se deshabilita por otro filtro personalizado. Dentro de la acción, el contenido del formulario se lee usando un MultipartReader (que lee cada MultipartSection individual), de forma que el archivo se procesa o el contenido se almacena, según corresponda. Una vez leídas las secciones de varias partes, la acción realiza su propio enlace de modelos.

La respuesta de la página inicial carga el formulario y guarda un token de antifalsificación en una cookie (a través del atributo GenerateAntiforgeryTokenCookieAttribute). El atributo usa la compatibilidad de antifalsificación integrada de ASP.NET Core para establecer una cookie con un token de solicitud:

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

El DisableFormValueModelBindingAttribute se usa para deshabilitar el enlace de modelos:

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

En la aplicación de ejemplo, GenerateAntiforgeryTokenCookieAttribute y DisableFormValueModelBindingAttribute se aplican como filtros a los modelos de aplicación de la página de /StreamedSingleFileUploadDb y /StreamedSingleFileUploadPhysical en Startup.ConfigureServices con las convenciones de Razor Pages:

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

Dado que el enlace de modelos no lee el formulario, los parámetros enlazados desde el formulario no se enlazan (la consulta, la ruta y el encabezado siguen funcionando). El método de acción funciona directamente con la propiedad Request. Se usa un elemento MultipartReader para leer cada sección. Los datos de clave-valor se almacenan en un KeyValueAccumulator. Una vez leídas las secciones de varias partes, el contenido del KeyValueAccumulator se usa para enlazar los datos del formulario a un tipo de modelo.

El método StreamingController.UploadDatabase completo para streaming a una base de datos con 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));
        }
    }
}

El método StreamingController.UploadPhysical completo para streaming a una ubicación física:

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

En la aplicación de ejemplo, las comprobaciones de validación las controla FileHelpers.ProcessStreamedFile.

Validación

La clase FileHelpers de la aplicación de ejemplo muestra diversas comprobaciones de cargas de archivos de streaming y IFormFile almacenado en búfer. Para procesar cargas de archivos almacenadas en búfer de IFormFile en la aplicación de ejemplo, consulte el método ProcessFormFile en el archivo Utilities/FileHelpers.cs. Para procesar archivos de streaming, consulte el método ProcessStreamedFile en el mismo archivo.

Advertencia

Los métodos de procesamiento de validación mostrados en la aplicación de ejemplo no examinan el contenido de los archivos cargados. En la mayoría de los escenarios de producción, se usa una API de analizador de virus/malware en el archivo antes de que el archivo esté disponible para los usuarios u otros sistemas.

Aunque el ejemplo de tema proporciona un ejemplo funcional de técnicas de validación, no implemente la clase FileHelpers en una aplicación de producción a menos que:

  • Comprenda totalmente la implementación.
  • Modifique la implementación según corresponda en función del entorno y las especificaciones de la aplicación.

No implemente nunca de manera indiscriminada el código de seguridad en una aplicación sin abordar estos requisitos.

Validación del contenido

Use una API de detección de virus/malware de terceros en el contenido cargado.

El análisis de archivos exige recursos del servidor en escenarios de gran volumen. Si disminuye el rendimiento de procesamiento de solicitudes debido al análisis de archivos, considere la posibilidad de descargar el trabajo de análisis a un servicio en segundo plano, posiblemente un servicio que se ejecute en otro servidor desde el servidor de la aplicación. Habitualmente, los archivos cargados se almacenan en un área en cuarentena hasta que el programa de detección de virus en segundo plano los revisa. Cuando se pasa un archivo, se mueve a la ubicación de almacenamiento de archivos habitual. Por lo general, estos pasos se realizan junto con un registro de base de datos que indica el estado de análisis de un archivo. Mediante el uso de este enfoque, la aplicación y el servidor de aplicaciones permanecen centrados en responder a las solicitudes.

Validación de la extensión del archivo

La extensión del archivo cargado debe comprobarse con una lista de extensiones permitidas. Por ejemplo:

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
}

Validación de firma del archivo

La firma de un archivo viene determinada por los primeros bytes al principio de un archivo. Estos bytes se pueden usar para indicar si la extensión coincide con el contenido del archivo. La aplicación de ejemplo comprueba las firmas de archivo de algunos tipos de archivo comunes. En el ejemplo siguiente, la firma de archivo de una imagen JPEG se comprueba con respecto al archivo:

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 obtener firmas de archivo adicionales, use la base de datos de firmas de archivo (resultado de búsqueda de Google) y las especificaciones de archivo oficiales. Las especificaciones del archivo oficial de consultoría pueden asegurarse de que las firmas seleccionadas sean válidas.

Seguridad de nombre de archivo

Nunca use un nombre de archivo proporcionado por el cliente para guardar un archivo en el almacenamiento físico. Cree un nombre de archivo seguro para el archivo con Path.GetRandomFileName o Path.GetTempFileName para crear una ruta de acceso completa (incluido el nombre de archivo) para el almacenamiento temporal.

Razor codifica automáticamente en HTML los valores de propiedad para mostrarlos. El código siguiente es seguro de usar:

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

Fuera de Razor, siempre use HtmlEncode al contenido del nombre de archivo de una solicitud de un usuario.

Muchas implementaciones deben incluir una comprobación de que el archivo existe; de lo contrario, el archivo se sobrescribe por un archivo con el mismo nombre. Proporcione lógica adicional para satisfacer las especificaciones de la aplicación.

Validación del tamaño

Limite el tamaño de los archivos cargados.

En la aplicación de ejemplo, el tamaño del archivo está limitado a 2 MB (se indica en bytes). El límite se proporciona a través de Configuración del archivo appsettings.json:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit se inserta en las clases PageModel:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Cuando el tamaño de un archivo supera el límite, se rechaza el archivo:

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

Coincidencia del valor de atributo de nombre con el nombre del parámetro del método POST

En los formularios que no son de Razorque realizan la operación POST en los datos de formulario o usan directamente FormData de JavaScript, el nombre especificado en el elemento del formulario o FormData debe coincidir con el nombre del parámetro en la acción del controlador.

En el ejemplo siguiente:

  • Cuando se usa un elemento <input>, el atributo name se establece en el valor battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Cuando se usa FormData en JavaScript, el nombre se establece en el valor battlePlans:

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

Use un nombre coincidente para el parámetro del método de C# (battlePlans):

  • Para un método de control de páginas de Razor Pages denominado Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Para un método de acción de controlador POST de MVC:

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

Configuración del servidor y de la aplicación

Límite de longitud del cuerpo de varias partes

MultipartBodyLengthLimit establece el límite de la longitud de cada cuerpo de varias partes. Las secciones del formulario que superan este límite inician una InvalidDataException cuando se analizan. El valor predeterminado es 134 217 728 (128 MB). Personalice el límite mediante el valor MultipartBodyLengthLimit en Startup.ConfigureServices:

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

RequestFormLimitsAttribute se utiliza para establecer el MultipartBodyLengthLimit para una sola página o acción.

En una aplicación de Razor Pages, aplique el filtro con una convención en Startup.ConfigureServices:

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

En una aplicación de Razor Pages o una aplicación MVC, aplique el filtro al método de acción o al modelo de página:

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

Kestrel tamaño máximo del cuerpo de la solicitud

En el caso de las aplicaciones hospedadas por Kestrel, el tamaño máximo predeterminado del cuerpo de solicitud es 30 000 000 bytes, que son aproximadamente 28,6 MB. Personalice el límite con la opción MaxRequestBodySize del servidor de Kestrel:

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

RequestSizeLimitAttribute se usa para establecer el valor de MaxRequestBodySize de una sola página o acción.

En una aplicación de Razor Pages, aplique el filtro con una convención en Startup.ConfigureServices:

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

En una aplicación de Razor Pages o una aplicación MVC, aplique el filtro al método de acción o a la clase de control de página:

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

El RequestSizeLimitAttribute también se puede aplicar con la directiva @attributeRazor:

@attribute [RequestSizeLimitAttribute(52428800)]

Otros límites de Kestrel

Otros límites de Kestrel pueden aplicarse a las aplicaciones hospedadas por Kestrel:

IIS

El límite predeterminado de solicitudes (maxAllowedContentLength) es 30 000 000 bytes, que son aproximadamente 28,6 MB. Personalice el límite en el archivo web.config. En el ejemplo siguiente, el límite se establece en 50 MB (52 428 800 bytes):

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

La configuración de maxAllowedContentLength solo ocurre en IIS. Para más información, vea Características de la solicitud de <requestLimits>.

Solucionar problemas

Aquí incluimos algunos problemas comunes que pueden surgir al cargar archivos, así como sus posibles soluciones.

Error No encontrado al implementar en un servidor IIS

El error siguiente indica que el archivo cargado supera la longitud configurada del contenido del servidor:

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

Para más información, consulte la sección sobre IIS.

Error de conexión

Un error de conexión y una conexión del servidor de restablecimiento probablemente indica que el archivo cargado supera el tamaño máximo del cuerpo de la solicitud de Kestrel. Para más información, consulte la sección Tamaño máximo del cuerpo de la solicitud de Kestrel. Los límites de conexión del cliente de Kestrel también pueden requerir ajustes.

Excepción de referencia nula con IFormFile

Si el controlador acepta archivos cargados con IFormFile, pero el valor es null, confirme que el formulario HTML especifica un valor enctype de multipart/form-data. Si este atributo no está establecido en el elemento <form>, no se llevará a cabo la carga del archivo y cualquier argumento IFormFile enlazado será null. Confirme también que la nomenclatura de la carga en los datos de formulario coincide con la nomenclatura de la aplicación.

El flujo era demasiado largo

Los ejemplos de este tema se basan en MemoryStream para almacenar el contenido del archivo cargado. El límite de tamaño de MemoryStream es int.MaxValue. Si el escenario de carga de archivos de la aplicación requiere almacenar contenido de archivo superior a 50 MB, use un enfoque alternativo que no dependa de un único valor de MemoryStream para almacenar el contenido de un archivo cargado.

ASP.NET Core admite la carga de uno o varios archivos mediante el enlace de modelos almacenado en búfer de archivos más pequeños y el streaming no almacenado en búfer de archivos de mayor tamaño.

Vea o descargue el código de ejemplo (cómo descargarlo)

Consideraciones sobre la seguridad

Tenga precaución al proporcionar a los usuarios la capacidad de cargar archivos en un servidor. Es posible que los atacantes intenten lo siguiente:

  • Ejecutar ataques por denegación de servicio.
  • Cargar virus o malware.
  • Poner en riesgo redes y servidores de otras maneras.

Estos son algunos de los pasos de seguridad con los que se reduce la probabilidad de sufrir ataques:

  • Cargue los archivos a un área de carga de archivos dedicada, preferiblemente una unidad que no sea de sistema. Una ubicación dedicada facilita la imposición de restricciones de seguridad en los archivos cargados. Deshabilite la ejecución de los permisos en la ubicación de carga de archivos.†
  • Los archivos cargados no se deben persistir en el mismo árbol de directorio que la aplicación.†
  • Use un nombre de archivo seguro determinado por la aplicación. No use un nombre de archivo proporcionado por el usuario ni el nombre de archivo que no sea de confianza del archivo cargado.† HTML codifica el nombre de archivo que no es de confianza al mostrarlo. Por ejemplo, al registrar el nombre de archivo o mostrarlo en la interfaz de usuario (Razor codifica de forma automática la salida HTML).
  • Permita solo las extensiones de archivo aprobadas para la especificación de diseño de la aplicación.†
  • Compruebe que las comprobaciones por parte del cliente se hacen en el servidor.† Las comprobaciones por parte del cliente son fáciles de eludir.
  • Compruebe el tamaño de un archivo cargado. Establezca un límite de tamaño máximo para evitar cargas grandes.†
  • Cuando un archivo cargado con el mismo nombre no deba sobrescribir los archivos, vuelva a comprobar el nombre de archivo en la base de datos o en el almacenamiento físico antes de cargarlo.
  • Ejecute un detector de virus o malware en el contenido cargado antes de que se almacene el archivo.

†La aplicación de ejemplo muestra un enfoque que cumple los criterios.

Advertencia

La carga de código malintencionado en un sistema suele ser el primer paso para ejecutar código que puede:

  • Obtener el control completo de un sistema.
  • Sobrecargar un sistema de manera que el sistema se bloquea.
  • Poner en peligro los datos del usuario o del sistema.
  • Aplicar grafitis a una interfaz de usuario pública.

Vea los siguientes recursos para más información sobre cómo reducir el área expuesta de ataques al aceptar archivos de los usuarios:

Para más información sobre cómo implementar medidas de seguridad, incluidos ejemplos de la aplicación de ejemplo, consulte la sección Validación.

Escenarios de almacenamiento

Las opciones de almacenamiento comunes para los archivos incluyen:

  • Base de datos

    • En el caso de cargas de archivos pequeñas, una base de datos suele ser más rápida que las opciones de almacenamiento físico (sistema de archivos o recurso compartido de red).
    • Una base de datos suele ser más conveniente que las opciones de almacenamiento físico, ya que la recuperación de un registro de base de datos para los datos de usuario puede proporcionar el contenido del archivo (por ejemplo, una imagen de avatar).
    • Una base de datos puede ser más económica que usar un servicio de almacenamiento de datos.
  • Almacenamiento físico (sistema de archivos o recurso compartido de red)

    • Para cargas de archivos de gran tamaño:
      • Los límites de base de datos pueden restringir el tamaño de la carga.
      • El almacenamiento físico suele ser menos económico que el almacenamiento en una base de datos.
    • El almacenamiento físico puede ser más económico que usar un servicio de almacenamiento de datos.
    • El proceso de la aplicación debe tener permisos de lectura y escritura en la ubicación de almacenamiento. Nunca conceda el permiso de ejecución.
  • Servicio de almacenamiento de datos (por ejemplo, Azure Blob Storage)

    • Los servicios suelen ofrecer una escalabilidad y resistencia mejoradas sobre las soluciones locales que normalmente están sujetas a únicos puntos de error.
    • Los servicios pueden tener un costo menor en escenarios de infraestructura de almacenamiento de gran tamaño.

    Para más información, consulte Inicio rápido: Uso de .NET para crear un blob en el almacenamiento de objetos.

Escenarios de carga de archivos

Dos enfoques generales para cargar archivos son el almacenamiento en búfer y el streaming.

de respuesta

El archivo completo se lee en un IFormFile, que es una representación de C# del archivo que se usa para procesar o guardar el archivo.

Los recursos (disco, memoria) que se usan en las cargas de archivos dependen de la cantidad y del tamaño de las cargas de archivos que se realizan simultáneamente. Si una aplicación intenta almacenar demasiadas cargas en el búfer, el sitio se bloquea cuando se queda sin memoria o sin espacio en disco. Si el tamaño o la frecuencia de las cargas de archivos está agotando los recursos de la aplicación, use el streaming.

Nota

Cualquier archivo almacenado en búfer único que supere los 64 KB se mueve de la memoria a un archivo temporal en el disco.

En las secciones siguientes de este tema se habla del almacenamiento en búfer de archivos pequeños:

Streaming

El archivo se recibe de una solicitud de varias partes y lo procesa o guarda directamente la aplicación. El streaming no mejora considerablemente el rendimiento. El streaming reduce las demandas de memoria o espacio en disco cuando se cargan archivos.

El streaming de archivos grandes se describe en la sección Carga de archivos de gran tamaño con streaming.

Carga de archivos pequeños con enlace de modelos almacenado en búfer al almacenamiento físico

Para cargar archivos pequeños, se puede usar un formulario de varias partes o construir una solicitud POST con JavaScript.

En el ejemplo siguiente se muestra el uso de un formulario de Razor Pages para cargar un archivo único (Pages/BufferedSingleFileUploadPhysical.cshtml en la aplicación de ejemplo):

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

El ejemplo siguiente es análogo al ejemplo anterior, salvo en que:

  • (Fetch API) de JavaScript se usa para enviar los datos del formulario.
  • No hay ninguna validación.
<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 realizar la solicitud POST en JavaScript para los clientes que no admiten Fetch API, use uno de estos enfoques:

  • Use un Fetch Polyfill (por ejemplo, window.fetch polyfill (github/fetch)).

  • Use XMLHttpRequest. Por ejemplo:

    <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 admitir las cargas de archivos, los formularios HTML deben especificar un tipo de codificación (enctype) de multipart/form-data.

Para que un elemento de entrada files admita al carga de varios archivos, proporcione el atributo multiple en el elemento <input>:

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

Es posible acceder a archivos individuales cargados en el servidor a través del enlace de modelos mediante IFormFile. La aplicación de ejemplo muestra varias cargas de archivos almacenados en búfer para escenarios de almacenamiento físico y base de datos.

Advertencia

No utilice la propiedad FileName de IFormFile para usos distintos a la presentación y el registro. Para fines de presentación y registro, codifique el nombre de archivo en HTML. Un atacante puede proporcionar un nombre de archivo malintencionado, incluidas rutas de acceso completas o relativas. Las aplicaciones deben:

  • Quitar la ruta de acceso del nombre de archivo proporcionado por el usuario.
  • Guardar el nombre de archivo codificado en HTML, sin la ruta de acceso para la interfaz de usuario o el registro.
  • Generar un nombre de archivo aleatorio nuevo para el almacenamiento.

En el código siguiente se quita la ruta de acceso del nombre del archivo:

string untrustedFileName = Path.GetFileName(pathName);

Los ejemplos proporcionados hasta ahora no tienen en cuenta las consideraciones de seguridad. Se proporciona información adicional en las secciones siguientes y en la aplicación de ejemplo:

Al cargar archivos mediante el enlace de modelos y IFormFile, el método de acción puede aceptar:

Nota

El enlace coincide con los archivos de formulario por nombre. Por ejemplo, el valor name HTML en <input type="file" name="formFile"> debe coincidir con la propiedad/el parámetro enlazado de C# (FormFile). Para más información, consulte la sección Coincidencia del valor de atributo de nombre con el nombre del parámetro del método POST.

En el ejemplo siguiente:

  • Recorre en bucle uno o más archivos cargados.
  • Usa Path.GetTempFileName para devolver una ruta de acceso completa de un archivo, incluido el nombre de archivo.
  • Guarda los archivos en el sistema de archivos local con un nombre de archivo generado por la aplicación.
  • Devuelve el número total y el tamaño de los archivos cargados.
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 generar un nombre de archivo sin una ruta de acceso. En el ejemplo siguiente, la ruta de acceso se obtiene de la configuración:

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

La ruta de acceso pasada a FileStreamdebe incluir el nombre de archivo. Si no se proporciona el nombre de archivo, se produce una UnauthorizedAccessException en tiempo de ejecución.

Los archivos que se cargan usando la técnica IFormFile se almacenan en búfer en memoria o en disco en el servidor web antes de procesarse. Dentro del método de acción, se puede tener acceso al contenido de IFormFile como Stream. Además del sistema de archivos local, los archivos se pueden guardar en un recurso compartido de red o en un servicio de almacenamiento de archivos, como Azure Blob Storage.

Para ver otro ejemplo que recorre en bucle varios archivos para cargar y usa nombres de archivo seguros, consulte Pages/BufferedMultipleFileUploadPhysical.cshtml.cs en la aplicación de ejemplo.

Advertencia

Path.GetTempFileName arroja una IOException si se crean más de 65 535 archivos sin eliminar los archivos temporales anteriores. El límite de 65 535 archivos es un límite por servidor. Para más información sobre este límite en el sistema operativo Windows, consulte las notas en los temas siguientes:

Carga de archivos pequeños con enlace de modelos almacenado en búfer a una base de datos

Para almacenar datos de archivo binario en una base de datos con Entity Framework, defina una propiedad de matriz Byte en la entidad:

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

Especifique una propiedad de modelo de página para la clase que incluya un IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Nota

IFormFile se puede usar directamente como un parámetro de método de acción o como una propiedad de modelo enlazado. En el ejemplo anterior se utiliza una propiedad de modelo enlazado.

FileUpload se usa en el formulario de Razor Pages:

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

Cuando el formulario se publique en el servidor, copie el IFormFile en un flujo y guárdelo como matriz de bytes en la base de datos. En el ejemplo siguiente, _dbContext almacena el contexto de base de datos de la aplicación:

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

El ejemplo anterior es similar a un escenario que se muestra en la aplicación de ejemplo:

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

Advertencia

Tenga cuidado al almacenar los datos binarios en bases de datos relacionales, ya que esto puede repercutir adversamente en el rendimiento.

No se base ni confíe en la propiedad FileName de IFormFile sin validarla. La propiedad FileName solo debe usarse para fines de presentación y solo después de la codificación HTML.

Los ejemplos proporcionados no tienen en cuenta las consideraciones de seguridad. Se proporciona información adicional en las secciones siguientes y en la aplicación de ejemplo:

Carga de archivos de gran tamaño con streaming

En el ejemplo siguiente se muestra cómo usar JavaScript para transmitir un archivo a una acción de controlador. El token de antifalsificación del archivo se genera por medio de un atributo de filtro personalizado y se pasa a los encabezados HTTP del cliente, en lugar de en el cuerpo de la solicitud. Dado que el método de acción procesa los datos cargados directamente, el enlace de modelos del formulario se deshabilita por otro filtro personalizado. Dentro de la acción, el contenido del formulario se lee usando un MultipartReader (que lee cada MultipartSection individual), de forma que el archivo se procesa o el contenido se almacena, según corresponda. Una vez leídas las secciones de varias partes, la acción realiza su propio enlace de modelos.

La respuesta de la página inicial carga el formulario y guarda un token de antifalsificación en una cookie (a través del atributo GenerateAntiforgeryTokenCookieAttribute). El atributo usa la compatibilidad de antifalsificación integrada de ASP.NET Core para establecer una cookie con un token de solicitud:

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

El DisableFormValueModelBindingAttribute se usa para deshabilitar el enlace de modelos:

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

En la aplicación de ejemplo, GenerateAntiforgeryTokenCookieAttribute y DisableFormValueModelBindingAttribute se aplican como filtros a los modelos de aplicación de la página de /StreamedSingleFileUploadDb y /StreamedSingleFileUploadPhysical en Startup.ConfigureServices con las convenciones de Razor Pages:

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

Dado que el enlace de modelos no lee el formulario, los parámetros enlazados desde el formulario no se enlazan (la consulta, la ruta y el encabezado siguen funcionando). El método de acción funciona directamente con la propiedad Request. Se usa un elemento MultipartReader para leer cada sección. Los datos de clave-valor se almacenan en un KeyValueAccumulator. Una vez leídas las secciones de varias partes, el contenido del KeyValueAccumulator se usa para enlazar los datos del formulario a un tipo de modelo.

El método StreamingController.UploadDatabase completo para streaming a una base de datos con 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));
        }
    }
}

El método StreamingController.UploadPhysical completo para streaming a una ubicación física:

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

En la aplicación de ejemplo, las comprobaciones de validación las controla FileHelpers.ProcessStreamedFile.

Validación

La clase FileHelpers de la aplicación de ejemplo muestra varias comprobaciones de cargas de archivos de streaming y IFormFile almacenado en búfer. Para procesar cargas de archivos almacenadas en búfer de IFormFile en la aplicación de ejemplo, consulte el método ProcessFormFile en el archivo Utilities/FileHelpers.cs. Para procesar archivos de streaming, consulte el método ProcessStreamedFile en el mismo archivo.

Advertencia

Los métodos de procesamiento de validación mostrados en la aplicación de ejemplo no examinan el contenido de los archivos cargados. En la mayoría de los escenarios de producción, se usa una API de analizador de virus/malware en el archivo antes de que el archivo esté disponible para los usuarios u otros sistemas.

Aunque el ejemplo de tema proporciona un ejemplo funcional de técnicas de validación, no implemente la clase FileHelpers en una aplicación de producción a menos que:

  • Comprenda totalmente la implementación.
  • Modifique la implementación según corresponda en función del entorno y las especificaciones de la aplicación.

No implemente nunca de manera indiscriminada el código de seguridad en una aplicación sin abordar estos requisitos.

Validación del contenido

Use una API de detección de virus/malware de terceros en el contenido cargado.

El análisis de archivos exige recursos del servidor en escenarios de gran volumen. Si disminuye el rendimiento de procesamiento de solicitudes debido al análisis de archivos, considere la posibilidad de descargar el trabajo de análisis a un servicio en segundo plano, posiblemente un servicio que se ejecute en otro servidor desde el servidor de la aplicación. Habitualmente, los archivos cargados se almacenan en un área en cuarentena hasta que el programa de detección de virus en segundo plano los revisa. Cuando se pasa un archivo, se mueve a la ubicación de almacenamiento de archivos habitual. Por lo general, estos pasos se realizan junto con un registro de base de datos que indica el estado de análisis de un archivo. Mediante el uso de este enfoque, la aplicación y el servidor de aplicaciones permanecen centrados en responder a las solicitudes.

Validación de la extensión del archivo

La extensión del archivo cargado debe comprobarse con una lista de extensiones permitidas. Por ejemplo:

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
}

Validación de firma del archivo

La firma de un archivo viene determinada por los primeros bytes al principio de un archivo. Estos bytes se pueden usar para indicar si la extensión coincide con el contenido del archivo. La aplicación de ejemplo comprueba las firmas de archivo de algunos tipos de archivo comunes. En el ejemplo siguiente, la firma de archivo de una imagen JPEG se comprueba con respecto al archivo:

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 obtener firmas de archivo adicionales, use la base de datos de firmas de archivo (resultado de búsqueda de Google) y las especificaciones de archivo oficiales. Las especificaciones del archivo oficial de consultoría pueden asegurarse de que las firmas seleccionadas sean válidas.

Seguridad de nombre de archivo

Nunca use un nombre de archivo proporcionado por el cliente para guardar un archivo en el almacenamiento físico. Cree un nombre de archivo seguro para el archivo con Path.GetRandomFileName o Path.GetTempFileName para crear una ruta de acceso completa (incluido el nombre de archivo) para el almacenamiento temporal.

Razor codifica automáticamente en HTML los valores de propiedad para mostrarlos. El código siguiente es seguro de usar:

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

Fuera de Razor, siempre use HtmlEncode al contenido del nombre de archivo de una solicitud de un usuario.

Muchas implementaciones deben incluir una comprobación de que el archivo existe; de lo contrario, el archivo se sobrescribe por un archivo con el mismo nombre. Proporcione lógica adicional para satisfacer las especificaciones de la aplicación.

Validación del tamaño

Limite el tamaño de los archivos cargados.

En la aplicación de ejemplo, el tamaño del archivo está limitado a 2 MB (se indica en bytes). El límite se proporciona a través de Configuración del archivo appsettings.json:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit se inserta en las clases PageModel:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Cuando el tamaño de un archivo supera el límite, se rechaza el archivo:

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

Coincidencia del valor de atributo de nombre con el nombre del parámetro del método POST

En los formularios que no son de Razorque realizan la operación POST en los datos de formulario o usan directamente FormData de JavaScript, el nombre especificado en el elemento del formulario o FormData debe coincidir con el nombre del parámetro en la acción del controlador.

En el ejemplo siguiente:

  • Cuando se usa un elemento <input>, el atributo name se establece en el valor battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Cuando se usa FormData en JavaScript, el nombre se establece en el valor battlePlans:

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

Use un nombre coincidente para el parámetro del método de C# (battlePlans):

  • Para un método de control de páginas de Razor Pages denominado Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Para un método de acción de controlador POST de MVC:

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

Configuración del servidor y de la aplicación

Límite de longitud del cuerpo de varias partes

MultipartBodyLengthLimit establece el límite de la longitud de cada cuerpo de varias partes. Las secciones del formulario que superan este límite inician una InvalidDataException cuando se analizan. El valor predeterminado es 134 217 728 (128 MB). Personalice el límite mediante el valor MultipartBodyLengthLimit en Startup.ConfigureServices:

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

RequestFormLimitsAttribute se utiliza para establecer el MultipartBodyLengthLimit para una sola página o acción.

En una aplicación de Razor Pages, aplique el filtro con una convención en Startup.ConfigureServices:

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

En una aplicación de Razor Pages o una aplicación MVC, aplique el filtro al método de acción o al modelo de página:

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

Kestrel tamaño máximo del cuerpo de la solicitud

En el caso de las aplicaciones hospedadas por Kestrel, el tamaño máximo predeterminado del cuerpo de solicitud es 30 000 000 bytes, que son aproximadamente 28,6 MB. Personalice el límite con la opción MaxRequestBodySize del servidor de Kestrel:

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

RequestSizeLimitAttribute se usa para establecer el valor de MaxRequestBodySize de una sola página o acción.

En una aplicación de Razor Pages, aplique el filtro con una convención en Startup.ConfigureServices:

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

En una aplicación de Razor Pages o una aplicación MVC, aplique el filtro al método de acción o a la clase de control de página:

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

El RequestSizeLimitAttribute también se puede aplicar con la directiva @attributeRazor:

@attribute [RequestSizeLimitAttribute(52428800)]

Otros límites de Kestrel

Otros límites de Kestrel pueden aplicarse a las aplicaciones hospedadas por Kestrel:

IIS

El límite predeterminado de solicitudes (maxAllowedContentLength) es 30 000 000 bytes, que son aproximadamente 28,6 MB. Personalice el límite en el archivo web.config. En el ejemplo siguiente, el límite se establece en 50 MB (52 428 800 bytes):

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

La configuración de maxAllowedContentLength solo ocurre en IIS. Para más información, vea Características de la solicitud de <requestLimits>.

Aumente el tamaño máximo del cuerpo de la solicitud HTTP estableciendo IISServerOptions.MaxRequestBodySize en Startup.ConfigureServices. En el ejemplo siguiente, el límite se establece en 50 MB (52 428 800 bytes):

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

Para más información, vea Host ASP.NET Core on Windows with IIS (Hospedar ASP.NET Core en Windows con IIS).

Solucionar problemas

Aquí incluimos algunos problemas comunes que pueden surgir al cargar archivos, así como sus posibles soluciones.

Error No encontrado al implementar en un servidor IIS

El error siguiente indica que el archivo cargado supera la longitud configurada del contenido del servidor:

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

Para más información, consulte la sección sobre IIS.

Error de conexión

Un error de conexión y una conexión del servidor de restablecimiento probablemente indica que el archivo cargado supera el tamaño máximo del cuerpo de la solicitud de Kestrel. Para más información, consulte la sección Tamaño máximo del cuerpo de la solicitud de Kestrel. Los límites de conexión del cliente de Kestrel también pueden requerir ajustes.

Excepción de referencia nula con IFormFile

Si el controlador acepta archivos cargados con IFormFile, pero el valor es null, confirme que el formulario HTML especifica un valor enctype de multipart/form-data. Si este atributo no está establecido en el elemento <form>, no se llevará a cabo la carga del archivo y cualquier argumento IFormFile enlazado será null. Confirme también que la nomenclatura de la carga en los datos de formulario coincide con la nomenclatura de la aplicación.

El flujo era demasiado largo

Los ejemplos de este tema se basan en MemoryStream para almacenar el contenido del archivo cargado. El límite de tamaño de MemoryStream es int.MaxValue. Si el escenario de carga de archivos de la aplicación requiere almacenar contenido de archivo superior a 50 MB, use un enfoque alternativo que no dependa de un único valor de MemoryStream para almacenar el contenido de un archivo cargado.

ASP.NET Core admite la carga de uno o varios archivos mediante el enlace de modelos almacenado en búfer de archivos más pequeños y el streaming no almacenado en búfer de archivos de mayor tamaño.

Vea o descargue el código de ejemplo (cómo descargarlo)

Consideraciones sobre la seguridad

Tenga precaución al proporcionar a los usuarios la capacidad de cargar archivos en un servidor. Es posible que los atacantes intenten lo siguiente:

  • Ejecutar ataques por denegación de servicio.
  • Cargar virus o malware.
  • Poner en riesgo redes y servidores de otras maneras.

Estos son algunos de los pasos de seguridad con los que se reduce la probabilidad de sufrir ataques:

  • Cargue los archivos a un área de carga de archivos dedicada, preferiblemente una unidad que no sea de sistema. Una ubicación dedicada facilita la imposición de restricciones de seguridad en los archivos cargados. Deshabilite la ejecución de los permisos en la ubicación de carga de archivos.†
  • Los archivos cargados no se deben persistir en el mismo árbol de directorio que la aplicación.†
  • Use un nombre de archivo seguro determinado por la aplicación. No use un nombre de archivo proporcionado por el usuario ni el nombre de archivo que no sea de confianza del archivo cargado.† HTML codifica el nombre de archivo que no es de confianza al mostrarlo. Por ejemplo, al registrar el nombre de archivo o mostrarlo en la interfaz de usuario (Razor codifica de forma automática la salida HTML).
  • Permita solo las extensiones de archivo aprobadas para la especificación de diseño de la aplicación.†
  • Compruebe que las comprobaciones por parte del cliente se hacen en el servidor.† Las comprobaciones por parte del cliente son fáciles de eludir.
  • Compruebe el tamaño de un archivo cargado. Establezca un límite de tamaño máximo para evitar cargas grandes.†
  • Cuando un archivo cargado con el mismo nombre no deba sobrescribir los archivos, vuelva a comprobar el nombre de archivo en la base de datos o en el almacenamiento físico antes de cargarlo.
  • Ejecute un detector de virus o malware en el contenido cargado antes de que se almacene el archivo.

†La aplicación de ejemplo muestra un enfoque que cumple los criterios.

Advertencia

La carga de código malintencionado en un sistema suele ser el primer paso para ejecutar código que puede:

  • Obtener el control completo de un sistema.
  • Sobrecargar un sistema de manera que el sistema se bloquea.
  • Poner en peligro los datos del usuario o del sistema.
  • Aplicar grafitis a una interfaz de usuario pública.

Vea los siguientes recursos para más información sobre cómo reducir el área expuesta de ataques al aceptar archivos de los usuarios:

Para más información sobre cómo implementar medidas de seguridad, incluidos ejemplos de la aplicación de ejemplo, consulte la sección Validación.

Escenarios de almacenamiento

Las opciones de almacenamiento comunes para los archivos incluyen:

  • Base de datos

    • En el caso de cargas de archivos pequeñas, una base de datos suele ser más rápida que las opciones de almacenamiento físico (sistema de archivos o recurso compartido de red).
    • Una base de datos suele ser más conveniente que las opciones de almacenamiento físico, ya que la recuperación de un registro de base de datos para los datos de usuario puede proporcionar el contenido del archivo (por ejemplo, una imagen de avatar).
    • Una base de datos puede ser más económica que usar un servicio de almacenamiento de datos.
  • Almacenamiento físico (sistema de archivos o recurso compartido de red)

    • Para cargas de archivos de gran tamaño:
      • Los límites de base de datos pueden restringir el tamaño de la carga.
      • El almacenamiento físico suele ser menos económico que el almacenamiento en una base de datos.
    • El almacenamiento físico puede ser más económico que usar un servicio de almacenamiento de datos.
    • El proceso de la aplicación debe tener permisos de lectura y escritura en la ubicación de almacenamiento. Nunca conceda el permiso de ejecución.
  • Servicio de almacenamiento de datos (por ejemplo, Azure Blob Storage)

    • Los servicios suelen ofrecer una escalabilidad y resistencia mejoradas sobre las soluciones locales que normalmente están sujetas a únicos puntos de error.
    • Los servicios pueden tener un costo menor en escenarios de infraestructura de almacenamiento de gran tamaño.

    Para más información, consulte Inicio rápido: Uso de .NET para crear un blob en el almacenamiento de objetos. En el tema se muestra UploadFromFileAsync, pero se puede usar UploadFromStreamAsync para guardar un FileStream en el almacenamiento de blobs cuando se trabaja con un Stream.

Escenarios de carga de archivos

Dos enfoques generales para cargar archivos son el almacenamiento en búfer y el streaming.

de respuesta

El archivo completo se lee en un IFormFile, que es una representación de C# del archivo que se usa para procesar o guardar el archivo.

Los recursos (disco, memoria) que se usan en las cargas de archivos dependen de la cantidad y del tamaño de las cargas de archivos que se realizan simultáneamente. Si una aplicación intenta almacenar demasiadas cargas en el búfer, el sitio se bloquea cuando se queda sin memoria o sin espacio en disco. Si el tamaño o la frecuencia de las cargas de archivos está agotando los recursos de la aplicación, use el streaming.

Nota

Cualquier archivo almacenado en búfer único que supere los 64 KB se mueve de la memoria a un archivo temporal en el disco.

En las secciones siguientes de este tema se habla del almacenamiento en búfer de archivos pequeños:

Streaming

El archivo se recibe de una solicitud de varias partes y lo procesa o guarda directamente la aplicación. El streaming no mejora considerablemente el rendimiento. El streaming reduce las demandas de memoria o espacio en disco cuando se cargan archivos.

El streaming de archivos grandes se describe en la sección Carga de archivos de gran tamaño con streaming.

Carga de archivos pequeños con enlace de modelos almacenado en búfer al almacenamiento físico

Para cargar archivos pequeños, se puede usar un formulario de varias partes o construir una solicitud POST con JavaScript.

En el ejemplo siguiente se muestra el uso de un formulario de Razor Pages para cargar un archivo único (Pages/BufferedSingleFileUploadPhysical.cshtml en la aplicación de ejemplo):

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

El ejemplo siguiente es análogo al ejemplo anterior, salvo en que:

  • (Fetch API) de JavaScript se usa para enviar los datos del formulario.
  • No hay ninguna validación.
<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 realizar la solicitud POST en JavaScript para los clientes que no admiten Fetch API, use uno de estos enfoques:

  • Use un Fetch Polyfill (por ejemplo, window.fetch polyfill (github/fetch)).

  • Use XMLHttpRequest. Por ejemplo:

    <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 admitir las cargas de archivos, los formularios HTML deben especificar un tipo de codificación (enctype) de multipart/form-data.

Para que un elemento de entrada files admita al carga de varios archivos, proporcione el atributo multiple en el elemento <input>:

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

Es posible acceder a archivos individuales cargados en el servidor a través del enlace de modelos mediante IFormFile. La aplicación de ejemplo muestra varias cargas de archivos almacenados en búfer para escenarios de almacenamiento físico y base de datos.

Advertencia

No utilice la propiedad FileName de IFormFile para usos distintos a la presentación y el registro. Para fines de presentación y registro, codifique el nombre de archivo en HTML. Un atacante puede proporcionar un nombre de archivo malintencionado, incluidas rutas de acceso completas o relativas. Las aplicaciones deben:

  • Quitar la ruta de acceso del nombre de archivo proporcionado por el usuario.
  • Guardar el nombre de archivo codificado en HTML, sin la ruta de acceso para la interfaz de usuario o el registro.
  • Generar un nombre de archivo aleatorio nuevo para el almacenamiento.

En el código siguiente se quita la ruta de acceso del nombre del archivo:

string untrustedFileName = Path.GetFileName(pathName);

Los ejemplos proporcionados hasta ahora no tienen en cuenta las consideraciones de seguridad. Se proporciona información adicional en las secciones siguientes y en la aplicación de ejemplo:

Al cargar archivos mediante el enlace de modelos y IFormFile, el método de acción puede aceptar:

Nota

El enlace coincide con los archivos de formulario por nombre. Por ejemplo, el valor name HTML en <input type="file" name="formFile"> debe coincidir con la propiedad/el parámetro enlazado de C# (FormFile). Para más información, consulte la sección Coincidencia del valor de atributo de nombre con el nombre del parámetro del método POST.

En el ejemplo siguiente:

  • Recorre en bucle uno o más archivos cargados.
  • Usa Path.GetTempFileName para devolver una ruta de acceso completa de un archivo, incluido el nombre de archivo.
  • Guarda los archivos en el sistema de archivos local con un nombre de archivo generado por la aplicación.
  • Devuelve el número total y el tamaño de los archivos cargados.
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 generar un nombre de archivo sin una ruta de acceso. En el ejemplo siguiente, la ruta de acceso se obtiene de la configuración:

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

La ruta de acceso pasada a FileStreamdebe incluir el nombre de archivo. Si no se proporciona el nombre de archivo, se produce una UnauthorizedAccessException en tiempo de ejecución.

Los archivos que se cargan usando la técnica IFormFile se almacenan en búfer en memoria o en disco en el servidor web antes de procesarse. Dentro del método de acción, se puede tener acceso al contenido de IFormFile como Stream. Además del sistema de archivos local, los archivos se pueden guardar en un recurso compartido de red o en un servicio de almacenamiento de archivos, como Azure Blob Storage.

Para ver otro ejemplo que recorre en bucle varios archivos para cargar y usa nombres de archivo seguros, consulte Pages/BufferedMultipleFileUploadPhysical.cshtml.cs en la aplicación de ejemplo.

Advertencia

Path.GetTempFileName arroja una IOException si se crean más de 65 535 archivos sin eliminar los archivos temporales anteriores. El límite de 65 535 archivos es un límite por servidor. Para más información sobre este límite en el sistema operativo Windows, consulte las notas en los temas siguientes:

Carga de archivos pequeños con enlace de modelos almacenado en búfer a una base de datos

Para almacenar datos de archivo binario en una base de datos con Entity Framework, defina una propiedad de matriz Byte en la entidad:

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

Especifique una propiedad de modelo de página para la clase que incluya un IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Nota

IFormFile se puede usar directamente como un parámetro de método de acción o como una propiedad de modelo enlazado. En el ejemplo anterior se utiliza una propiedad de modelo enlazado.

FileUpload se usa en el formulario de Razor Pages:

<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>

Cuando el formulario se publique en el servidor, copie el IFormFile en un flujo y guárdelo como matriz de bytes en la base de datos. En el ejemplo siguiente, _dbContext almacena el contexto de base de datos de la aplicación:

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

El ejemplo anterior es similar a un escenario que se muestra en la aplicación de ejemplo:

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

Advertencia

Tenga cuidado al almacenar los datos binarios en bases de datos relacionales, ya que esto puede repercutir adversamente en el rendimiento.

No se base ni confíe en la propiedad FileName de IFormFile sin validarla. La propiedad FileName solo debe usarse para fines de presentación y solo después de la codificación HTML.

Los ejemplos proporcionados no tienen en cuenta las consideraciones de seguridad. Se proporciona información adicional en las secciones siguientes y en la aplicación de ejemplo:

Carga de archivos de gran tamaño con streaming

En el ejemplo siguiente se muestra cómo usar JavaScript para transmitir un archivo a una acción de controlador. El token de antifalsificación del archivo se genera por medio de un atributo de filtro personalizado y se pasa a los encabezados HTTP del cliente, en lugar de en el cuerpo de la solicitud. Dado que el método de acción procesa los datos cargados directamente, el enlace de modelos del formulario se deshabilita por otro filtro personalizado. Dentro de la acción, el contenido del formulario se lee usando un MultipartReader (que lee cada MultipartSection individual), de forma que el archivo se procesa o el contenido se almacena, según corresponda. Una vez leídas las secciones de varias partes, la acción realiza su propio enlace de modelos.

La respuesta de la página inicial carga el formulario y guarda un token de antifalsificación en una cookie (a través del atributo GenerateAntiforgeryTokenCookieAttribute). El atributo usa la compatibilidad de antifalsificación integrada de ASP.NET Core para establecer una cookie con un token de solicitud:

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

El DisableFormValueModelBindingAttribute se usa para deshabilitar el enlace de modelos:

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

En la aplicación de ejemplo, GenerateAntiforgeryTokenCookieAttribute y DisableFormValueModelBindingAttribute se aplican como filtros a los modelos de aplicación de la página de /StreamedSingleFileUploadDb y /StreamedSingleFileUploadPhysical en Startup.ConfigureServices con las convenciones de Razor Pages:

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

Dado que el enlace de modelos no lee el formulario, los parámetros enlazados desde el formulario no se enlazan (la consulta, la ruta y el encabezado siguen funcionando). El método de acción funciona directamente con la propiedad Request. Se usa un elemento MultipartReader para leer cada sección. Los datos de clave-valor se almacenan en un KeyValueAccumulator. Una vez leídas las secciones de varias partes, el contenido del KeyValueAccumulator se usa para enlazar los datos del formulario a un tipo de modelo.

El método StreamingController.UploadDatabase completo para streaming a una base de datos con 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));
        }
    }
}

El método StreamingController.UploadPhysical completo para streaming a una ubicación física:

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

En la aplicación de ejemplo, las comprobaciones de validación las controla FileHelpers.ProcessStreamedFile.

Validación

La clase FileHelpers de la aplicación de ejemplo muestra varias comprobaciones de cargas de archivos de streaming y IFormFile almacenado en búfer. Para procesar cargas de archivos almacenadas en búfer de IFormFile en la aplicación de ejemplo, consulte el método ProcessFormFile en el archivo Utilities/FileHelpers.cs. Para procesar archivos de streaming, consulte el método ProcessStreamedFile en el mismo archivo.

Advertencia

Los métodos de procesamiento de validación mostrados en la aplicación de ejemplo no examinan el contenido de los archivos cargados. En la mayoría de los escenarios de producción, se usa una API de analizador de virus/malware en el archivo antes de que el archivo esté disponible para los usuarios u otros sistemas.

Aunque el ejemplo de tema proporciona un ejemplo funcional de técnicas de validación, no implemente la clase FileHelpers en una aplicación de producción a menos que:

  • Comprenda totalmente la implementación.
  • Modifique la implementación según corresponda en función del entorno y las especificaciones de la aplicación.

No implemente nunca de manera indiscriminada el código de seguridad en una aplicación sin abordar estos requisitos.

Validación del contenido

Use una API de detección de virus/malware de terceros en el contenido cargado.

El análisis de archivos exige recursos del servidor en escenarios de gran volumen. Si disminuye el rendimiento de procesamiento de solicitudes debido al análisis de archivos, considere la posibilidad de descargar el trabajo de análisis a un servicio en segundo plano, posiblemente un servicio que se ejecute en otro servidor desde el servidor de la aplicación. Habitualmente, los archivos cargados se almacenan en un área en cuarentena hasta que el programa de detección de virus en segundo plano los revisa. Cuando se pasa un archivo, se mueve a la ubicación de almacenamiento de archivos habitual. Por lo general, estos pasos se realizan junto con un registro de base de datos que indica el estado de análisis de un archivo. Mediante el uso de este enfoque, la aplicación y el servidor de aplicaciones permanecen centrados en responder a las solicitudes.

Validación de la extensión del archivo

La extensión del archivo cargado debe comprobarse con una lista de extensiones permitidas. Por ejemplo:

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
}

Validación de firma del archivo

La firma de un archivo viene determinada por los primeros bytes al principio de un archivo. Estos bytes se pueden usar para indicar si la extensión coincide con el contenido del archivo. La aplicación de ejemplo comprueba las firmas de archivo de algunos tipos de archivo comunes. En el ejemplo siguiente, la firma de archivo de una imagen JPEG se comprueba con respecto al archivo:

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 obtener firmas de archivo adicionales, use la base de datos de firmas de archivo (resultado de búsqueda de Google) y las especificaciones de archivo oficiales. Las especificaciones del archivo oficial de consultoría pueden asegurarse de que las firmas seleccionadas sean válidas.

Seguridad de nombre de archivo

Nunca use un nombre de archivo proporcionado por el cliente para guardar un archivo en el almacenamiento físico. Cree un nombre de archivo seguro para el archivo con Path.GetRandomFileName o Path.GetTempFileName para crear una ruta de acceso completa (incluido el nombre de archivo) para el almacenamiento temporal.

Razor codifica automáticamente en HTML los valores de propiedad para mostrarlos. El código siguiente es seguro de usar:

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

Fuera de Razor, siempre use HtmlEncode al contenido del nombre de archivo de una solicitud de un usuario.

Muchas implementaciones deben incluir una comprobación de que el archivo existe; de lo contrario, el archivo se sobrescribe por un archivo con el mismo nombre. Proporcione lógica adicional para satisfacer las especificaciones de la aplicación.

Validación del tamaño

Limite el tamaño de los archivos cargados.

En la aplicación de ejemplo, el tamaño del archivo está limitado a 2 MB (se indica en bytes). El límite se proporciona a través de Configuración del archivo appsettings.json:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit se inserta en las clases PageModel:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Cuando el tamaño de un archivo supera el límite, se rechaza el archivo:

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

Coincidencia del valor de atributo de nombre con el nombre del parámetro del método POST

En los formularios que no son de Razorque realizan la operación POST en los datos de formulario o usan directamente FormData de JavaScript, el nombre especificado en el elemento del formulario o FormData debe coincidir con el nombre del parámetro en la acción del controlador.

En el ejemplo siguiente:

  • Cuando se usa un elemento <input>, el atributo name se establece en el valor battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Cuando se usa FormData en JavaScript, el nombre se establece en el valor battlePlans:

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

Use un nombre coincidente para el parámetro del método de C# (battlePlans):

  • Para un método de control de páginas de Razor Pages denominado Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Para un método de acción de controlador POST de MVC:

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

Configuración del servidor y de la aplicación

Límite de longitud del cuerpo de varias partes

MultipartBodyLengthLimit establece el límite de la longitud de cada cuerpo de varias partes. Las secciones del formulario que superan este límite inician una InvalidDataException cuando se analizan. El valor predeterminado es 134 217 728 (128 MB). Personalice el límite mediante el valor MultipartBodyLengthLimit en Startup.ConfigureServices:

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

RequestFormLimitsAttribute se utiliza para establecer el MultipartBodyLengthLimit para una sola página o acción.

En una aplicación de Razor Pages, aplique el filtro con una convención en 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);

En una aplicación de Razor Pages o una aplicación MVC, aplique el filtro al método de acción o al modelo de página:

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

Kestrel tamaño máximo del cuerpo de la solicitud

En el caso de las aplicaciones hospedadas por Kestrel, el tamaño máximo predeterminado del cuerpo de solicitud es 30 000 000 bytes, que son aproximadamente 28,6 MB. Personalice el límite con la opción MaxRequestBodySize del servidor de Kestrel:

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

RequestSizeLimitAttribute se usa para establecer el valor de MaxRequestBodySize de una sola página o acción.

En una aplicación de Razor Pages, aplique el filtro con una convención en 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);

En una aplicación de Razor Pages o una aplicación MVC, aplique el filtro al método de acción o a la clase de control de página:

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

Otros límites de Kestrel

Otros límites de Kestrel pueden aplicarse a las aplicaciones hospedadas por Kestrel:

IIS

El límite predeterminado de solicitudes (maxAllowedContentLength) es 30 000 000 bytes, que son aproximadamente 28,6 MB. Personalice el límite en el archivo web.config. En el ejemplo siguiente, el límite se establece en 50 MB (52 428 800 bytes):

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

La configuración de maxAllowedContentLength solo ocurre en IIS. Para más información, vea Características de la solicitud de <requestLimits>.

Aumente el tamaño máximo del cuerpo de la solicitud HTTP estableciendo IISServerOptions.MaxRequestBodySize en Startup.ConfigureServices. En el ejemplo siguiente, el límite se establece en 50 MB (52 428 800 bytes):

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

Para más información, vea Host ASP.NET Core on Windows with IIS (Hospedar ASP.NET Core en Windows con IIS).

Solucionar problemas

Aquí incluimos algunos problemas comunes que pueden surgir al cargar archivos, así como sus posibles soluciones.

Error No encontrado al implementar en un servidor IIS

El error siguiente indica que el archivo cargado supera la longitud configurada del contenido del servidor:

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

Para más información, consulte la sección sobre IIS.

Error de conexión

Un error de conexión y una conexión del servidor de restablecimiento probablemente indica que el archivo cargado supera el tamaño máximo del cuerpo de la solicitud de Kestrel. Para más información, consulte la sección Tamaño máximo del cuerpo de la solicitud de Kestrel. Los límites de conexión del cliente de Kestrel también pueden requerir ajustes.

Excepción de referencia nula con IFormFile

Si el controlador acepta archivos cargados con IFormFile, pero el valor es null, confirme que el formulario HTML especifica un valor enctype de multipart/form-data. Si este atributo no está establecido en el elemento <form>, no se llevará a cabo la carga del archivo y cualquier argumento IFormFile enlazado será null. Confirme también que la nomenclatura de la carga en los datos de formulario coincide con la nomenclatura de la aplicación.

El flujo era demasiado largo

Los ejemplos de este tema se basan en MemoryStream para almacenar el contenido del archivo cargado. El límite de tamaño de MemoryStream es int.MaxValue. Si el escenario de carga de archivos de la aplicación requiere almacenar contenido de archivo superior a 50 MB, use un enfoque alternativo que no dependa de un único valor de MemoryStream para almacenar el contenido de un archivo cargado.

Recursos adicionales