Передача файлов в ASP.NET Core

Рутгер Шторм

Действия ASP.NET Core поддерживают передачу одного или нескольких файлов с помощью привязки модели с буферизацией для небольших файлов или потоковой передачи без буферизации для более крупных файлов.

Просмотреть или скачать образец кода (описание загрузки)

Вопросы безопасности

Необходимо соблюдать осторожность при предоставлении пользователям возможности отправки файлов на сервер. Злоумышленники могут попытаться:

  • Выполнить атаку типа отказ в обслуживании.
  • Передать вирусы и вредоносные программы.
  • Нарушить безопасность сетей и серверов другими способами.

Ниже приведены некоторые действия по обеспечению безопасности, которые снижают вероятность успешных атак.

  • Передавайте файлы в выделенную область для отправки файлов, желательно не на системный диск. Использование выделенного расположения упрощает применение мер безопасности к отправленным файлам. Отключение разрешений на выполнение в папке отправки файла.†
  • Не сохраняйте отправленные файлы в том же дереве каталогов, что и app.†
  • Используйте безопасное имя файла, определяемое приложением. Не используйте имя файла, предоставленное пользователем или ненадежным именем файла отправленного файла.† HTML, кодируйте ненадежное имя файла при отображении. Например, ведение журнала имени файла или отображение в пользовательском интерфейсе (Razor автоматически кодирует выходные данные HTML).
  • Разрешить только утвержденные расширения файлов для спецификации конструктора приложения.†
  • Убедитесь, что клиентские проверка выполняются на сервере.† клиентских проверка легко обойти.
  • Проверьте размер отправленного файла. Задайте максимальное ограничение размера, чтобы предотвратить крупные отправки.†
  • Если файлы не должны перезаписываться переданным файлом с тем же именем, перед отправкой файла проверьте его имя в базе данных или физическом хранилище.
  • Запустите сканер для проверки отправляемого содержимого на наличие вирусов и вредоносных программ, прежде чем сохранять файл.

† Пример приложения демонстрирует подход, соответствующий критериям.

Предупреждение

Отправка в систему вредоносного кода часто является первым шагом перед выполнением кода, который может:

  • полностью получить контроль над системой;
  • перезагрузить систему так, что она окажется в неработоспособном состоянии;
  • скомпрометировать пользовательские или системные данные;
  • применить граффити к открытому интерфейсу.

Сведения об уменьшении контактной зоны атаки во время приема файлов от пользователей см. в следующих ресурсах:

Дополнительные сведения о реализации мер безопасности, включая примеры из примера приложения, см. в статье Передача файлов в ASP.NET Core.

Сценарии использования хранилища

К общим вариантам хранилища файлов относятся следующие:

  • База данных

    • Для отправки небольших файлов база данных часто быстрее, чем параметры физического хранилища (файловой системы или сетевой папки).
    • База данных часто более удобна по сравнению с вариантами физического хранилища, так как получение записи из базы пользовательских данных может одновременно предоставить содержимое файла (например, изображение аватара).
    • База данных может быть менее дорогой, чем использование облачной службы хранилища данных.
  • Физическое хранилище (файловая система или сетевая папка).

    • Для отправки больших файлов:
      • Ограничения базы данных могут ограничивать размер передачи.
      • Физическое хранилище часто менее экономически выгодно, чем хранилище в базе данных.
    • Физическое хранилище может быть менее дорогостоящим, чем использование облачной службы хранилища данных.
    • Процесс приложения должен иметь разрешения на чтение и запись для места хранения. Никогда не предоставляйте разрешение на выполнение.
  • Служба облачного хранилища данных, например Хранилище BLOB-объектов Azure.

    • Обычно службы обеспечивают улучшенную масштабируемость и устойчивость по сравнению с локальными решениями, которые обычно подвержены единым точкам отказа.
    • Затраты на использование служб обычно ниже в сценариях с крупномасштабной инфраструктурой хранения.

    Дополнительные сведения см . в кратком руководстве. Использование .NET для создания большого двоичного объекта в хранилище объектов.

Небольшие и большие файлы

Определение небольших и больших файлов зависит от доступных вычислительных ресурсов. Приложения должны тестировать подход к хранилищу, используемый для обработки ожидаемых размеров. Производительность памяти, ЦП, диска и базы данных.

Хотя определенные границы не могут быть предоставлены для небольших и больших для развертывания, ниже приведены некоторые связанные значения по умолчанию AspNetCore для FormOptions:

  • По умолчанию HttpRequest.Form не буферизирует весь текст запроса (BufferBody), но буферизирует все включенные многопартийные файлы форм.
  • MultipartBodyLengthLimit— максимальный размер буферных файлов форм, по умолчанию — 128 МБ.
  • MemoryBufferThresholdуказывает, сколько нужно буферизуть файлы в памяти перед переходом на буферный файл на диске, по умолчанию — 64 КБ. MemoryBufferThreshold выступает в качестве границы между небольшими и большими файлами, которые вызываются или снижаются в зависимости от ресурсов и сценариев приложений.

Дополнительные сведения смFormOptions. в исходном коде.

Сценарии передачи файлов

Есть два распространенных подхода к передаче файлов — буферизация и потоковая передача.

Буферизация

Весь файл считывается в .IFormFile IFormFile — это представление файла C#, используемого для обработки или сохранения файла.

Диск и память, используемые отправкой файлов, зависят от количества и размера одновременных отправки файлов. При попытке приложения поместить в буфер слишком много файлов может произойти аварийное завершение работы сайта из-за нехватки памяти или места на диске. Если размер или частота отправки файлов исчерпывают ресурсы приложения, используйте потоковую передачу.

Один буферизованный файл размером свыше 64 КБ перемещается из памяти во временный файл на диске.

Временные файлы для больших запросов записываются в расположение, названное в переменной ASPNETCORE_TEMP среды. Если ASPNETCORE_TEMP он не определен, файлы записываются во временную папку текущего пользователя.

Буферизация небольших файлов описана в следующих разделах этой статьи:

Потоковая передача

Файл можно получить с помощью составного запроса. Затем он обрабатывается или сохраняется приложением напрямую. Потоковая передача повышает производительность не значительно. При отправке файлов потоковая передача снижает нагрузку на память или на место на диске.

Потоковая передача больших файлов рассматривается в разделе Передача больших файлов с помощью потоковой передачи.

Передача небольших файлов с привязкой буферизованной модели к физическому хранилищу

Для передачи небольших файлов можно применить составную форму или сформировать запрос POST на языке JavaScript.

В следующем примере показано использование формы Pages для отправки Razor одного файла (Pages/BufferedSingleFileUploadPhysical.cshtml в примере приложения):

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

Следующий пример аналогичен предыдущему примеру, за исключением следующего:

  • JavaScript (Fetch API) используется для отправки данных формы.
  • Проверка не выполняется.
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>

Для выполнения отправки формы в JavaScript для клиентов, которые не поддерживают Fetch API, используйте один из следующих подходов:

  • Используйте функцию Fetch Polyfill (например, window.fetch polyfill (github/fetch)).

  • Используйте XMLHttpRequest. Например:

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

Для поддержки передачи файлов в HTML-формах должен указываться тип кодировки enctype со значением multipart/form-data.

Для элемента ввода files, поддерживающего отправку нескольких файлов, в элементе <input> необходимо указать атрибут multiple:

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

Доступ к отдельным файлам, переданным на сервер, можно получать посредством привязки модели с помощью интерфейса IFormFile. В примере приложения показано несколько отправок буферизованных файлов для баз данных и физических хранилищ.

Предупреждение

Не используйте свойство FileName объекта IFormFile, кроме как для отображения и ведения журнала. При отображении или ведении журнала кодируйте имя файла в формате HTML. Злоумышленник может предоставить имя вредоносного файла, включая полные или относительные пути. Приложения должны:

  • удалить путь из имени файла, указываемого пользователем;
  • сохранить имя файла, закодированное в формате HTML, откуда был удален путь, для пользовательского интерфейса или ведения журнала.
  • создать случайное имя файла для хранения.

Следующий код удаляет путь из имени файла:

string untrustedFileName = Path.GetFileName(pathName);

В приведенных выше примерах не учитываются вопросы безопасности. Дополнительные сведения приведены в следующих разделах и в примере приложения.

При отправке файлов с помощью привязки модели и IFormFile метод действия может принимать следующие файлы:

Примечание.

Привязка сопоставляет файлы форм по имени. Например, значение HTML name в <input type="file" name="formFile"> должно соответствовать привязанному к C# параметру или свойству (FormFile). Дополнительные сведения см. в разделе Сопоставление значения атрибута имени и имени параметра метода POST.

Следующий пример:

  • Циклично отправляет один или несколько передаваемых файлов.
  • Использует метод Path.GetTempFileName, чтобы вернуть полный путь к файлу, включая его имя.
  • Сохраняет файлы в локальную файловую систему, используя имя файла, созданное приложением.
  • Возвращает общее число и размер отправленных файлов.
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

Чтобы создать имя файла без пути, используйте Path.GetRandomFileName. В следующем примере путь получен из конфигурации:

foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}

Передаваемый в FileStream путь должен содержать имя файла. Если имя файла не указано, в среде выполнения возникает исключение UnauthorizedAccessException.

Файлы, передаваемые с помощью интерфейса IFormFile, буферизуются в памяти или на диске на сервере перед обработкой. Внутри метода действия содержимое IFormFile доступно в виде Stream. Помимо локальной файловой системы, файлы можно сохранять в сетевой папке или в службе хранилища файлов, например в хранилище BLOB-объектов Azure.

Другой пример, который циклирует по нескольким файлам для отправки и использует безопасные имена файлов, см Pages/BufferedMultipleFileUploadPhysical.cshtml.cs . в примере приложения.

Предупреждение

Метод Path.GetTempFileName вызывает исключение IOException в случае создания более чем 65 535 файлов без удаления предыдущих временных файлов. Ограничение в 65 535 файлов предусмотрено для каждого сервера. Дополнительные сведения об этом ограничении в ОС Windows см. в примечаниях в следующих разделах:

Передача небольших файлов с привязкой буферизованной модели к базе данных

Для сохранения данных двоичных файлов в базе данных с помощью Entity Framework определите для сущности свойство массива Byte:

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

Укажите свойство модели страницы для класса, который содержит IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Примечание.

IFormFile можно использовать непосредственно как параметр метода действия или свойство модели привязки. В предыдущем примере используется свойство модели привязки.

Используется FileUpload в Razor форме Pages:

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

При публикации формы на сервере скопируйте IFormFile в поток и сохраните его в базе данных в виде массива байтов. В следующем примере _dbContext сохраняет контекст базы данных приложения:

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}

Предыдущий пример похож на сценарий, продемонстрированный в примере приложения:

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

Предупреждение

При сохранении двоичных данных в реляционных базах данных следует соблюдать осторожность, так как это может отрицательно сказаться на производительности.

Свойство FileName параметра IFormFile требует обязательной проверки. Свойство FileName следует использовать только в целях вывода и только после HTML-кодирования.

В приведенных выше примерах не учитываются вопросы безопасности. Дополнительные сведения приведены в следующих разделах и в примере приложения.

Передача больших файлов с помощью потоковой передачи

В примере 3.1 показано, как использовать JavaScript для потоковой передачи файла в действие контроллера. Токен против подделки файла создается с помощью пользовательского атрибута фильтра и передается в заголовках HTTP клиента, а не в теле запроса. Так как метод действия обрабатывает передаваемые данные напрямую, привязка модели формы отключается другим пользовательским фильтром. Внутри действия содержимое формы считывается с помощью объекта MultipartReader, который считывает каждый объект MultipartSection по отдельности, обрабатывая файл или сохраняя содержимое. После считывания составных разделов действие выполняет собственную привязку модели.

Начальный ответ страницы загружает форму и сохраняет маркер антифоргерии в виде cookie (через GenerateAntiforgeryTokenCookieAttribute атрибут). Этот атрибут использует встроенную антифоргерскую поддержку Core ASP.NET для установки cookie маркера запроса:

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

Для отключения привязки модели используется DisableFormValueModelBindingAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

В примере приложения GenerateAntiforgeryTokenCookieAttribute и применяются в качестве фильтров к моделям /StreamedSingleFileUploadDb приложений страницы и Startup.ConfigureServices/StreamedSingleFileUploadPhysical в использованииRazorсоглашенийDisableFormValueModelBindingAttribute 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());
            });
});

Так как привязка модели не считывает форму, параметры, привязанные из формы, не привязываются (запросы, маршруты и заголовки продолжают работать). Метод действия работает напрямую со свойством Request. Для считывания каждого раздела служит объект MultipartReader. Данные "ключ — значение" хранятся в KeyValueAccumulator. После считывания составных разделов содержимое KeyValueAccumulator используется для привязки данных формы к типу модели.

Полный StreamingController.UploadDatabase метод потоковой передачи в базу данных с помощью EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

Полный метод StreamingController.UploadPhysical для потоковой передачи в физическое расположение:

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

        return BadRequest(ModelState);
    }

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

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

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

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

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

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

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

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

В примере приложения проверки обрабатываются с помощью FileHelpers.ProcessStreamedFile.

Проверка

Класс примера приложения FileHelpers демонстрирует несколько проверка для отправки буферированных IFormFile и потоковых файлов. Сведения об обработке IFormFile отправки буферизованного файла в примере приложения см ProcessFormFile . в методе Utilities/FileHelpers.cs в файле. Сведения об обработке потоковых файлов см. в описании метода ProcessStreamedFile в том же файле.

Предупреждение

Методы обработки проверки, показанные в примере приложения, не проверяют содержимое отправленных файлов. В большинстве рабочих сценариев в файле применяется API сканирования на наличие вирусов и вредоносных программ, прежде чем сделать файл доступным для пользователей или других систем.

Хотя пример в разделе содержит рабочий пример методов проверки, не следует реализовывать класс FileHelpers в рабочем приложении, кроме таких случаев:

  • Вы полностью разбираетесь в реализации.
  • Вы изменяете реализацию соответствующим образом для среды и спецификаций приложения.

Никогда не реализуйте код безопасности в приложении, не выполнив эти требования.

Проверка содержимого

Используйте сторонний API сканирования на наличие вирусов и вредоносных программ для отправленного содержимого.

Сканирование файлов требует использования ресурсов сервера в сценариях с большими объемами данных. Если производительность обработки запросов снижается из-за сканирования файлов, рассмотрите возможность разгрузки сканирования путем использования фоновой службы, возможно, службы, которая работает на сервере, отличном от сервера приложения. Как правило, передаваемые файлы хранятся в карантинной области до тех пор, пока фоновый сканер не проверит их на наличие вирусов. При передаче файл перемещается в нормальное место хранения файлов. Эти действия обычно выполняются вместе с записью базы данных, которая указывает состояние сканирования файла. Благодаря такому подходу приложение и сервер приложений остаются в режиме реагирования на запросы.

Проверка расширения файла

Расширение переданного файла должно проверяться в соответствии со списком разрешенных расширений. Например:

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

Проверка подписи файла

Подпись файла определяется по первым нескольким байтам в начале файла. Эти байты можно использовать, чтобы указать, совпадает ли расширение с содержимым файла. Пример приложения проверяет подписи файлов на соответствие нескольким распространенным типам файлов. В следующем примере проверяется подпись файла для изображения JPEG:

private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}

Чтобы получить дополнительные подписи файлов, используйте базу данных подписей файлов (результат поиска Google) и официальные спецификации файлов. Консультирование официальных спецификаций файлов может гарантировать, что выбранные подписи действительны.

Безопасность имени файла

Никогда не используйте для хранения файла в физическом хранилище имя, предоставляемое клиентом. Создайте надежное имя файла с помощью Path.GetRandomFileName или Path.GetTempFileName, чтобы получить полный путь (включая имя файла) для временного хранилища.

Razor Автоматически HTML кодирует значения свойств для отображения. Ниже приведен безопасный код, который можно использовать.

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

За пределами всегда HtmlEncode содержимого Razorимени файла из запроса пользователя.

Во многих реализациях следует включать проверку существования файла. В противном случае файл перезаписывается файлом с тем же именем. Предоставьте дополнительную логику для соответствия спецификациям приложения.

Проверка размера

Ограничьте размер передаваемых файлов.

В примере приложения размер файла ограничен 2 МБ (указывается в байтах). Ограничение предоставляется с помощью конфигурации из appsettings.json файла:

{
  "FileSizeLimit": 2097152
}

В классы PageModel внедряется FileSizeLimit.

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Если размер файла превышает ограничение, файл отклоняется:

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

Сопоставление значения атрибута имени и имени параметра метода POST

В формах, неRazor относящихся к данным POST или напрямую использующее JavaScript FormData , имя, указанное в элементе формы, или FormData должно соответствовать имени параметра в действии контроллера.

В следующем примере :

  • При использовании элемента <input> атрибуту name присваивается значение battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • При использовании FormData в JavaScript для имени задается значение battlePlans:

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

Используйте соответствующее имя для параметра метода C# (battlePlans):

  • Razor Для метода обработчика страниц Pages с именемUpload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Для метода действия контроллера POST приложения MVC:

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

Конфигурация сервера и приложения

Ограничение длины составного текста

MultipartBodyLengthLimit устанавливает ограничение длины каждого составного текста. Разделы формы, превышающие это ограничение, вызовут InvalidDataException при синтаксическом анализе. Значение по умолчанию — 134 217 728 байт (128 МБ). Настройте ограничение с помощью параметра MultipartBodyLengthLimit в Startup.ConfigureServices:

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

RequestFormLimitsAttribute используется для настройки MultipartBodyLengthLimit для одной страницы или действия.

Razor В приложении Pages примените фильтр с соглашением в Startup.ConfigureServicesследующих статьях:

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

Razor В приложении Pages или приложении MVC примените фильтр к модели страницы или методу действия:

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

Kestrel максимальный размер текста запроса

Для приложений, размещенных по Kestrelумолчанию, максимальный размер текста запроса составляет 30 000 000 байт, что составляет примерно 28,6 МБ. Настройте ограничение с помощью параметра сервера MaxRequestBodySizeKestrel :

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

RequestSizeLimitAttribute используется для настройки MaxRequestBodySize для одной страницы или действия.

Razor В приложении Pages примените фильтр с соглашением в Startup.ConfigureServicesследующих статьях:

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

Razor В приложении страниц или приложении MVC примените фильтр к классу или методу действия обработчика страниц:

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

Его RequestSizeLimitAttribute также можно применить с помощью директивы @attributeRazor :

@attribute [RequestSizeLimitAttribute(52428800)]

Другие Kestrel ограничения

Другие Kestrel ограничения могут применяться для приложений, размещенных в Kestrel:

IIS

Ограничение запроса по умолчанию составляетmaxAllowedContentLength 30 000 000 байт, что составляет примерно 28,6 МБ. Настройте ограничение в web.config файле. В следующем примере ограничение равно 50 МБ (52 428 800 байт):

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

Параметр maxAllowedContentLength применяется только к службам IIS. Дополнительные сведения см. в разделе "Ограничения <requestLimits>запросов".

Устранить неполадки

Ниже описываются некоторые распространенные проблемы, которые возникают при передаче файлов, и возможные способы их решения.

Ошибка "Не найдено" при развертывании на сервере IIS

Следующая ошибка свидетельствует о том, что размер передаваемых файлов превышает настроенное на сервере ограничение длины содержимого:

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

Дополнительные сведения см. в описании IIS.

Сбой подключения

Ошибка подключения и подключение к серверу сброса, вероятно, указывает, что загруженный файл превышает Kestrelмаксимальный размер текста запроса. Дополнительные сведения см. в Kestrel разделе максимального размера текста запроса. Kestrel Ограничения подключения клиента также могут потребовать корректировки.

Исключение, связанное с пустой ссылкой в IFormFile

Если контроллер принимает передаваемые файлы с помощью IFormFile, но значение равно null, проверьте, указан ли в HTML-форме атрибут enctype со значением multipart/form-data. Если этот атрибут не задан для элемента <form>, передача файлов происходить не будет и все связанные аргументы IFormFile будут иметь значение null. Убедитесь также, что именование передачи в данных формы совпадает с именованием приложения.

Поток слишком длинный

В примерах в этом разделе MemoryStream используется для хранения содержимого отправленного файла. Максимальный размер MemoryStream ограничен значением int.MaxValue. Если в сценарии передачи файлов в приложении требуется хранить содержимое, размер которого больше 50 МБ, используйте другой подход, не зависящий от одного только MemoryStream.

Действия ASP.NET Core поддерживают передачу одного или нескольких файлов с помощью привязки модели с буферизацией для небольших файлов или потоковой передачи без буферизации для более крупных файлов.

Просмотреть или скачать образец кода (описание загрузки)

Вопросы безопасности

Необходимо соблюдать осторожность при предоставлении пользователям возможности отправки файлов на сервер. Злоумышленники могут попытаться:

  • Выполнить атаку типа отказ в обслуживании.
  • Передать вирусы и вредоносные программы.
  • Нарушить безопасность сетей и серверов другими способами.

Ниже приведены некоторые действия по обеспечению безопасности, которые снижают вероятность успешных атак.

  • Передавайте файлы в выделенную область для отправки файлов, желательно не на системный диск. Использование выделенного расположения упрощает применение мер безопасности к отправленным файлам. Отключение разрешений на выполнение в папке отправки файла.†
  • Не сохраняйте отправленные файлы в том же дереве каталогов, что и app.†
  • Используйте безопасное имя файла, определяемое приложением. Не используйте имя файла, предоставленное пользователем или ненадежным именем файла отправленного файла.† HTML, кодируйте ненадежное имя файла при отображении. Например, ведение журнала имени файла или отображение в пользовательском интерфейсе (Razor автоматически кодирует выходные данные HTML).
  • Разрешить только утвержденные расширения файлов для спецификации конструктора приложения.†
  • Убедитесь, что клиентские проверка выполняются на сервере.† клиентских проверка легко обойти.
  • Проверьте размер отправленного файла. Задайте максимальное ограничение размера, чтобы предотвратить крупные отправки.†
  • Если файлы не должны перезаписываться переданным файлом с тем же именем, перед отправкой файла проверьте его имя в базе данных или физическом хранилище.
  • Запустите сканер для проверки отправляемого содержимого на наличие вирусов и вредоносных программ, прежде чем сохранять файл.

† Пример приложения демонстрирует подход, соответствующий критериям.

Предупреждение

Отправка в систему вредоносного кода часто является первым шагом перед выполнением кода, который может:

  • полностью получить контроль над системой;
  • перезагрузить систему так, что она окажется в неработоспособном состоянии;
  • скомпрометировать пользовательские или системные данные;
  • применить граффити к открытому интерфейсу.

Сведения об уменьшении контактной зоны атаки во время приема файлов от пользователей см. в следующих ресурсах:

Дополнительные сведения о реализации мер безопасности, включая примеры из примера приложения, см. в статье Передача файлов в ASP.NET Core.

Сценарии использования хранилища

К общим вариантам хранилища файлов относятся следующие:

  • База данных

    • В случае отправки небольших файлов база данных часто работает быстрее, чем физическое хранилище (файловая система или сетевая папка).
    • База данных часто более удобна по сравнению с вариантами физического хранилища, так как получение записи из базы пользовательских данных может одновременно предоставить содержимое файла (например, изображение аватара).
    • Эксплуатация базы данных потенциально дешевле, чем использование службы хранилища данных.
  • Физическое хранилище (файловая система или сетевая папка).

    • Для отправки больших файлов:
      • Ограничения базы данных могут ограничивать размер передачи.
      • Физическое хранилище часто менее экономически выгодно, чем хранилище в базе данных.
    • Эксплуатация физического хранилища потенциально дешевле, чем использование службы хранилища данных.
    • Процесс приложения должен иметь разрешения на чтение и запись для места хранения. Никогда не предоставляйте разрешение на выполнение.
  • Служба хранилища данных (например, хранилище BLOB-объектов Azure).

    • Обычно службы обеспечивают улучшенную масштабируемость и устойчивость по сравнению с локальными решениями, которые обычно подвержены единым точкам отказа.
    • Затраты на использование служб обычно ниже в сценариях с крупномасштабной инфраструктурой хранения.

    Дополнительные сведения см . в кратком руководстве. Использование .NET для создания большого двоичного объекта в хранилище объектов.

Сценарии передачи файлов

Есть два распространенных подхода к передаче файлов — буферизация и потоковая передача.

Буферизация

Весь файл считывается в IFormFile (представление файла на C#, используемого для обработки или сохранения файла).

Потребление ресурсов (диска, памяти) при передаче файлов зависит от количества и размера одновременно передаваемых файлов. При попытке приложения поместить в буфер слишком много файлов может произойти аварийное завершение работы сайта из-за нехватки памяти или места на диске. Если размер или частота отправки файлов исчерпывают ресурсы приложения, используйте потоковую передачу.

Примечание.

Один буферизованный файл размером свыше 64 КБ перемещается из памяти во временный файл на диске.

Буферизация небольших файлов описана в следующих разделах этой статьи:

Потоковая передача

Файл можно получить с помощью составного запроса. Затем он обрабатывается или сохраняется приложением напрямую. Потоковая передача повышает производительность не значительно. При отправке файлов потоковая передача снижает нагрузку на память или на место на диске.

Потоковая передача больших файлов рассматривается в разделе Передача больших файлов с помощью потоковой передачи.

Передача небольших файлов с привязкой буферизованной модели к физическому хранилищу

Для передачи небольших файлов можно применить составную форму или сформировать запрос POST на языке JavaScript.

В следующем примере показано использование формы Pages для отправки Razor одного файла (Pages/BufferedSingleFileUploadPhysical.cshtml в примере приложения):

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

Следующий пример аналогичен предыдущему примеру, за исключением следующего:

  • JavaScript (Fetch API) используется для отправки данных формы.
  • Проверка не выполняется.
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>

Для выполнения отправки формы в JavaScript для клиентов, которые не поддерживают Fetch API, используйте один из следующих подходов:

  • Используйте функцию Fetch Polyfill (например, window.fetch polyfill (github/fetch)).

  • Используйте XMLHttpRequest. Например:

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

Для поддержки передачи файлов в HTML-формах должен указываться тип кодировки enctype со значением multipart/form-data.

Для элемента ввода files, поддерживающего отправку нескольких файлов, в элементе <input> необходимо указать атрибут multiple:

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

Доступ к отдельным файлам, переданным на сервер, можно получать посредством привязки модели с помощью интерфейса IFormFile. В примере приложения показано несколько отправок буферизованных файлов для баз данных и физических хранилищ.

Предупреждение

Не используйте свойство FileName объекта IFormFile, кроме как для отображения и ведения журнала. При отображении или ведении журнала кодируйте имя файла в формате HTML. Злоумышленник может предоставить имя вредоносного файла, включая полные или относительные пути. Приложения должны:

  • удалить путь из имени файла, указываемого пользователем;
  • сохранить имя файла, закодированное в формате HTML, откуда был удален путь, для пользовательского интерфейса или ведения журнала.
  • создать случайное имя файла для хранения.

Следующий код удаляет путь из имени файла:

string untrustedFileName = Path.GetFileName(pathName);

В приведенных выше примерах не учитываются вопросы безопасности. Дополнительные сведения приведены в следующих разделах и в примере приложения.

При отправке файлов с помощью привязки модели и IFormFile метод действия может принимать следующие файлы:

Примечание.

Привязка сопоставляет файлы форм по имени. Например, значение HTML name в <input type="file" name="formFile"> должно соответствовать привязанному к C# параметру или свойству (FormFile). Дополнительные сведения см. в разделе Сопоставление значения атрибута имени и имени параметра метода POST.

Следующий пример:

  • Циклично отправляет один или несколько передаваемых файлов.
  • Использует метод Path.GetTempFileName, чтобы вернуть полный путь к файлу, включая его имя.
  • Сохраняет файлы в локальную файловую систему, используя имя файла, созданное приложением.
  • Возвращает общее число и размер отправленных файлов.
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

Чтобы создать имя файла без пути, используйте Path.GetRandomFileName. В следующем примере путь получен из конфигурации:

foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}

Передаваемый в FileStream путь должен содержать имя файла. Если имя файла не указано, в среде выполнения возникает исключение UnauthorizedAccessException.

Файлы, передаваемые с помощью интерфейса IFormFile, буферизуются в памяти или на диске на сервере перед обработкой. Внутри метода действия содержимое IFormFile доступно в виде Stream. Помимо локальной файловой системы, файлы можно сохранять в сетевой папке или в службе хранилища файлов, например в хранилище BLOB-объектов Azure.

Другой пример, который циклирует по нескольким файлам для отправки и использует безопасные имена файлов, см Pages/BufferedMultipleFileUploadPhysical.cshtml.cs . в примере приложения.

Предупреждение

Метод Path.GetTempFileName вызывает исключение IOException в случае создания более чем 65 535 файлов без удаления предыдущих временных файлов. Ограничение в 65 535 файлов предусмотрено для каждого сервера. Дополнительные сведения об этом ограничении в ОС Windows см. в примечаниях в следующих разделах:

Передача небольших файлов с привязкой буферизованной модели к базе данных

Для сохранения данных двоичных файлов в базе данных с помощью Entity Framework определите для сущности свойство массива Byte:

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

Укажите свойство модели страницы для класса, который содержит IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Примечание.

IFormFile можно использовать непосредственно как параметр метода действия или свойство модели привязки. В предыдущем примере используется свойство модели привязки.

Используется FileUpload в Razor форме Pages:

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

При публикации формы на сервере скопируйте IFormFile в поток и сохраните его в базе данных в виде массива байтов. В следующем примере _dbContext сохраняет контекст базы данных приложения:

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}

Предыдущий пример похож на сценарий, продемонстрированный в примере приложения:

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

Предупреждение

При сохранении двоичных данных в реляционных базах данных следует соблюдать осторожность, так как это может отрицательно сказаться на производительности.

Свойство FileName параметра IFormFile требует обязательной проверки. Свойство FileName следует использовать только в целях вывода и только после HTML-кодирования.

В приведенных выше примерах не учитываются вопросы безопасности. Дополнительные сведения приведены в следующих разделах и в примере приложения.

Передача больших файлов с помощью потоковой передачи

В приведенном ниже примере демонстрируется использование JavaScript для потоковой передачи файла в действие контроллера. Токен против подделки файла создается с помощью пользовательского атрибута фильтра и передается в заголовках HTTP клиента, а не в теле запроса. Так как метод действия обрабатывает передаваемые данные напрямую, привязка модели формы отключается другим пользовательским фильтром. Внутри действия содержимое формы считывается с помощью объекта MultipartReader, который считывает каждый объект MultipartSection по отдельности, обрабатывая файл или сохраняя содержимое. После считывания составных разделов действие выполняет собственную привязку модели.

Начальный ответ страницы загружает форму и сохраняет маркер антифоргерии в виде cookie (через GenerateAntiforgeryTokenCookieAttribute атрибут). Этот атрибут использует встроенную антифоргерскую поддержку Core ASP.NET для установки cookie маркера запроса:

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

Для отключения привязки модели используется DisableFormValueModelBindingAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

В примере приложения GenerateAntiforgeryTokenCookieAttribute и применяются в качестве фильтров к моделям /StreamedSingleFileUploadDb приложений страницы и Startup.ConfigureServices/StreamedSingleFileUploadPhysical в использованииRazorсоглашенийDisableFormValueModelBindingAttribute 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());
            });
});

Так как привязка модели не считывает форму, параметры, привязанные из формы, не привязываются (запросы, маршруты и заголовки продолжают работать). Метод действия работает напрямую со свойством Request. Для считывания каждого раздела служит объект MultipartReader. Данные "ключ — значение" хранятся в KeyValueAccumulator. После считывания составных разделов содержимое KeyValueAccumulator используется для привязки данных формы к типу модели.

Полный StreamingController.UploadDatabase метод потоковой передачи в базу данных с помощью EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

Полный метод StreamingController.UploadPhysical для потоковой передачи в физическое расположение:

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

        return BadRequest(ModelState);
    }

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

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

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

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

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

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

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

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

В примере приложения проверки обрабатываются с помощью FileHelpers.ProcessStreamedFile.

Проверка

В классе FileHelpers примера приложения демонстрируется несколько проверок буферизованного файла IFormFile и потоковой передачи файлов. Сведения об обработке IFormFile отправки буферизованного файла в примере приложения см ProcessFormFile . в методе Utilities/FileHelpers.cs в файле. Сведения об обработке потоковых файлов см. в описании метода ProcessStreamedFile в том же файле.

Предупреждение

Методы обработки проверки, показанные в примере приложения, не проверяют содержимое отправленных файлов. В большинстве рабочих сценариев в файле применяется API сканирования на наличие вирусов и вредоносных программ, прежде чем сделать файл доступным для пользователей или других систем.

Хотя пример в разделе содержит рабочий пример методов проверки, не следует реализовывать класс FileHelpers в рабочем приложении, кроме таких случаев:

  • Вы полностью разбираетесь в реализации.
  • Вы изменяете реализацию соответствующим образом для среды и спецификаций приложения.

Никогда не реализуйте код безопасности в приложении, не выполнив эти требования.

Проверка содержимого

Используйте сторонний API сканирования на наличие вирусов и вредоносных программ для отправленного содержимого.

Сканирование файлов требует использования ресурсов сервера в сценариях с большими объемами данных. Если производительность обработки запросов снижается из-за сканирования файлов, рассмотрите возможность разгрузки сканирования путем использования фоновой службы, возможно, службы, которая работает на сервере, отличном от сервера приложения. Как правило, передаваемые файлы хранятся в карантинной области до тех пор, пока фоновый сканер не проверит их на наличие вирусов. При передаче файл перемещается в нормальное место хранения файлов. Эти действия обычно выполняются вместе с записью базы данных, которая указывает состояние сканирования файла. Благодаря такому подходу приложение и сервер приложений остаются в режиме реагирования на запросы.

Проверка расширения файла

Расширение переданного файла должно проверяться в соответствии со списком разрешенных расширений. Например:

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

Проверка подписи файла

Подпись файла определяется по первым нескольким байтам в начале файла. Эти байты можно использовать, чтобы указать, совпадает ли расширение с содержимым файла. Пример приложения проверяет подписи файлов на соответствие нескольким распространенным типам файлов. В следующем примере проверяется подпись файла для изображения JPEG:

private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}

Чтобы получить дополнительные подписи файлов, используйте базу данных подписей файлов (результат поиска Google) и официальные спецификации файлов. Консультирование официальных спецификаций файлов может гарантировать, что выбранные подписи действительны.

Безопасность имени файла

Никогда не используйте для хранения файла в физическом хранилище имя, предоставляемое клиентом. Создайте надежное имя файла с помощью Path.GetRandomFileName или Path.GetTempFileName, чтобы получить полный путь (включая имя файла) для временного хранилища.

Razor Автоматически HTML кодирует значения свойств для отображения. Ниже приведен безопасный код, который можно использовать.

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

За пределами всегда HtmlEncode содержимого Razorимени файла из запроса пользователя.

Во многих реализациях следует включать проверку существования файла. В противном случае файл перезаписывается файлом с тем же именем. Предоставьте дополнительную логику для соответствия спецификациям приложения.

Проверка размера

Ограничьте размер передаваемых файлов.

В примере приложения размер файла ограничен 2 МБ (указывается в байтах). Ограничение предоставляется с помощью конфигурации из appsettings.json файла:

{
  "FileSizeLimit": 2097152
}

В классы PageModel внедряется FileSizeLimit.

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Если размер файла превышает ограничение, файл отклоняется:

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

Сопоставление значения атрибута имени и имени параметра метода POST

В формах, неRazor относящихся к данным POST или напрямую использующее JavaScript FormData , имя, указанное в элементе формы, или FormData должно соответствовать имени параметра в действии контроллера.

В следующем примере :

  • При использовании элемента <input> атрибуту name присваивается значение battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • При использовании FormData в JavaScript для имени задается значение battlePlans:

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

Используйте соответствующее имя для параметра метода C# (battlePlans):

  • Razor Для метода обработчика страниц Pages с именемUpload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Для метода действия контроллера POST приложения MVC:

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

Конфигурация сервера и приложения

Ограничение длины составного текста

MultipartBodyLengthLimit устанавливает ограничение длины каждого составного текста. Разделы формы, превышающие это ограничение, вызовут InvalidDataException при синтаксическом анализе. Значение по умолчанию — 134 217 728 байт (128 МБ). Настройте ограничение с помощью параметра MultipartBodyLengthLimit в Startup.ConfigureServices:

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

RequestFormLimitsAttribute используется для настройки MultipartBodyLengthLimit для одной страницы или действия.

Razor В приложении Pages примените фильтр с соглашением в Startup.ConfigureServicesследующих статьях:

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

Razor В приложении Pages или приложении MVC примените фильтр к модели страницы или методу действия:

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

Kestrel максимальный размер текста запроса

Для приложений, размещенных по Kestrelумолчанию, максимальный размер текста запроса составляет 30 000 000 байт, что составляет примерно 28,6 МБ. Настройте ограничение с помощью параметра сервера MaxRequestBodySizeKestrel :

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

RequestSizeLimitAttribute используется для настройки MaxRequestBodySize для одной страницы или действия.

Razor В приложении Pages примените фильтр с соглашением в Startup.ConfigureServicesследующих статьях:

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

Razor В приложении страниц или приложении MVC примените фильтр к классу или методу действия обработчика страниц:

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

Его RequestSizeLimitAttribute также можно применить с помощью директивы @attributeRazor :

@attribute [RequestSizeLimitAttribute(52428800)]

Другие Kestrel ограничения

Другие Kestrel ограничения могут применяться для приложений, размещенных в Kestrel:

IIS

Ограничение запроса по умолчанию составляетmaxAllowedContentLength 30 000 000 байт, что составляет примерно 28,6 МБ. Настройте ограничение в web.config файле. В следующем примере ограничение равно 50 МБ (52 428 800 байт):

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

Параметр maxAllowedContentLength применяется только к службам IIS. Дополнительные сведения см. в разделе "Ограничения <requestLimits>запросов".

Увеличьте максимальный размер текста запроса ДЛЯ HTTP-запроса, задав IISServerOptions.MaxRequestBodySize в Startup.ConfigureServicesполе . В следующем примере ограничение равно 50 МБ (52 428 800 байт):

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

Дополнительные сведения см. в разделе Размещение ASP.NET Core в Windows со службами IIS.

Устранить неполадки

Ниже описываются некоторые распространенные проблемы, которые возникают при передаче файлов, и возможные способы их решения.

Ошибка "Не найдено" при развертывании на сервере IIS

Следующая ошибка свидетельствует о том, что размер передаваемых файлов превышает настроенное на сервере ограничение длины содержимого:

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

Дополнительные сведения см. в описании IIS.

Сбой подключения

Ошибка подключения и подключение к серверу сброса, вероятно, указывает, что загруженный файл превышает Kestrelмаксимальный размер текста запроса. Дополнительные сведения см. в Kestrel разделе максимального размера текста запроса. Kestrel Ограничения подключения клиента также могут потребовать корректировки.

Исключение, связанное с пустой ссылкой в IFormFile

Если контроллер принимает передаваемые файлы с помощью IFormFile, но значение равно null, проверьте, указан ли в HTML-форме атрибут enctype со значением multipart/form-data. Если этот атрибут не задан для элемента <form>, передача файлов происходить не будет и все связанные аргументы IFormFile будут иметь значение null. Убедитесь также, что именование передачи в данных формы совпадает с именованием приложения.

Поток слишком длинный

В примерах в этом разделе MemoryStream используется для хранения содержимого отправленного файла. Максимальный размер MemoryStream ограничен значением int.MaxValue. Если в сценарии передачи файлов в приложении требуется хранить содержимое, размер которого больше 50 МБ, используйте другой подход, не зависящий от одного только MemoryStream.

Действия ASP.NET Core поддерживают передачу одного или нескольких файлов с помощью привязки модели с буферизацией для небольших файлов или потоковой передачи без буферизации для более крупных файлов.

Просмотреть или скачать образец кода (описание загрузки)

Вопросы безопасности

Необходимо соблюдать осторожность при предоставлении пользователям возможности отправки файлов на сервер. Злоумышленники могут попытаться:

  • Выполнить атаку типа отказ в обслуживании.
  • Передать вирусы и вредоносные программы.
  • Нарушить безопасность сетей и серверов другими способами.

Ниже приведены некоторые действия по обеспечению безопасности, которые снижают вероятность успешных атак.

  • Передавайте файлы в выделенную область для отправки файлов, желательно не на системный диск. Использование выделенного расположения упрощает применение мер безопасности к отправленным файлам. Отключение разрешений на выполнение в папке отправки файла.†
  • Не сохраняйте отправленные файлы в том же дереве каталогов, что и app.†
  • Используйте безопасное имя файла, определяемое приложением. Не используйте имя файла, предоставленное пользователем или ненадежным именем файла отправленного файла.† HTML, кодируйте ненадежное имя файла при отображении. Например, ведение журнала имени файла или отображение в пользовательском интерфейсе (Razor автоматически кодирует выходные данные HTML).
  • Разрешить только утвержденные расширения файлов для спецификации конструктора приложения.†
  • Убедитесь, что клиентские проверка выполняются на сервере.† клиентских проверка легко обойти.
  • Проверьте размер отправленного файла. Задайте максимальное ограничение размера, чтобы предотвратить крупные отправки.†
  • Если файлы не должны перезаписываться переданным файлом с тем же именем, перед отправкой файла проверьте его имя в базе данных или физическом хранилище.
  • Запустите сканер для проверки отправляемого содержимого на наличие вирусов и вредоносных программ, прежде чем сохранять файл.

† Пример приложения демонстрирует подход, соответствующий критериям.

Предупреждение

Отправка в систему вредоносного кода часто является первым шагом перед выполнением кода, который может:

  • полностью получить контроль над системой;
  • перезагрузить систему так, что она окажется в неработоспособном состоянии;
  • скомпрометировать пользовательские или системные данные;
  • применить граффити к открытому интерфейсу.

Сведения об уменьшении контактной зоны атаки во время приема файлов от пользователей см. в следующих ресурсах:

Дополнительные сведения о реализации мер безопасности, включая примеры из примера приложения, см. в статье Передача файлов в ASP.NET Core.

Сценарии использования хранилища

К общим вариантам хранилища файлов относятся следующие:

  • База данных

    • В случае отправки небольших файлов база данных часто работает быстрее, чем физическое хранилище (файловая система или сетевая папка).
    • База данных часто более удобна по сравнению с вариантами физического хранилища, так как получение записи из базы пользовательских данных может одновременно предоставить содержимое файла (например, изображение аватара).
    • Эксплуатация базы данных потенциально дешевле, чем использование службы хранилища данных.
  • Физическое хранилище (файловая система или сетевая папка).

    • Для отправки больших файлов:
      • Ограничения базы данных могут ограничивать размер передачи.
      • Физическое хранилище часто менее экономически выгодно, чем хранилище в базе данных.
    • Эксплуатация физического хранилища потенциально дешевле, чем использование службы хранилища данных.
    • Процесс приложения должен иметь разрешения на чтение и запись для места хранения. Никогда не предоставляйте разрешение на выполнение.
  • Служба хранилища данных (например, хранилище BLOB-объектов Azure).

    • Обычно службы обеспечивают улучшенную масштабируемость и устойчивость по сравнению с локальными решениями, которые обычно подвержены единым точкам отказа.
    • Затраты на использование служб обычно ниже в сценариях с крупномасштабной инфраструктурой хранения.

    Дополнительные сведения см . в кратком руководстве. Использование .NET для создания большого двоичного объекта в хранилище объектов. В этом разделе показан метод UploadFromFileAsync, но метод UploadFromStreamAsync можно использовать для сохранения FileStream в хранилище BLOB-объектов при работе с Stream.

Сценарии передачи файлов

Есть два распространенных подхода к передаче файлов — буферизация и потоковая передача.

Буферизация

Весь файл считывается в IFormFile (представление файла на C#, используемого для обработки или сохранения файла).

Потребление ресурсов (диска, памяти) при передаче файлов зависит от количества и размера одновременно передаваемых файлов. При попытке приложения поместить в буфер слишком много файлов может произойти аварийное завершение работы сайта из-за нехватки памяти или места на диске. Если размер или частота отправки файлов исчерпывают ресурсы приложения, используйте потоковую передачу.

Примечание.

Один буферизованный файл размером свыше 64 КБ перемещается из памяти во временный файл на диске.

Буферизация небольших файлов описана в следующих разделах этой статьи:

Потоковая передача

Файл можно получить с помощью составного запроса. Затем он обрабатывается или сохраняется приложением напрямую. Потоковая передача повышает производительность не значительно. При отправке файлов потоковая передача снижает нагрузку на память или на место на диске.

Потоковая передача больших файлов рассматривается в разделе Передача больших файлов с помощью потоковой передачи.

Передача небольших файлов с привязкой буферизованной модели к физическому хранилищу

Для передачи небольших файлов можно применить составную форму или сформировать запрос POST на языке JavaScript.

В следующем примере показано использование формы Pages для отправки Razor одного файла (Pages/BufferedSingleFileUploadPhysical.cshtml в примере приложения):

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

Следующий пример аналогичен предыдущему примеру, за исключением следующего:

  • JavaScript (Fetch API) используется для отправки данных формы.
  • Проверка не выполняется.
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>

Для выполнения отправки формы в JavaScript для клиентов, которые не поддерживают Fetch API, используйте один из следующих подходов:

  • Используйте функцию Fetch Polyfill (например, window.fetch polyfill (github/fetch)).

  • Используйте XMLHttpRequest. Например:

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

Для поддержки передачи файлов в HTML-формах должен указываться тип кодировки enctype со значением multipart/form-data.

Для элемента ввода files, поддерживающего отправку нескольких файлов, в элементе <input> необходимо указать атрибут multiple:

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

Доступ к отдельным файлам, переданным на сервер, можно получать посредством привязки модели с помощью интерфейса IFormFile. В примере приложения показано несколько отправок буферизованных файлов для баз данных и физических хранилищ.

Предупреждение

Не используйте свойство FileName объекта IFormFile, кроме как для отображения и ведения журнала. При отображении или ведении журнала кодируйте имя файла в формате HTML. Злоумышленник может предоставить имя вредоносного файла, включая полные или относительные пути. Приложения должны:

  • удалить путь из имени файла, указываемого пользователем;
  • сохранить имя файла, закодированное в формате HTML, откуда был удален путь, для пользовательского интерфейса или ведения журнала.
  • создать случайное имя файла для хранения.

Следующий код удаляет путь из имени файла:

string untrustedFileName = Path.GetFileName(pathName);

В приведенных выше примерах не учитываются вопросы безопасности. Дополнительные сведения приведены в следующих разделах и в примере приложения.

При отправке файлов с помощью привязки модели и IFormFile метод действия может принимать следующие файлы:

Примечание.

Привязка сопоставляет файлы форм по имени. Например, значение HTML name в <input type="file" name="formFile"> должно соответствовать привязанному к C# параметру или свойству (FormFile). Дополнительные сведения см. в разделе Сопоставление значения атрибута имени и имени параметра метода POST.

Следующий пример:

  • Циклично отправляет один или несколько передаваемых файлов.
  • Использует метод Path.GetTempFileName, чтобы вернуть полный путь к файлу, включая его имя.
  • Сохраняет файлы в локальную файловую систему, используя имя файла, созданное приложением.
  • Возвращает общее число и размер отправленных файлов.
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}

Чтобы создать имя файла без пути, используйте Path.GetRandomFileName. В следующем примере путь получен из конфигурации:

foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}

Передаваемый в FileStream путь должен содержать имя файла. Если имя файла не указано, в среде выполнения возникает исключение UnauthorizedAccessException.

Файлы, передаваемые с помощью интерфейса IFormFile, буферизуются в памяти или на диске на сервере перед обработкой. Внутри метода действия содержимое IFormFile доступно в виде Stream. Помимо локальной файловой системы, файлы можно сохранять в сетевой папке или в службе хранилища файлов, например в хранилище BLOB-объектов Azure.

Другой пример, который циклирует по нескольким файлам для отправки и использует безопасные имена файлов, см Pages/BufferedMultipleFileUploadPhysical.cshtml.cs . в примере приложения.

Предупреждение

Метод Path.GetTempFileName вызывает исключение IOException в случае создания более чем 65 535 файлов без удаления предыдущих временных файлов. Ограничение в 65 535 файлов предусмотрено для каждого сервера. Дополнительные сведения об этом ограничении в ОС Windows см. в примечаниях в следующих разделах:

Передача небольших файлов с привязкой буферизованной модели к базе данных

Для сохранения данных двоичных файлов в базе данных с помощью Entity Framework определите для сущности свойство массива Byte:

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

Укажите свойство модели страницы для класса, который содержит IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Примечание.

IFormFile можно использовать непосредственно как параметр метода действия или свойство модели привязки. В предыдущем примере используется свойство модели привязки.

Используется FileUpload в Razor форме Pages:

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

При публикации формы на сервере скопируйте IFormFile в поток и сохраните его в базе данных в виде массива байтов. В следующем примере _dbContext сохраняет контекст базы данных приложения:

public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}

Предыдущий пример похож на сценарий, продемонстрированный в примере приложения:

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

Предупреждение

При сохранении двоичных данных в реляционных базах данных следует соблюдать осторожность, так как это может отрицательно сказаться на производительности.

Свойство FileName параметра IFormFile требует обязательной проверки. Свойство FileName следует использовать только в целях вывода и только после HTML-кодирования.

В приведенных выше примерах не учитываются вопросы безопасности. Дополнительные сведения приведены в следующих разделах и в примере приложения.

Передача больших файлов с помощью потоковой передачи

В приведенном ниже примере демонстрируется использование JavaScript для потоковой передачи файла в действие контроллера. Токен против подделки файла создается с помощью пользовательского атрибута фильтра и передается в заголовках HTTP клиента, а не в теле запроса. Так как метод действия обрабатывает передаваемые данные напрямую, привязка модели формы отключается другим пользовательским фильтром. Внутри действия содержимое формы считывается с помощью объекта MultipartReader, который считывает каждый объект MultipartSection по отдельности, обрабатывая файл или сохраняя содержимое. После считывания составных разделов действие выполняет собственную привязку модели.

Начальный ответ страницы загружает форму и сохраняет маркер антифоргерии в виде cookie (через GenerateAntiforgeryTokenCookieAttribute атрибут). Этот атрибут использует встроенную антифоргерскую поддержку Core ASP.NET для установки cookie маркера запроса:

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

Для отключения привязки модели используется DisableFormValueModelBindingAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

В примере приложения GenerateAntiforgeryTokenCookieAttribute и применяются в качестве фильтров к моделям /StreamedSingleFileUploadDb приложений страницы и Startup.ConfigureServices/StreamedSingleFileUploadPhysical в использованииRazorсоглашенийDisableFormValueModelBindingAttribute 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);

Так как привязка модели не считывает форму, параметры, привязанные из формы, не привязываются (запросы, маршруты и заголовки продолжают работать). Метод действия работает напрямую со свойством Request. Для считывания каждого раздела служит объект MultipartReader. Данные "ключ — значение" хранятся в KeyValueAccumulator. После считывания составных разделов содержимое KeyValueAccumulator используется для привязки данных формы к типу модели.

Полный StreamingController.UploadDatabase метод потоковой передачи в базу данных с помощью EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

Полный метод StreamingController.UploadPhysical для потоковой передачи в физическое расположение:

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

        return BadRequest(ModelState);
    }

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

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

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

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

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

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

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

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

В примере приложения проверки обрабатываются с помощью FileHelpers.ProcessStreamedFile.

Проверка

В классе FileHelpers примера приложения демонстрируется несколько проверок буферизованного файла IFormFile и потоковой передачи файлов. Сведения об обработке IFormFile отправки буферизованного файла в примере приложения см ProcessFormFile . в методе Utilities/FileHelpers.cs в файле. Сведения об обработке потоковых файлов см. в описании метода ProcessStreamedFile в том же файле.

Предупреждение

Методы обработки проверки, показанные в примере приложения, не проверяют содержимое отправленных файлов. В большинстве рабочих сценариев в файле применяется API сканирования на наличие вирусов и вредоносных программ, прежде чем сделать файл доступным для пользователей или других систем.

Хотя пример в разделе содержит рабочий пример методов проверки, не следует реализовывать класс FileHelpers в рабочем приложении, кроме таких случаев:

  • Вы полностью разбираетесь в реализации.
  • Вы изменяете реализацию соответствующим образом для среды и спецификаций приложения.

Никогда не реализуйте код безопасности в приложении, не выполнив эти требования.

Проверка содержимого

Используйте сторонний API сканирования на наличие вирусов и вредоносных программ для отправленного содержимого.

Сканирование файлов требует использования ресурсов сервера в сценариях с большими объемами данных. Если производительность обработки запросов снижается из-за сканирования файлов, рассмотрите возможность разгрузки сканирования путем использования фоновой службы, возможно, службы, которая работает на сервере, отличном от сервера приложения. Как правило, передаваемые файлы хранятся в карантинной области до тех пор, пока фоновый сканер не проверит их на наличие вирусов. При передаче файл перемещается в нормальное место хранения файлов. Эти действия обычно выполняются вместе с записью базы данных, которая указывает состояние сканирования файла. Благодаря такому подходу приложение и сервер приложений остаются в режиме реагирования на запросы.

Проверка расширения файла

Расширение переданного файла должно проверяться в соответствии со списком разрешенных расширений. Например:

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}

Проверка подписи файла

Подпись файла определяется по первым нескольким байтам в начале файла. Эти байты можно использовать, чтобы указать, совпадает ли расширение с содержимым файла. Пример приложения проверяет подписи файлов на соответствие нескольким распространенным типам файлов. В следующем примере проверяется подпись файла для изображения JPEG:

private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}

Чтобы получить дополнительные подписи файлов, используйте базу данных подписей файлов (результат поиска Google) и официальные спецификации файлов. Консультирование официальных спецификаций файлов может гарантировать, что выбранные подписи действительны.

Безопасность имени файла

Никогда не используйте для хранения файла в физическом хранилище имя, предоставляемое клиентом. Создайте надежное имя файла с помощью Path.GetRandomFileName или Path.GetTempFileName, чтобы получить полный путь (включая имя файла) для временного хранилища.

Razor Автоматически HTML кодирует значения свойств для отображения. Ниже приведен безопасный код, который можно использовать.

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

За пределами всегда HtmlEncode содержимого Razorимени файла из запроса пользователя.

Во многих реализациях следует включать проверку существования файла. В противном случае файл перезаписывается файлом с тем же именем. Предоставьте дополнительную логику для соответствия спецификациям приложения.

Проверка размера

Ограничьте размер передаваемых файлов.

В примере приложения размер файла ограничен 2 МБ (указывается в байтах). Ограничение предоставляется с помощью конфигурации из appsettings.json файла:

{
  "FileSizeLimit": 2097152
}

В классы PageModel внедряется FileSizeLimit.

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Если размер файла превышает ограничение, файл отклоняется:

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

Сопоставление значения атрибута имени и имени параметра метода POST

В формах, неRazor относящихся к данным POST или напрямую использующее JavaScript FormData , имя, указанное в элементе формы, или FormData должно соответствовать имени параметра в действии контроллера.

В следующем примере :

  • При использовании элемента <input> атрибуту name присваивается значение battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • При использовании FormData в JavaScript для имени задается значение battlePlans:

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

Используйте соответствующее имя для параметра метода C# (battlePlans):

  • Razor Для метода обработчика страниц Pages с именемUpload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Для метода действия контроллера POST приложения MVC:

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

Конфигурация сервера и приложения

Ограничение длины составного текста

MultipartBodyLengthLimit устанавливает ограничение длины каждого составного текста. Разделы формы, превышающие это ограничение, вызовут InvalidDataException при синтаксическом анализе. Значение по умолчанию — 134 217 728 байт (128 МБ). Настройте ограничение с помощью параметра MultipartBodyLengthLimit в Startup.ConfigureServices:

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

RequestFormLimitsAttribute используется для настройки MultipartBodyLengthLimit для одной страницы или действия.

Razor В приложении Pages примените фильтр с соглашением в Startup.ConfigureServicesследующих статьях:

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

Razor В приложении Pages или приложении MVC примените фильтр к модели страницы или методу действия:

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

Kestrel максимальный размер текста запроса

Для приложений, размещенных по Kestrelумолчанию, максимальный размер текста запроса составляет 30 000 000 байт, что составляет примерно 28,6 МБ. Настройте ограничение с помощью параметра сервера MaxRequestBodySizeKestrel :

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

RequestSizeLimitAttribute используется для настройки MaxRequestBodySize для одной страницы или действия.

Razor В приложении Pages примените фильтр с соглашением в Startup.ConfigureServicesследующих статьях:

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

Razor В приложении страниц или приложении MVC примените фильтр к классу или методу действия обработчика страниц:

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

Другие Kestrel ограничения

Другие Kestrel ограничения могут применяться для приложений, размещенных в Kestrel:

IIS

Ограничение запроса по умолчанию составляетmaxAllowedContentLength 30 000 000 байт, что составляет примерно 28,6 МБ. Настройте ограничение в web.config файле. В следующем примере ограничение равно 50 МБ (52 428 800 байт):

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

Параметр maxAllowedContentLength применяется только к службам IIS. Дополнительные сведения см. в разделе "Ограничения <requestLimits>запросов".

Увеличьте максимальный размер текста запроса ДЛЯ HTTP-запроса, задав IISServerOptions.MaxRequestBodySize в Startup.ConfigureServicesполе . В следующем примере ограничение равно 50 МБ (52 428 800 байт):

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

Дополнительные сведения см. в разделе Размещение ASP.NET Core в Windows со службами IIS.

Устранить неполадки

Ниже описываются некоторые распространенные проблемы, которые возникают при передаче файлов, и возможные способы их решения.

Ошибка "Не найдено" при развертывании на сервере IIS

Следующая ошибка свидетельствует о том, что размер передаваемых файлов превышает настроенное на сервере ограничение длины содержимого:

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

Дополнительные сведения см. в описании IIS.

Сбой подключения

Ошибка подключения и подключение к серверу сброса, вероятно, указывает, что загруженный файл превышает Kestrelмаксимальный размер текста запроса. Дополнительные сведения см. в Kestrel разделе максимального размера текста запроса. Kestrel Ограничения подключения клиента также могут потребовать корректировки.

Исключение, связанное с пустой ссылкой в IFormFile

Если контроллер принимает передаваемые файлы с помощью IFormFile, но значение равно null, проверьте, указан ли в HTML-форме атрибут enctype со значением multipart/form-data. Если этот атрибут не задан для элемента <form>, передача файлов происходить не будет и все связанные аргументы IFormFile будут иметь значение null. Убедитесь также, что именование передачи в данных формы совпадает с именованием приложения.

Поток слишком длинный

В примерах в этом разделе MemoryStream используется для хранения содержимого отправленного файла. Максимальный размер MemoryStream ограничен значением int.MaxValue. Если в сценарии передачи файлов в приложении требуется хранить содержимое, размер которого больше 50 МБ, используйте другой подход, не зависящий от одного только MemoryStream.

Дополнительные ресурсы