Przekazywanie plików w ASP.NET Core

Autor : Rutger Storm

ASP.NET Core obsługuje przekazywanie co najmniej jednego pliku przy użyciu buforowanego powiązania modelu dla mniejszych plików i przesyłania strumieniowego bez przesyłania strumieniowego dla większych plików.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Zagadnienia dotyczące bezpieczeństwa

Należy zachować ostrożność podczas udostępniania użytkownikom możliwości przekazywania plików na serwer. Osoby atakujące mogą próbować:

  • Wykonywanie ataków typu "odmowa usługi ".
  • Przekazywanie wirusów lub złośliwego oprogramowania.
  • Naruszenie zabezpieczeń sieci i serwerów na inne sposoby.

Kroki zabezpieczeń, które zmniejszają prawdopodobieństwo pomyślnego ataku, to:

  • Przekazywanie plików do dedykowanego obszaru przekazywania plików, najlepiej do dysku niesystemowego. Dedykowana lokalizacja ułatwia nakładanie ograniczeń zabezpieczeń na przekazane pliki. Wyłącz uprawnienia wykonywania w lokalizacji przekazywania pliku.†
  • Nie utrwalaj przekazanych plików w tym samym drzewie katalogu co app.†
  • Użyj bezpiecznej nazwy pliku określonej przez aplikację. Nie używaj nazwy pliku podanej przez użytkownika ani niezaufanej nazwy pliku przekazanego.† HTML koduje niezaufaną nazwę pliku podczas wyświetlania. Na przykład rejestrowanie nazwy pliku lub wyświetlanie w interfejsie użytkownika (Razor automatycznie koduje dane wyjściowe HTML).
  • Zezwalaj tylko na zatwierdzone rozszerzenia plików dla specyfikacji projektu aplikacji.†
  • Sprawdź, czy kontrole po stronie klienta są wykonywane na serwerze.† Kontrole po stronie klienta są łatwe do obejścia.
  • Sprawdź rozmiar przekazanego pliku. Ustaw maksymalny limit rozmiaru, aby zapobiec dużym przekazywaniem.†
  • Gdy pliki nie powinny być zastępowane przez przekazany plik o tej samej nazwie, sprawdź nazwę pliku względem bazy danych lub magazynu fizycznego przed przekazaniem pliku.
  • Uruchom skaner wirusów/złośliwego oprogramowania na przekazanej zawartości przed zapisaniem pliku.

† Przykładowa aplikacja demonstruje podejście spełniające kryteria.

Ostrzeżenie

Przekazywanie złośliwego kodu do systemu jest często pierwszym krokiem do wykonywania kodu, który może:

  • Całkowicie przejmij kontrolę nad systemem.
  • Przeciążanie systemu z powodu awarii systemu.
  • Naruszenie danych użytkownika lub systemu.
  • Zastosuj graffiti do publicznego interfejsu użytkownika.

Aby uzyskać informacje na temat zmniejszenia obszaru obszaru podatnego na ataki podczas akceptowania plików od użytkowników, zobacz następujące zasoby:

Aby uzyskać więcej informacji na temat implementowania środków zabezpieczeń, w tym przykładów z przykładowej aplikacji, zobacz sekcję Walidacja .

Scenariusze magazynowania

Typowe opcje magazynowania dla plików obejmują:

  • baza danych

    • W przypadku przekazywania małych plików baza danych jest często szybsza niż opcje magazynu fizycznego (systemu plików lub udziału sieciowego).
    • Baza danych jest często wygodniejsza niż opcje magazynu fizycznego, ponieważ pobieranie rekordu bazy danych dla danych użytkownika może współbieżnie dostarczać zawartość pliku (na przykład obraz awatara).
    • Baza danych jest potencjalnie tańsza niż korzystanie z usługi magazynu danych w chmurze.
  • Magazyn fizyczny (system plików lub udział sieciowy)

    • W przypadku przekazywania dużych plików:
      • Limity bazy danych mogą ograniczać rozmiar przekazywania.
      • Magazyn fizyczny jest często mniej ekonomiczny niż magazyn w bazie danych.
    • Magazyn fizyczny jest potencjalnie mniej kosztowny niż korzystanie z usługi magazynu danych w chmurze.
    • Proces aplikacji musi mieć uprawnienia do odczytu i zapisu w lokalizacji magazynu. Nigdy nie udzielaj uprawnień do wykonywania.
  • Usługa magazynu danych w chmurze, na przykład Azure Blob Storage.

    • Usługi zwykle oferują lepszą skalowalność i odporność w przypadku rozwiązań lokalnych, które zwykle podlegają pojedynczym punktom awarii.
    • Usługi są potencjalnie niższe kosztem w scenariuszach infrastruktury magazynowania.

    Aby uzyskać więcej informacji, zobacz Szybki start: tworzenie obiektu blob w magazynie obiektów za pomocą platformy .NET.

Małe i duże pliki

Definicja małych i dużych plików zależy od dostępnych zasobów obliczeniowych. Aplikacje powinny porównać podejście magazynu używane do zapewnienia, że może obsłużyć oczekiwane rozmiary. Przeprowadź testy wydajności pamięci, procesora CPU, dysku i bazy danych.

Chociaż nie można podać określonych granic dla tego, co jest małe i duże dla wdrożenia, poniżej przedstawiono niektóre powiązane wartości domyślne aspNetCore dla formOptions:

  • Domyślnie obiekt HttpRequest.Form nie buforuje całej treści żądania (BufferBody), ale buforuje wszystkie dołączone pliki formularzy wieloczęściowych.
  • MultipartBodyLengthLimit to maksymalny rozmiar buforowanych plików formularzy, domyślnie to 128 MB.
  • MemoryBufferThreshold wskazuje ilość buforowania plików w pamięci przed przejściem do pliku buforu na dysku, domyślnie to 64 KB. MemoryBufferThreshold działa jako granica między małymi i dużymi plikami, które są wywoływane lub obniżane w zależności od zasobów i scenariuszy aplikacji.

Aby uzyskać więcej informacji na temat FormOptionselementu , zobacz kod źródłowy.

Scenariusze przekazywania plików

Dwa ogólne podejścia do przekazywania plików to buforowanie i przesyłanie strumieniowe.

Buforowanie

Cały plik jest odczytywany w pliku IFormFile. IFormFile to reprezentacja pliku w języku C# używana do przetwarzania lub zapisywania pliku.

Dysk i pamięć używana przez przekazywanie plików zależą od liczby i rozmiaru współbieżnych przekazywania plików. Jeśli aplikacja próbuje buforować zbyt wiele przekazywania, witryna ulega awarii, gdy zabraknie pamięci lub miejsca na dysku. Jeśli rozmiar lub częstotliwość przekazywania plików wyczerpały zasoby aplikacji, użyj przesyłania strumieniowego.

Każdy pojedynczy buforowany plik przekraczający 64 KB jest przenoszony z pamięci do pliku tymczasowego na dysku.

Pliki tymczasowe dla większych żądań są zapisywane w lokalizacji o nazwie w zmiennej środowiskowej ASPNETCORE_TEMP . Jeśli ASPNETCORE_TEMP nie zdefiniowano, pliki są zapisywane w folderze tymczasowym bieżącego użytkownika.

Buforowanie małych plików zostało omówione w następujących sekcjach tego tematu:

Przesyłanie strumieniowe

Plik jest odbierany z żądania wieloczęściowego i bezpośrednio przetwarzany lub zapisywany przez aplikację. Przesyłanie strumieniowe nie poprawia wydajności znacząco. Przesyłanie strumieniowe zmniejsza zapotrzebowanie na pamięć lub miejsce na dysku podczas przekazywania plików.

Przesyłanie strumieniowe dużych plików jest omówione w sekcji Przekazywanie dużych plików za pomocą przesyłania strumieniowego .

Przekazywanie małych plików z powiązaniem modelu buforowanego do magazynu fizycznego

Aby przekazać małe pliki, użyj formularza wieloczęściowego lub skonstruuj żądanie POST przy użyciu języka JavaScript.

W poniższym przykładzie pokazano użycie Razor formularza Strony do przekazania pojedynczego pliku (Pages/BufferedSingleFileUploadPhysical.cshtml w przykładowej aplikacji):

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

Poniższy przykład jest analogiczny do poprzedniego przykładu, z tą różnicą, że:

  • Interfejs API pobierania kodu JavaScript służy do przesyłania danych formularza.
  • Nie ma walidacji.
<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>

Aby wykonać formularz POST w języku JavaScript dla klientów, którzy nie obsługują interfejsu API pobierania, użyj jednej z następujących metod:

  • Użyj funkcji Fetch Polyfill (na przykład window.fetch polyfill (github/fetch)).

  • Użyj polecenia XMLHttpRequest. Przykład:

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

Aby obsługiwać przekazywanie plików, formularze HTML muszą określać typ kodowania (enctype) elementu multipart/form-data.

files Aby element wejściowy obsługiwał przekazywanie wielu plików, podaj multiple atrybut elementu<input>:

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

Dostęp do poszczególnych plików przekazanych do serwera można uzyskać za pośrednictwem powiązania modelu przy użyciu polecenia IFormFile. Przykładowa aplikacja demonstruje wiele buforowanych plików przekazywanych dla scenariuszy bazy danych i magazynu fizycznego.

Ostrzeżenie

Nie należy używać FileName właściwości innej niż w przypadku wyświetlania IFormFile i rejestrowania. Podczas wyświetlania lub rejestrowania kod HTML zakoduj nazwę pliku. Osoba atakująca może podać złośliwą nazwę pliku, w tym pełne ścieżki lub ścieżki względne. Aplikacje powinny:

  • Usuń ścieżkę z podanej przez użytkownika nazwy pliku.
  • Zapisz zakodowaną w formacie HTML, usuniętą ścieżkę nazwę pliku dla interfejsu użytkownika lub rejestrowania.
  • Wygeneruj nową losową nazwę pliku dla magazynu.

Poniższy kod usuwa ścieżkę z nazwy pliku:

string untrustedFileName = Path.GetFileName(pathName);

Przedstawione do tej pory przykłady nie uwzględniają zagadnień dotyczących zabezpieczeń. Dodatkowe informacje są udostępniane przez następujące sekcje i przykładową aplikację:

Podczas przekazywania plików przy użyciu powiązania modelu i IFormFilemetody akcji można zaakceptować następujące elementy:

Uwaga

Powiązanie pasuje do plików formularzy według nazwy. Na przykład wartość HTML name w <input type="file" name="formFile"> pliku musi być zgodna z powiązanym parametrem/właściwością języka C#(FormFile). Aby uzyskać więcej informacji, zobacz sekcję Match name attribute value to parameter name of POST method (Dopasowanie wartości atrybutu nazwy do nazwy parametru metody POST ).

Poniższy przykład:

  • Przechodzi w pętli przez co najmniej jeden przekazany plik.
  • Używa path.GetTempFileName , aby zwrócić pełną ścieżkę pliku, w tym nazwę pliku.
  • Zapisuje pliki w lokalnym systemie plików przy użyciu nazwy pliku wygenerowanej przez aplikację.
  • Zwraca łączną liczbę i rozmiar przekazanych plików.
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 });
}

Użyj polecenia Path.GetRandomFileName , aby wygenerować nazwę pliku bez ścieżki. W poniższym przykładzie ścieżka jest uzyskiwana z konfiguracji:

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

Ścieżka przekazana do elementu FileStreammusi zawierać nazwę pliku. Jeśli nie podano nazwy pliku, UnauthorizedAccessException element jest zgłaszany w czasie wykonywania.

Pliki przekazywane przy użyciu IFormFile tej techniki są buforowane w pamięci lub na dysku na serwerze przed przetworzeniem. Wewnątrz metody IFormFile akcji zawartość jest dostępna jako Stream. Oprócz lokalnego systemu plików pliki można zapisywać w udziale sieciowym lub w usłudze magazynu plików, takiej jak Azure Blob Storage.

Inny przykład, który zapętla wiele plików do przekazywania i używa bezpiecznych nazw plików, zobacz Pages/BufferedMultipleFileUploadPhysical.cshtml.cs w przykładowej aplikacji.

Ostrzeżenie

Path.GetTempFileName zgłasza błąd IOException , jeśli utworzono więcej niż 65 535 plików bez usuwania poprzednich plików tymczasowych. Limit 65 535 plików jest limitem na serwer. Aby uzyskać więcej informacji na temat tego limitu w systemie operacyjnym Windows, zobacz uwagi w następujących tematach:

Przekazywanie małych plików z powiązaniem modelu buforowanego do bazy danych

Aby przechowywać dane plików binarnych w bazie danych przy użyciu programu Entity Framework, zdefiniuj Byte właściwość tablicy w jednostce:

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

Określ właściwość modelu strony dla klasy zawierającej element IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Uwaga

IFormFile można używać bezpośrednio jako parametru metody akcji lub jako powiązanej właściwości modelu. W poprzednim przykładzie użyto właściwości powiązanego modelu.

Element FileUpload jest używany w formularzu Razor Strony:

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

Gdy formularz jest poSTed na serwerze, skopiuj IFormFile element do strumienia i zapisz go jako tablicę bajtów w bazie danych. W poniższym przykładzie _dbContext przechowuje kontekst bazy danych aplikacji:

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

Powyższy przykład jest podobny do scenariusza przedstawionego w przykładowej aplikacji:

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

Ostrzeżenie

Należy zachować ostrożność podczas przechowywania danych binarnych w relacyjnych bazach danych, ponieważ może to mieć negatywny wpływ na wydajność.

Nie należy polegać na właściwości ani ufać FileName jej IFormFile bez walidacji. Właściwość FileName powinna być używana tylko do celów wyświetlania i tylko po kodowaniu HTML.

Podane przykłady nie uwzględniają zagadnień dotyczących zabezpieczeń. Dodatkowe informacje są udostępniane przez następujące sekcje i przykładową aplikację:

Przekazywanie dużych plików za pomocą przesyłania strumieniowego

W przykładzie w wersji 3.1 pokazano, jak za pomocą języka JavaScript przesyłać strumieniowo plik do akcji kontrolera. Token antyforgery pliku jest generowany przy użyciu niestandardowego atrybutu filtru i przekazywany do nagłówków HTTP klienta zamiast w treści żądania. Ponieważ metoda akcji przetwarza przekazane dane bezpośrednio, powiązanie modelu formularza jest wyłączone przez inny filtr niestandardowy. W ramach akcji zawartość formularza jest odczytywana przy użyciu MultipartReaderelementu , który odczytuje poszczególne elementy MultipartSection, przetwarzając plik lub przechowując zawartość zgodnie z potrzebami. Po przeczytaniu sekcji wieloczęściowych akcja wykonuje własne powiązanie modelu.

Początkowa odpowiedź strony ładuje formularz i zapisuje token antyforgery w cookie obiekcie (za pośrednictwem atrybutu GenerateAntiforgeryTokenCookieAttribute ). Atrybut używa wbudowanej obsługi antyforgeryjnej ASP.NET Core, aby ustawić cookie element z tokenem żądania:

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

Element DisableFormValueModelBindingAttribute służy do wyłączania powiązania modelu:

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

W przykładowej aplikacji GenerateAntiforgeryTokenCookieAttribute i DisableFormValueModelBindingAttribute są stosowane jako filtry do modeli /StreamedSingleFileUploadDb aplikacji strony i /StreamedSingleFileUploadPhysical w Startup.ConfigureServiceskonwencjiRazor stron:

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

Ponieważ powiązanie modelu nie odczytuje formularza, parametry powiązane z formularzem nie są powiązane (zapytanie, trasa i nagłówek nadal działają). Metoda akcji działa bezpośrednio z właściwością Request . Element MultipartReader służy do odczytywania każdej sekcji. Dane klucza/wartości są przechowywane w obiekcie KeyValueAccumulator. Po odczytaniu sekcji wieloczęściowych KeyValueAccumulator zawartość elementu jest używana do powiązania danych formularza z typem modelu.

Kompletna StreamingController.UploadDatabase metoda przesyłania strumieniowego do bazy danych za pomocą EF Corepolecenia :

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

Pełna StreamingController.UploadPhysical metoda przesyłania strumieniowego do lokalizacji fizycznej:

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

W przykładowej aplikacji sprawdzanie poprawności jest obsługiwane przez usługę FileHelpers.ProcessStreamedFile.

Walidacja

Klasa przykładowej FileHelpers aplikacji demonstruje kilka testów przekazywania plików buforowanych IFormFile i przesyłanych strumieniowo. Aby uzyskać informacje na temat przetwarzania IFormFile przekazywania buforowanych plików w przykładowej aplikacji, zobacz metodę ProcessFormFileUtilities/FileHelpers.cs w pliku . Aby uzyskać informacje o przetwarzaniu plików przesyłanych strumieniowo, zobacz metodę ProcessStreamedFile w tym samym pliku.

Ostrzeżenie

Metody przetwarzania weryfikacji przedstawione w przykładowej aplikacji nie skanują zawartości przekazanych plików. W większości scenariuszy produkcyjnych interfejs API skanera wirusów/złośliwego oprogramowania jest używany w pliku przed udostępnieniem pliku użytkownikom lub innym systemom.

Chociaż przykładowy temat zawiera działający przykład technik weryfikacji, nie implementuj FileHelpers klasy w aplikacji produkcyjnej, chyba że:

  • Pełne zrozumienie implementacji.
  • Zmodyfikuj implementację odpowiednio do środowiska i specyfikacji aplikacji.

Nigdy nieupowszechnie implementuj kod zabezpieczeń w aplikacji bez spełnienia tych wymagań.

Walidacja zawartości

Użyj interfejsu API skanowania wirusów/złośliwego oprogramowania innej firmy w przypadku przekazanej zawartości.

Skanowanie plików wymaga zasobów serwera w scenariuszach o dużej ilości. Jeśli wydajność przetwarzania żądań zostanie zmniejszona z powodu skanowania plików, rozważ odciążenie pracy skanowania do usługi w tle, prawdopodobnie usługi działającej na serwerze innym niż serwer aplikacji. Zazwyczaj przekazane pliki są przechowywane w obszarze poddane kwarantannie, dopóki skaner wirusów w tle ich nie sprawdzi. Po przejściu pliku plik jest przenoszony do normalnej lokalizacji przechowywania plików. Te kroki są zwykle wykonywane w połączeniu z rekordem bazy danych, który wskazuje stan skanowania pliku. Korzystając z takiego podejścia, aplikacja i serwer aplikacji pozostają skoncentrowane na odpowiadaniu na żądania.

Walidacja rozszerzenia pliku

Rozszerzenie przekazanego pliku powinno być sprawdzane pod kątem listy dozwolonych rozszerzeń. Przykład:

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
}

Sprawdzanie poprawności podpisu pliku

Podpis pliku jest określany przez kilka pierwszych bajtów na początku pliku. Te bajty mogą służyć do wskazania, czy rozszerzenie jest zgodne z zawartością pliku. Przykładowa aplikacja sprawdza podpisy plików dla kilku typowych typów plików. W poniższym przykładzie podpis pliku dla obrazu JPEG jest sprawdzany względem pliku:

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

Aby uzyskać dodatkowe podpisy plików, użyj bazy danych podpisów plików (wynik wyszukiwania Google) i oficjalnych specyfikacji plików. Konsultując się z oficjalnymi specyfikacjami plików, można upewnić się, że wybrane podpisy są prawidłowe.

Zabezpieczenia nazw plików

Nigdy nie należy używać nazwy pliku dostarczonego przez klienta do zapisywania pliku w magazynie fizycznym. Utwórz bezpieczną nazwę pliku przy użyciu parametru Path.GetRandomFileName lub Path.GetTempFileName , aby utworzyć pełną ścieżkę (w tym nazwę pliku) dla magazynu tymczasowego.

Razor program automatycznie koduje wartości właściwości dla wyświetlania. Następujący kod jest bezpieczny do użycia:

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

Poza elementem Razornależy zawsze HtmlEncode nazwać pliku z żądania użytkownika.

Wiele implementacji musi zawierać sprawdzenie, czy plik istnieje; w przeciwnym razie plik jest zastępowany przez plik o tej samej nazwie. Podaj dodatkową logikę, aby spełnić specyfikacje aplikacji.

Walidacja rozmiaru

Ogranicz rozmiar przekazanych plików.

W przykładowej aplikacji rozmiar pliku jest ograniczony do 2 MB (wskazane w bajtach). Limit jest dostarczany za pośrednictwem konfiguracji z appsettings.json pliku:

{
  "FileSizeLimit": 2097152
}

Element FileSizeLimit jest wstrzykiwany do PageModel klas:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Gdy rozmiar pliku przekroczy limit, plik zostanie odrzucony:

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

Dopasuj wartość atrybutu nazwy do nazwy parametru metody POST

W formularzach innychRazor niż dane formularza POST lub bezpośrednio używających kodu JavaScript FormData nazwa określona w elememencie formularza lub FormData musi być zgodna z nazwą parametru w akcji kontrolera.

W poniższym przykładzie:

  • W przypadku używania <input> elementu name atrybut jest ustawiony na wartość battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • W przypadku używania FormData w języku JavaScript nazwa jest ustawiona na wartość battlePlans:

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

Użyj pasującej nazwy dla parametru metody C# (battlePlans):

  • Razor Dla metody obsługi stron Pages o nazwie Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Dla metody akcji kontrolera POST MVC:

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

Konfiguracja serwera i aplikacji

Limit długości ciała wieloczęściowego

MultipartBodyLengthLimit określa limit długości każdej treści wieloczęściowej. Sekcje formularzy, które przekraczają ten limit, zgłaszają InvalidDataException błąd podczas analizowania. Wartość domyślna to 134 217 728 (128 MB). Dostosowywanie limitu MultipartBodyLengthLimit przy użyciu ustawienia w programie Startup.ConfigureServices:

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

RequestFormLimitsAttribute służy do ustawiania MultipartBodyLengthLimit elementu dla pojedynczej strony lub akcji.

Razor W aplikacji Pages zastosuj filtr z konwencją w pliku Startup.ConfigureServices:

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

Razor W aplikacji Pages lub aplikacji MVC zastosuj filtr do modelu strony lub metody akcji:

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

Kestrel maksymalny rozmiar treści żądania

W przypadku aplikacji hostowanych przez Kestrelprogram domyślny maksymalny rozmiar treści żądania to 30 000 000 bajtów, czyli około 28,6 MB. Dostosuj limit przy użyciu opcji serwera 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 Służy do ustawiania parametru MaxRequestBodySize dla pojedynczej strony lub akcji.

Razor W aplikacji Pages zastosuj filtr z konwencją w pliku Startup.ConfigureServices:

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

Razor W aplikacji stron lub aplikacji MVC zastosuj filtr do klasy obsługi stron lub metody akcji:

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

Można RequestSizeLimitAttribute go również zastosować przy użyciu @attributeRazor dyrektywy :

@attribute [RequestSizeLimitAttribute(52428800)]

Inne Kestrel limity

Inne Kestrel limity mogą dotyczyć aplikacji hostowanych przez Kestrelusługę :

IIS

Domyślny limit żądań (maxAllowedContentLength) wynosi 30 000 000 bajtów, czyli około 28,6 MB. Dostosuj limit w web.config pliku. W poniższym przykładzie limit jest ustawiony na 50 MB (52 428 800 bajtów):

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

To maxAllowedContentLength ustawienie dotyczy tylko usług IIS. Aby uzyskać więcej informacji, zobacz Limity żądań <requestLimits>.

Rozwiązywanie problemów

Poniżej przedstawiono niektóre typowe problemy występujące podczas pracy z przekazywaniem plików i ich możliwymi rozwiązaniami.

Nie znaleziono błędu podczas wdrażania na serwerze usług IIS

Następujący błąd wskazuje, że przekazany plik przekracza skonfigurowaną długość zawartości serwera:

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

Aby uzyskać więcej informacji, zobacz sekcję IIS .

Błąd połączenia

Błąd połączenia i połączenie serwera resetowania prawdopodobnie wskazuje, że przekazany plik przekracza Kestrelmaksymalny rozmiar treści żądania. Aby uzyskać więcej informacji, zobacz sekcję maksymalnego rozmiaruKestrel treści żądania. Kestrel Limity połączeń klienta mogą również wymagać korekty.

Wyjątek odwołania o wartości null z plikiem IFormFile

Jeśli kontroler akceptuje przekazane pliki przy użyciu wartości IFormFile , ale wartość to null, upewnij się, że formularz HTML określa enctype wartość multipart/form-data. Jeśli ten atrybut nie jest ustawiony na <form> elemecie, przekazywanie pliku nie występuje, a żadne argumenty powiązane IFormFile to null. Upewnij się również, że przekazywanie nazewnictwa w danych formularza jest zgodne z nazewnictwem aplikacji.

Strumień był za długi

Przykłady w tym temacie polegają na MemoryStream przechowywaniu zawartości przekazanego pliku. Limit rozmiaru elementu MemoryStream to int.MaxValue. Jeśli scenariusz przekazywania plików aplikacji wymaga przechowywania zawartości pliku większej niż 50 MB, użyj alternatywnego podejścia, które nie polega na jednym MemoryStream do przechowywania zawartości przekazanego pliku.

ASP.NET Core obsługuje przekazywanie co najmniej jednego pliku przy użyciu buforowanego powiązania modelu dla mniejszych plików i przesyłania strumieniowego bez przesyłania strumieniowego dla większych plików.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Zagadnienia dotyczące bezpieczeństwa

Należy zachować ostrożność podczas udostępniania użytkownikom możliwości przekazywania plików na serwer. Osoby atakujące mogą próbować:

  • Wykonywanie ataków typu "odmowa usługi ".
  • Przekazywanie wirusów lub złośliwego oprogramowania.
  • Naruszenie zabezpieczeń sieci i serwerów na inne sposoby.

Kroki zabezpieczeń, które zmniejszają prawdopodobieństwo pomyślnego ataku, to:

  • Przekazywanie plików do dedykowanego obszaru przekazywania plików, najlepiej do dysku niesystemowego. Dedykowana lokalizacja ułatwia nakładanie ograniczeń zabezpieczeń na przekazane pliki. Wyłącz uprawnienia wykonywania w lokalizacji przekazywania pliku.†
  • Nie utrwalaj przekazanych plików w tym samym drzewie katalogu co app.†
  • Użyj bezpiecznej nazwy pliku określonej przez aplikację. Nie używaj nazwy pliku podanej przez użytkownika ani niezaufanej nazwy pliku przekazanego.† HTML koduje niezaufaną nazwę pliku podczas wyświetlania. Na przykład rejestrowanie nazwy pliku lub wyświetlanie w interfejsie użytkownika (Razor automatycznie koduje dane wyjściowe HTML).
  • Zezwalaj tylko na zatwierdzone rozszerzenia plików dla specyfikacji projektu aplikacji.†
  • Sprawdź, czy kontrole po stronie klienta są wykonywane na serwerze.† Kontrole po stronie klienta są łatwe do obejścia.
  • Sprawdź rozmiar przekazanego pliku. Ustaw maksymalny limit rozmiaru, aby zapobiec dużym przekazywaniem.†
  • Gdy pliki nie powinny być zastępowane przez przekazany plik o tej samej nazwie, sprawdź nazwę pliku względem bazy danych lub magazynu fizycznego przed przekazaniem pliku.
  • Uruchom skaner wirusów/złośliwego oprogramowania na przekazanej zawartości przed zapisaniem pliku.

† Przykładowa aplikacja demonstruje podejście spełniające kryteria.

Ostrzeżenie

Przekazywanie złośliwego kodu do systemu jest często pierwszym krokiem do wykonywania kodu, który może:

  • Całkowicie przejmij kontrolę nad systemem.
  • Przeciążanie systemu z powodu awarii systemu.
  • Naruszenie danych użytkownika lub systemu.
  • Zastosuj graffiti do publicznego interfejsu użytkownika.

Aby uzyskać informacje na temat zmniejszenia obszaru obszaru podatnego na ataki podczas akceptowania plików od użytkowników, zobacz następujące zasoby:

Aby uzyskać więcej informacji na temat implementowania środków zabezpieczeń, w tym przykładów z przykładowej aplikacji, zobacz sekcję Walidacja .

Scenariusze magazynowania

Typowe opcje magazynowania dla plików obejmują:

  • baza danych

    • W przypadku przekazywania małych plików baza danych jest często szybsza niż opcje magazynu fizycznego (systemu plików lub udziału sieciowego).
    • Baza danych jest często wygodniejsza niż opcje magazynu fizycznego, ponieważ pobieranie rekordu bazy danych dla danych użytkownika może współbieżnie dostarczać zawartość pliku (na przykład obraz awatara).
    • Baza danych jest potencjalnie tańsza niż korzystanie z usługi magazynu danych.
  • Magazyn fizyczny (system plików lub udział sieciowy)

    • W przypadku przekazywania dużych plików:
      • Limity bazy danych mogą ograniczać rozmiar przekazywania.
      • Magazyn fizyczny jest często mniej ekonomiczny niż magazyn w bazie danych.
    • Magazyn fizyczny jest potencjalnie mniej kosztowny niż użycie usługi magazynu danych.
    • Proces aplikacji musi mieć uprawnienia do odczytu i zapisu w lokalizacji magazynu. Nigdy nie udzielaj uprawnień do wykonywania.
  • Usługa magazynu danych (na przykład Azure Blob Storage)

    • Usługi zwykle oferują lepszą skalowalność i odporność w przypadku rozwiązań lokalnych, które zwykle podlegają pojedynczym punktom awarii.
    • Usługi są potencjalnie niższe kosztem w scenariuszach infrastruktury magazynowania.

    Aby uzyskać więcej informacji, zobacz Szybki start: tworzenie obiektu blob w magazynie obiektów za pomocą platformy .NET.

Scenariusze przekazywania plików

Dwa ogólne podejścia do przekazywania plików to buforowanie i przesyłanie strumieniowe.

Buforowanie

Cały plik jest odczytywany w pliku IFormFile, który jest reprezentacją pliku w języku C# służącą do przetwarzania lub zapisywania pliku.

Zasoby (dysk, pamięć) używane przez przekazywanie plików zależą od liczby i rozmiaru współbieżnych przekazywania plików. Jeśli aplikacja próbuje buforować zbyt wiele przekazywania, witryna ulega awarii, gdy zabraknie pamięci lub miejsca na dysku. Jeśli rozmiar lub częstotliwość przekazywania plików wyczerpały zasoby aplikacji, użyj przesyłania strumieniowego.

Uwaga

Każdy pojedynczy buforowany plik przekraczający 64 KB jest przenoszony z pamięci do pliku tymczasowego na dysku.

Buforowanie małych plików zostało omówione w następujących sekcjach tego tematu:

Przesyłanie strumieniowe

Plik jest odbierany z żądania wieloczęściowego i bezpośrednio przetwarzany lub zapisywany przez aplikację. Przesyłanie strumieniowe nie poprawia wydajności znacząco. Przesyłanie strumieniowe zmniejsza zapotrzebowanie na pamięć lub miejsce na dysku podczas przekazywania plików.

Przesyłanie strumieniowe dużych plików jest omówione w sekcji Przekazywanie dużych plików za pomocą przesyłania strumieniowego .

Przekazywanie małych plików z powiązaniem modelu buforowanego do magazynu fizycznego

Aby przekazać małe pliki, użyj formularza wieloczęściowego lub skonstruuj żądanie POST przy użyciu języka JavaScript.

W poniższym przykładzie pokazano użycie Razor formularza Strony do przekazania pojedynczego pliku (Pages/BufferedSingleFileUploadPhysical.cshtml w przykładowej aplikacji):

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

Poniższy przykład jest analogiczny do poprzedniego przykładu, z tą różnicą, że:

  • Interfejs API pobierania kodu JavaScript służy do przesyłania danych formularza.
  • Nie ma walidacji.
<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>

Aby wykonać formularz POST w języku JavaScript dla klientów, którzy nie obsługują interfejsu API pobierania, użyj jednej z następujących metod:

  • Użyj funkcji Fetch Polyfill (na przykład window.fetch polyfill (github/fetch)).

  • Użyj polecenia XMLHttpRequest. Przykład:

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

Aby obsługiwać przekazywanie plików, formularze HTML muszą określać typ kodowania (enctype) elementu multipart/form-data.

files Aby element wejściowy obsługiwał przekazywanie wielu plików, podaj multiple atrybut elementu<input>:

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

Dostęp do poszczególnych plików przekazanych do serwera można uzyskać za pośrednictwem powiązania modelu przy użyciu polecenia IFormFile. Przykładowa aplikacja demonstruje wiele buforowanych plików przekazywanych dla scenariuszy bazy danych i magazynu fizycznego.

Ostrzeżenie

Nie należy używać FileName właściwości innej niż w przypadku wyświetlania IFormFile i rejestrowania. Podczas wyświetlania lub rejestrowania kod HTML koduje nazwę pliku. Osoba atakująca może podać złośliwą nazwę pliku, w tym pełne ścieżki lub ścieżki względne. Aplikacje powinny:

  • Usuń ścieżkę z podanej przez użytkownika nazwy pliku.
  • Zapisz zakodowaną w formacie HTML, usuniętą ścieżkę nazwę pliku interfejsu użytkownika lub rejestrowania.
  • Wygeneruj nową losową nazwę pliku dla magazynu.

Poniższy kod usuwa ścieżkę z nazwy pliku:

string untrustedFileName = Path.GetFileName(pathName);

Przykłady przedstawione do tej pory nie uwzględniają zagadnień dotyczących zabezpieczeń. Dodatkowe informacje są dostarczane przez następujące sekcje i przykładową aplikację:

Podczas przekazywania plików przy użyciu powiązania modelu i IFormFilemetody akcji można zaakceptować:

Uwaga

Powiązanie pasuje do plików formularzy według nazwy. Na przykład wartość HTML name w <input type="file" name="formFile"> pliku musi być zgodna z powiązanym parametrem/właściwością języka C# (FormFile). Aby uzyskać więcej informacji, zobacz sekcję Dopasowanie wartości atrybutu nazwy do parametru metody POST .

Poniższy przykład:

  • Pętle przez co najmniej jeden przekazany plik.
  • Używa path.GetTempFileName , aby zwrócić pełną ścieżkę pliku, w tym nazwę pliku.
  • Zapisuje pliki w lokalnym systemie plików przy użyciu nazwy pliku wygenerowanej przez aplikację.
  • Zwraca łączną liczbę i rozmiar przekazanych plików.
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 });
}

Użyj polecenia Path.GetRandomFileName , aby wygenerować nazwę pliku bez ścieżki. W poniższym przykładzie ścieżka jest uzyskiwana z konfiguracji:

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

Ścieżka przekazana do FileStream elementu musi zawierać nazwę pliku. Jeśli nazwa pliku nie zostanie podana, UnauthorizedAccessException zostanie zgłoszony w czasie wykonywania.

Pliki przekazywane przy użyciu IFormFile techniki są buforowane w pamięci lub na dysku na serwerze przed przetworzeniem. Wewnątrz metody IFormFile akcji zawartość jest dostępna jako Stream. Oprócz lokalnego systemu plików pliki można zapisywać w udziale sieciowym lub w usłudze magazynu plików, takiej jak Azure Blob Storage.

Innym przykładem, który zapętla wiele plików do przekazywania i używa bezpiecznych nazw plików, zobacz Pages/BufferedMultipleFileUploadPhysical.cshtml.cs w przykładowej aplikacji.

Ostrzeżenie

Path.GetTempFileName zgłasza błąd IOException , jeśli utworzono więcej niż 65 535 plików bez usuwania poprzednich plików tymczasowych. Limit 65 535 plików to limit na serwer. Aby uzyskać więcej informacji na temat tego limitu w systemie operacyjnym Windows, zobacz uwagi w następujących tematach:

Przekazywanie małych plików z powiązaniem modelu buforowanego do bazy danych

Aby przechowywać dane plików binarnych w bazie danych przy użyciu programu Entity Framework, zdefiniuj Byte właściwość tablicy w jednostce:

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

Określ właściwość modelu strony dla klasy, która zawiera element IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Uwaga

IFormFile można użyć bezpośrednio jako parametru metody akcji lub jako właściwości powiązanego modelu. W poprzednim przykładzie użyto właściwości powiązanego modelu.

Element FileUpload jest używany w formularzu Razor Strony:

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

Gdy formularz jest poSTed na serwerze, skopiuj element IFormFile do strumienia i zapisz go jako tablicę bajtów w bazie danych. W poniższym przykładzie _dbContext przechowuje kontekst bazy danych aplikacji:

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

Powyższy przykład jest podobny do scenariusza przedstawionego w przykładowej aplikacji:

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

Ostrzeżenie

Należy zachować ostrożność podczas przechowywania danych binarnych w relacyjnych bazach danych, ponieważ może to mieć negatywny wpływ na wydajność.

Nie polegaj na właściwościach IFormFile bez walidacji ani nie ufaj FileName jej. Właściwość FileName powinna być używana tylko do celów wyświetlania i tylko po kodowaniu HTML.

Podane przykłady nie uwzględniają zagadnień dotyczących zabezpieczeń. Dodatkowe informacje są udostępniane przez następujące sekcje i przykładową aplikację:

Przekazywanie dużych plików za pomocą przesyłania strumieniowego

W poniższym przykładzie pokazano, jak za pomocą języka JavaScript przesyłać strumieniowo plik do akcji kontrolera. Token antyforgery pliku jest generowany przy użyciu niestandardowego atrybutu filtru i przekazywany do nagłówków HTTP klienta zamiast w treści żądania. Ponieważ metoda akcji przetwarza przekazane dane bezpośrednio, powiązanie modelu formularza jest wyłączone przez inny filtr niestandardowy. W ramach akcji zawartość formularza jest odczytywana przy użyciu MultipartReaderelementu , który odczytuje poszczególne elementy MultipartSection, przetwarzając plik lub przechowując zawartość zgodnie z potrzebami. Po przeczytaniu sekcji wieloczęściowych akcja wykonuje własne powiązanie modelu.

Początkowa odpowiedź strony ładuje formularz i zapisuje token antyforgery w cookie obiekcie (za pośrednictwem atrybutu GenerateAntiforgeryTokenCookieAttribute ). Atrybut używa wbudowanej obsługi antyforgeryjnej ASP.NET Core, aby ustawić cookie element z tokenem żądania:

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

Element DisableFormValueModelBindingAttribute służy do wyłączania powiązania modelu:

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

W przykładowej aplikacji GenerateAntiforgeryTokenCookieAttribute i DisableFormValueModelBindingAttribute są stosowane jako filtry do modeli /StreamedSingleFileUploadDb aplikacji strony i /StreamedSingleFileUploadPhysical w Startup.ConfigureServiceskonwencjiRazor stron:

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

Ponieważ powiązanie modelu nie odczytuje formularza, parametry powiązane z formularzem nie są powiązane (zapytanie, trasa i nagłówek nadal działają). Metoda akcji działa bezpośrednio z właściwością Request . Element MultipartReader służy do odczytywania każdej sekcji. Dane klucza/wartości są przechowywane w obiekcie KeyValueAccumulator. Po odczytaniu sekcji wieloczęściowych KeyValueAccumulator zawartość elementu jest używana do powiązania danych formularza z typem modelu.

Kompletna StreamingController.UploadDatabase metoda przesyłania strumieniowego do bazy danych za pomocą EF Corepolecenia :

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

Pełna StreamingController.UploadPhysical metoda przesyłania strumieniowego do lokalizacji fizycznej:

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

W przykładowej aplikacji sprawdzanie poprawności jest obsługiwane przez usługę FileHelpers.ProcessStreamedFile.

Walidacja

Klasa przykładowej FileHelpers aplikacji demonstruje kilka testów przekazywania plików buforowanych IFormFile i przesyłanych strumieniowo. Aby uzyskać informacje na temat przetwarzania IFormFile przekazywania buforowanych plików w przykładowej aplikacji, zobacz metodę ProcessFormFileUtilities/FileHelpers.cs w pliku . Aby uzyskać informacje o przetwarzaniu plików przesyłanych strumieniowo, zobacz metodę ProcessStreamedFile w tym samym pliku.

Ostrzeżenie

Metody przetwarzania weryfikacji przedstawione w przykładowej aplikacji nie skanują zawartości przekazanych plików. W większości scenariuszy produkcyjnych interfejs API skanera wirusów/złośliwego oprogramowania jest używany w pliku przed udostępnieniem pliku użytkownikom lub innym systemom.

Chociaż przykładowy temat zawiera działający przykład technik weryfikacji, nie implementuj FileHelpers klasy w aplikacji produkcyjnej, chyba że:

  • Pełne zrozumienie implementacji.
  • Zmodyfikuj implementację odpowiednio do środowiska i specyfikacji aplikacji.

Nigdy nieupowszechnie implementuj kod zabezpieczeń w aplikacji bez spełnienia tych wymagań.

Walidacja zawartości

Użyj interfejsu API skanowania wirusów/złośliwego oprogramowania innej firmy w przypadku przekazanej zawartości.

Skanowanie plików wymaga zasobów serwera w scenariuszach o dużej ilości. Jeśli wydajność przetwarzania żądań zostanie zmniejszona z powodu skanowania plików, rozważ odciążenie pracy skanowania do usługi w tle, prawdopodobnie usługi działającej na serwerze innym niż serwer aplikacji. Zazwyczaj przekazane pliki są przechowywane w obszarze poddane kwarantannie, dopóki skaner wirusów w tle ich nie sprawdzi. Po przejściu pliku plik jest przenoszony do normalnej lokalizacji przechowywania plików. Te kroki są zwykle wykonywane w połączeniu z rekordem bazy danych, który wskazuje stan skanowania pliku. Korzystając z takiego podejścia, aplikacja i serwer aplikacji pozostają skoncentrowane na odpowiadaniu na żądania.

Walidacja rozszerzenia pliku

Rozszerzenie przekazanego pliku powinno być sprawdzane pod kątem listy dozwolonych rozszerzeń. Przykład:

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
}

Sprawdzanie poprawności podpisu pliku

Podpis pliku jest określany przez kilka pierwszych bajtów na początku pliku. Te bajty mogą służyć do wskazania, czy rozszerzenie jest zgodne z zawartością pliku. Przykładowa aplikacja sprawdza podpisy plików dla kilku typowych typów plików. W poniższym przykładzie podpis pliku dla obrazu JPEG jest sprawdzany względem pliku:

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

Aby uzyskać dodatkowe podpisy plików, użyj bazy danych podpisów plików (wynik wyszukiwania Google) i oficjalnych specyfikacji plików. Konsultując oficjalne specyfikacje plików, można zagwarantować, że wybrane podpisy są prawidłowe.

Zabezpieczenia nazw plików

Nigdy nie używaj nazwy pliku dostarczonego przez klienta do zapisywania pliku w magazynie fizycznym. Utwórz bezpieczną nazwę pliku przy użyciu path.GetRandomFileName lub Path.GetTempFileName , aby utworzyć pełną ścieżkę (w tym nazwę pliku) dla magazynu tymczasowego.

Razor automatycznie koduje wartości właściwości html do wyświetlania. Poniższy kod jest bezpieczny do użycia:

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

RazorPoza programem zawsze HtmlEncode należy nazwać zawartość pliku z żądania użytkownika.

Wiele implementacji musi zawierać sprawdzenie, czy plik istnieje; w przeciwnym razie plik jest zastępowany przez plik o tej samej nazwie. Podaj dodatkową logikę, aby spełnić specyfikacje aplikacji.

Walidacja rozmiaru

Ogranicz rozmiar przekazanych plików.

W przykładowej aplikacji rozmiar pliku jest ograniczony do 2 MB (wskazane w bajtach). Limit jest dostarczany za pośrednictwem konfiguracji z appsettings.json pliku :

{
  "FileSizeLimit": 2097152
}

Element FileSizeLimit jest wstrzykiwany do PageModel klas:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Gdy rozmiar pliku przekracza limit, plik zostanie odrzucony:

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

Dopasuj wartość atrybutu nazwy do nazwy parametru metody POST

W formularzach innych niżRazor dane post lub bezpośrednio używających FormData języka JavaScript nazwa określona w elememencie formularza lub FormData musi być zgodna z nazwą parametru w akcji kontrolera.

W poniższym przykładzie:

  • W przypadku korzystania z <input> elementu name atrybut jest ustawiony na wartość battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • W przypadku używania FormData w języku JavaScript nazwa jest ustawiona na wartość battlePlans:

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

Użyj pasującej nazwy parametru metody C# (battlePlans):

  • Razor W przypadku metody obsługi strony Pages o nazwie Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Dla metody akcji kontrolera MVC POST:

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

Konfiguracja serwera i aplikacji

Limit długości treści wieloczęściowej

MultipartBodyLengthLimit określa limit długości każdej treści wieloczęściowej. Sekcje formularza, które przekraczają ten limit, zgłaszają wyjątek InvalidDataException podczas analizowania. Wartość domyślna to 134 217 728 (128 MB). Dostosuj limit przy użyciu ustawienia w pliku MultipartBodyLengthLimitStartup.ConfigureServices:

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

RequestFormLimitsAttribute służy do ustawiania MultipartBodyLengthLimit elementu dla jednej strony lub akcji.

Razor W aplikacji Pages zastosuj filtr z konwencją w pliku Startup.ConfigureServices:

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

Razor W aplikacji Pages lub aplikacji MVC zastosuj filtr do modelu strony lub metody akcji:

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

Kestrel maksymalny rozmiar treści żądania

W przypadku aplikacji hostowanych przez Kestrelprogram domyślny maksymalny rozmiar treści żądania to 30 000 000 bajtów, czyli około 28,6 MB. Dostosuj limit przy użyciu opcji serwera 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 Służy do ustawiania parametru MaxRequestBodySize dla jednej strony lub akcji.

Razor W aplikacji Pages zastosuj filtr z konwencją w pliku Startup.ConfigureServices:

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

Razor W aplikacji stron lub aplikacji MVC zastosuj filtr do klasy obsługi stron lub metody akcji:

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

Można RequestSizeLimitAttribute go również zastosować przy użyciu @attributeRazor dyrektywy :

@attribute [RequestSizeLimitAttribute(52428800)]

Inne Kestrel limity

Inne Kestrel limity mogą dotyczyć aplikacji hostowanych przez Kestrelprogram :

IIS

Domyślny limit żądań (maxAllowedContentLength) wynosi 30 000 000 bajtów, czyli około 28,6 MB. Dostosuj limit w web.config pliku. W poniższym przykładzie limit jest ustawiony na 50 MB (52 428 800 bajtów):

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

Ustawienie maxAllowedContentLength dotyczy tylko usług IIS. Aby uzyskać więcej informacji, zobacz Request Limits (Limity żądań <requestLimits>).

Zwiększ maksymalny rozmiar treści żądania dla żądania HTTP, ustawiając wartość w pliku IISServerOptions.MaxRequestBodySizeStartup.ConfigureServices. W poniższym przykładzie limit jest ustawiony na 50 MB (52 428 800 bajtów):

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

Aby uzyskać więcej informacji, zobacz Host ASP.NET Core w systemie Windows przy użyciu usług IIS.

Rozwiązywanie problemów

Poniżej przedstawiono niektóre typowe problemy występujące podczas pracy z przekazywaniem plików i ich możliwymi rozwiązaniami.

Nie znaleziono błędu podczas wdrażania na serwerze usług IIS

Następujący błąd wskazuje, że przekazany plik przekracza skonfigurowaną długość zawartości serwera:

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

Aby uzyskać więcej informacji, zobacz sekcję IIS .

Błąd połączenia

Błąd połączenia i połączenie serwera resetowania prawdopodobnie wskazuje, że przekazany plik przekracza Kestrelmaksymalny rozmiar treści żądania. Aby uzyskać więcej informacji, zobacz sekcję Kestrel maksymalny rozmiar treści żądania . Kestrel Limity połączeń klienta mogą również wymagać korekty.

Wyjątek odwołania o wartości null z IFormFile

Jeśli kontroler akceptuje przekazane pliki przy użyciu wartości IFormFile , ale wartość to null, upewnij się, że formularz HTML określa enctype wartość multipart/form-data. Jeśli ten atrybut nie jest ustawiony na elememencie <form> , przekazywanie pliku nie występuje, a argumenty powiązane IFormFile to null. Upewnij się również, że przekazywanie nazewnictwa w danych formularza jest zgodne z nazewnictwem aplikacji.

Strumień był za długi

Przykłady w tym temacie polegają na MemoryStream przechowywaniu zawartości przekazanego pliku. Limit rozmiaru obiektu MemoryStream to int.MaxValue. Jeśli scenariusz przekazywania plików aplikacji wymaga przechowywania zawartości pliku większej niż 50 MB, użyj alternatywnego podejścia, które nie polega na jednym MemoryStream do przechowywania zawartości przekazanego pliku.

ASP.NET Core obsługuje przekazywanie co najmniej jednego pliku przy użyciu powiązania modelu buforowanego dla mniejszych plików i niebuforowanego przesyłania strumieniowego dla większych plików.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Zagadnienia dotyczące bezpieczeństwa

Należy zachować ostrożność podczas udostępniania użytkownikom możliwości przekazywania plików na serwer. Osoby atakujące mogą próbować:

  • Wykonywanie ataków typu "odmowa usługi ".
  • Przekazywanie wirusów lub złośliwego oprogramowania.
  • Naruszenie zabezpieczeń sieci i serwerów na inne sposoby.

Kroki zabezpieczeń, które zmniejszają prawdopodobieństwo pomyślnego ataku:

  • Przekaż pliki do dedykowanego obszaru przekazywania plików, najlepiej do dysku niesystemowego. Dedykowana lokalizacja ułatwia nakładanie ograniczeń zabezpieczeń na przekazane pliki. Wyłącz uprawnienia do wykonywania w lokalizacji przekazywania pliku.†
  • Nie utrwalaj przekazanych plików w tym samym drzewie katalogów co app.†
  • Użyj bezpiecznej nazwy pliku określonej przez aplikację. Nie używaj nazwy pliku podanej przez użytkownika ani niezaufanej nazwy pliku przekazanego.† HTML zakoduj niezaufaną nazwę pliku podczas jego wyświetlania. Na przykład rejestrowanie nazwy pliku lub wyświetlanie w interfejsie użytkownika (Razor automatycznie koduje dane wyjściowe HTML).
  • Zezwalaj tylko na zatwierdzone rozszerzenia plików dla specyfikacji projektu aplikacji.†
  • Sprawdź, czy kontrole po stronie klienta są wykonywane na serwerze.† Kontrole po stronie klienta można łatwo obejść.
  • Sprawdź rozmiar przekazanego pliku. Ustaw maksymalny limit rozmiaru, aby zapobiec dużym przekazywaniem.†
  • Jeśli pliki nie powinny być zastępowane przez przekazany plik o tej samej nazwie, przed przekazaniem pliku sprawdź nazwę pliku względem bazy danych lub magazynu fizycznego.
  • Uruchom skaner wirusów/złośliwego oprogramowania na przekazanej zawartości przed zapisaniem pliku.

† Przykładowa aplikacja demonstruje podejście spełniające kryteria.

Ostrzeżenie

Przekazywanie złośliwego kodu do systemu jest często pierwszym krokiem do wykonywania kodu, który może:

  • Całkowicie przejąć kontrolę nad systemem.
  • Przeciążanie systemu wynikiem awarii systemu.
  • Naruszenie bezpieczeństwa danych użytkownika lub systemu.
  • Stosowanie narzędzia graffiti do publicznego interfejsu użytkownika.

Aby uzyskać informacje na temat zmniejszania obszaru obszaru podatnego na ataki podczas akceptowania plików od użytkowników, zobacz następujące zasoby:

Aby uzyskać więcej informacji na temat implementowania środków zabezpieczeń, w tym przykładów z przykładowej aplikacji, zobacz sekcję Walidacja .

Scenariusze magazynu

Typowe opcje magazynowania dla plików obejmują:

  • baza danych

    • W przypadku przekazywania małych plików baza danych jest często szybsza niż opcje magazynu fizycznego (system plików lub udział sieciowy).
    • Baza danych jest często wygodniejsza niż opcje magazynu fizycznego, ponieważ pobieranie rekordu bazy danych dla danych użytkownika może współbieżnie dostarczać zawartość pliku (na przykład obraz awatara).
    • Baza danych jest potencjalnie tańsza niż korzystanie z usługi magazynu danych.
  • Magazyn fizyczny (system plików lub udział sieciowy)

    • W przypadku przekazywania dużych plików:
      • Limity bazy danych mogą ograniczać rozmiar przekazywania.
      • Magazyn fizyczny jest często mniej ekonomiczny niż magazyn w bazie danych.
    • Magazyn fizyczny jest potencjalnie mniej kosztowny niż korzystanie z usługi magazynu danych.
    • Proces aplikacji musi mieć uprawnienia do odczytu i zapisu w lokalizacji magazynu. Nigdy nie udzielaj uprawnień do wykonywania.
  • Usługa magazynu danych (na przykład Azure Blob Storage)

    • Usługi zwykle oferują lepszą skalowalność i odporność w przypadku rozwiązań lokalnych, które zwykle podlegają pojedynczym punktom awarii.
    • Usługi są potencjalnie niższe kosztem w scenariuszach z dużą infrastrukturą magazynu.

    Aby uzyskać więcej informacji, zobacz Szybki start: tworzenie obiektu blob w magazynie obiektów za pomocą platformy .NET. W tym temacie przedstawiono metodę UploadFromFileAsyncFileStream , ale UploadFromStreamAsync można jej użyć do zapisania obiektu w magazynie obiektów blob podczas pracy z usługą Stream.

Scenariusze przekazywania plików

Dwa ogólne podejścia do przekazywania plików to buforowanie i przesyłanie strumieniowe.

Buforowanie

Cały plik jest odczytywany w IFormFileobiekcie , który jest reprezentacją pliku w języku C# służącą do przetwarzania lub zapisywania pliku.

Zasoby (dysk, pamięć) używane przez przekazywanie plików zależą od liczby i rozmiaru współbieżnych operacji przekazywania plików. Jeśli aplikacja próbuje buforować zbyt wiele operacji przekazywania, witryna ulega awarii, gdy zabraknie pamięci lub miejsca na dysku. Jeśli rozmiar lub częstotliwość przekazywania plików wyczerpują zasoby aplikacji, użyj przesyłania strumieniowego.

Uwaga

Każdy pojedynczy plik buforowany przekraczający 64 KB jest przenoszony z pamięci do pliku tymczasowego na dysku.

Buforowanie małych plików opisano w poniższych sekcjach tego tematu:

Przesyłanie strumieniowe

Plik jest odbierany z żądania wieloczęściowego i bezpośrednio przetwarzany lub zapisywany przez aplikację. Przesyłanie strumieniowe nie poprawia wydajności znacząco. Przesyłanie strumieniowe zmniejsza zapotrzebowanie na pamięć lub miejsce na dysku podczas przekazywania plików.

Przesyłanie strumieniowe dużych plików zostało omówione w sekcji Przekazywanie dużych plików za pomocą przesyłania strumieniowego .

Przekazywanie małych plików z powiązaniem modelu buforowanego z magazynem fizycznym

Aby przekazać małe pliki, użyj formularza wieloczęściowego lub skonstruuj żądanie POST przy użyciu języka JavaScript.

W poniższym przykładzie pokazano użycie Razor formularza Strony do przekazania pojedynczego pliku (Pages/BufferedSingleFileUploadPhysical.cshtml w przykładowej aplikacji):

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

Poniższy przykład jest analogiczny do poprzedniego przykładu, z tą różnicą, że:

  • Interfejs API pobierania (Fetch API) języka JavaScript służy do przesyłania danych formularza.
  • Nie ma walidacji.
<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>

Aby wykonać formularz POST w języku JavaScript dla klientów, którzy nie obsługują interfejsu API pobierania, użyj jednej z następujących metod:

  • Użyj funkcji Polyfill pobierania (na przykład window.fetch polyfill (github/fetch)).

  • Użyj polecenia XMLHttpRequest. Przykład:

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

Aby obsługiwać przekazywanie plików, formularze HTML muszą określać typ kodowania (enctype) elementu multipart/form-data.

files Aby element wejściowy obsługiwał przekazywanie wielu plików, podaj multiple atrybut w elemecie <input> :

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

Dostęp do poszczególnych plików przekazanych do serwera można uzyskać za pośrednictwem powiązania modelu przy użyciu polecenia IFormFile. Przykładowa aplikacja demonstruje wiele buforowanych przekazywania plików dla scenariuszy bazy danych i magazynu fizycznego.

Ostrzeżenie

Nie należy używać FileName właściwości innej IFormFile niż w przypadku wyświetlania i rejestrowania. Podczas wyświetlania lub rejestrowania kod HTML zakoduj nazwę pliku. Osoba atakująca może podać złośliwą nazwę pliku, w tym pełne ścieżki lub ścieżki względne. Aplikacje powinny:

  • Usuń ścieżkę z podanej przez użytkownika nazwy pliku.
  • Zapisz zakodowaną w formacie HTML, usuniętą ścieżkę nazwę pliku dla interfejsu użytkownika lub rejestrowania.
  • Wygeneruj nową losową nazwę pliku dla magazynu.

Poniższy kod usuwa ścieżkę z nazwy pliku:

string untrustedFileName = Path.GetFileName(pathName);

Przedstawione do tej pory przykłady nie uwzględniają zagadnień dotyczących zabezpieczeń. Dodatkowe informacje są udostępniane przez następujące sekcje i przykładową aplikację:

Podczas przekazywania plików przy użyciu powiązania modelu i IFormFilemetody akcji można zaakceptować następujące elementy:

Uwaga

Powiązanie pasuje do plików formularzy według nazwy. Na przykład wartość HTML name w <input type="file" name="formFile"> pliku musi być zgodna z powiązanym parametrem/właściwością języka C#(FormFile). Aby uzyskać więcej informacji, zobacz sekcję Match name attribute value to parameter name of POST method (Dopasowanie wartości atrybutu nazwy do nazwy parametru metody POST ).

Poniższy przykład:

  • Przechodzi w pętli przez co najmniej jeden przekazany plik.
  • Używa path.GetTempFileName , aby zwrócić pełną ścieżkę pliku, w tym nazwę pliku.
  • Zapisuje pliki w lokalnym systemie plików przy użyciu nazwy pliku wygenerowanej przez aplikację.
  • Zwraca łączną liczbę i rozmiar przekazanych plików.
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 });
}

Użyj polecenia Path.GetRandomFileName , aby wygenerować nazwę pliku bez ścieżki. W poniższym przykładzie ścieżka jest uzyskiwana z konfiguracji:

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

Ścieżka przekazana do elementu FileStreammusi zawierać nazwę pliku. Jeśli nie podano nazwy pliku, UnauthorizedAccessException element jest zgłaszany w czasie wykonywania.

Pliki przekazywane przy użyciu IFormFile tej techniki są buforowane w pamięci lub na dysku na serwerze przed przetworzeniem. Wewnątrz metody IFormFile akcji zawartość jest dostępna jako Stream. Oprócz lokalnego systemu plików pliki można zapisywać w udziale sieciowym lub w usłudze magazynu plików, takiej jak Azure Blob Storage.

Inny przykład, który zapętla wiele plików do przekazywania i używa bezpiecznych nazw plików, zobacz Pages/BufferedMultipleFileUploadPhysical.cshtml.cs w przykładowej aplikacji.

Ostrzeżenie

Path.GetTempFileName zgłasza błąd IOException , jeśli utworzono więcej niż 65 535 plików bez usuwania poprzednich plików tymczasowych. Limit 65 535 plików jest limitem na serwer. Aby uzyskać więcej informacji na temat tego limitu w systemie operacyjnym Windows, zobacz uwagi w następujących tematach:

Przekazywanie małych plików z powiązaniem modelu buforowanego do bazy danych

Aby przechowywać dane plików binarnych w bazie danych przy użyciu programu Entity Framework, zdefiniuj Byte właściwość tablicy w jednostce:

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

Określ właściwość modelu strony dla klasy zawierającej element IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Uwaga

IFormFile można używać bezpośrednio jako parametru metody akcji lub jako powiązanej właściwości modelu. W poprzednim przykładzie użyto właściwości powiązanego modelu.

Element FileUpload jest używany w formularzu Razor Strony:

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

Gdy formularz jest poSTed na serwerze, skopiuj IFormFile element do strumienia i zapisz go jako tablicę bajtów w bazie danych. W poniższym przykładzie _dbContext przechowuje kontekst bazy danych aplikacji:

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

Powyższy przykład jest podobny do scenariusza przedstawionego w przykładowej aplikacji:

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

Ostrzeżenie

Należy zachować ostrożność podczas przechowywania danych binarnych w relacyjnych bazach danych, ponieważ może to mieć negatywny wpływ na wydajność.

Nie należy polegać na właściwości ani ufać FileName jej IFormFile bez walidacji. Właściwość FileName powinna być używana tylko do celów wyświetlania i tylko po kodowaniu HTML.

Podane przykłady nie uwzględniają zagadnień dotyczących zabezpieczeń. Dodatkowe informacje są udostępniane przez następujące sekcje i przykładową aplikację:

Przekazywanie dużych plików za pomocą przesyłania strumieniowego

W poniższym przykładzie pokazano, jak za pomocą języka JavaScript przesyłać strumieniowo plik do akcji kontrolera. Token antyforgery pliku jest generowany przy użyciu niestandardowego atrybutu filtru i przekazywany do nagłówków HTTP klienta zamiast w treści żądania. Ponieważ metoda akcji przetwarza przekazane dane bezpośrednio, powiązanie modelu formularza jest wyłączone przez inny filtr niestandardowy. W ramach akcji zawartość formularza jest odczytywana przy użyciu MultipartReaderelementu , który odczytuje poszczególne elementy MultipartSection, przetwarzając plik lub przechowując zawartość zgodnie z potrzebami. Po przeczytaniu sekcji wieloczęściowych akcja wykonuje własne powiązanie modelu.

Początkowa odpowiedź strony ładuje formularz i zapisuje token antyforgery w cookie obiekcie (za pośrednictwem atrybutu GenerateAntiforgeryTokenCookieAttribute ). Atrybut używa wbudowanej obsługi antyforgeryjnej ASP.NET Core, aby ustawić cookie element z tokenem żądania:

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

Element DisableFormValueModelBindingAttribute służy do wyłączania powiązania modelu:

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

W przykładowej aplikacji GenerateAntiforgeryTokenCookieAttribute i DisableFormValueModelBindingAttribute są stosowane jako filtry do modeli /StreamedSingleFileUploadDb aplikacji strony i /StreamedSingleFileUploadPhysical w Startup.ConfigureServiceskonwencjiRazor stron:

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

Ponieważ powiązanie modelu nie odczytuje formularza, parametry powiązane z formularzem nie są powiązane (zapytanie, trasa i nagłówek nadal działają). Metoda akcji działa bezpośrednio z właściwością Request . Element MultipartReader służy do odczytywania każdej sekcji. Dane klucza/wartości są przechowywane w obiekcie KeyValueAccumulator. Po odczytaniu sekcji wieloczęściowych KeyValueAccumulator zawartość elementu jest używana do powiązania danych formularza z typem modelu.

Kompletna StreamingController.UploadDatabase metoda przesyłania strumieniowego do bazy danych za pomocą EF Corepolecenia :

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

Pełna StreamingController.UploadPhysical metoda przesyłania strumieniowego do lokalizacji fizycznej:

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

W przykładowej aplikacji sprawdzanie poprawności jest obsługiwane przez usługę FileHelpers.ProcessStreamedFile.

Walidacja

Klasa przykładowej FileHelpers aplikacji demonstruje kilka testów przekazywania plików buforowanych IFormFile i przesyłanych strumieniowo. Aby uzyskać informacje na temat przetwarzania IFormFile przekazywania buforowanych plików w przykładowej aplikacji, zobacz metodę ProcessFormFileUtilities/FileHelpers.cs w pliku . Aby uzyskać informacje o przetwarzaniu plików przesyłanych strumieniowo, zobacz metodę ProcessStreamedFile w tym samym pliku.

Ostrzeżenie

Metody przetwarzania weryfikacji przedstawione w przykładowej aplikacji nie skanują zawartości przekazanych plików. W większości scenariuszy produkcyjnych interfejs API skanera wirusów/złośliwego oprogramowania jest używany w pliku przed udostępnieniem pliku użytkownikom lub innym systemom.

Chociaż przykładowy temat zawiera działający przykład technik weryfikacji, nie implementuj FileHelpers klasy w aplikacji produkcyjnej, chyba że:

  • Pełne zrozumienie implementacji.
  • Zmodyfikuj implementację odpowiednio do środowiska i specyfikacji aplikacji.

Nigdy nieupowszechnie implementuj kod zabezpieczeń w aplikacji bez spełnienia tych wymagań.

Walidacja zawartości

Użyj interfejsu API skanowania wirusów/złośliwego oprogramowania innej firmy w przypadku przekazanej zawartości.

Skanowanie plików wymaga zasobów serwera w scenariuszach o dużej ilości. Jeśli wydajność przetwarzania żądań zostanie zmniejszona z powodu skanowania plików, rozważ odciążenie pracy skanowania do usługi w tle, prawdopodobnie usługi działającej na serwerze innym niż serwer aplikacji. Zazwyczaj przekazane pliki są przechowywane w obszarze poddane kwarantannie, dopóki skaner wirusów w tle ich nie sprawdzi. Po przejściu pliku plik jest przenoszony do normalnej lokalizacji przechowywania plików. Te kroki są zwykle wykonywane w połączeniu z rekordem bazy danych, który wskazuje stan skanowania pliku. Korzystając z takiego podejścia, aplikacja i serwer aplikacji pozostają skoncentrowane na odpowiadaniu na żądania.

Walidacja rozszerzenia pliku

Rozszerzenie przekazanego pliku powinno być sprawdzane pod kątem listy dozwolonych rozszerzeń. Przykład:

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
}

Sprawdzanie poprawności podpisu pliku

Podpis pliku jest określany przez kilka pierwszych bajtów na początku pliku. Te bajty mogą służyć do wskazania, czy rozszerzenie jest zgodne z zawartością pliku. Przykładowa aplikacja sprawdza podpisy plików dla kilku typowych typów plików. W poniższym przykładzie podpis pliku dla obrazu JPEG jest sprawdzany względem pliku:

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

Aby uzyskać dodatkowe podpisy plików, użyj bazy danych podpisów plików (wynik wyszukiwania Google) i oficjalnych specyfikacji plików. Konsultując się z oficjalnymi specyfikacjami plików, można upewnić się, że wybrane podpisy są prawidłowe.

Zabezpieczenia nazw plików

Nigdy nie należy używać nazwy pliku dostarczonego przez klienta do zapisywania pliku w magazynie fizycznym. Utwórz bezpieczną nazwę pliku przy użyciu parametru Path.GetRandomFileName lub Path.GetTempFileName , aby utworzyć pełną ścieżkę (w tym nazwę pliku) dla magazynu tymczasowego.

Razor program automatycznie koduje wartości właściwości dla wyświetlania. Następujący kod jest bezpieczny do użycia:

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

Poza elementem Razornależy zawsze HtmlEncode nazwać pliku z żądania użytkownika.

Wiele implementacji musi zawierać sprawdzenie, czy plik istnieje; w przeciwnym razie plik jest zastępowany przez plik o tej samej nazwie. Podaj dodatkową logikę, aby spełnić specyfikacje aplikacji.

Walidacja rozmiaru

Ogranicz rozmiar przekazanych plików.

W przykładowej aplikacji rozmiar pliku jest ograniczony do 2 MB (wskazane w bajtach). Limit jest dostarczany za pośrednictwem konfiguracji z appsettings.json pliku:

{
  "FileSizeLimit": 2097152
}

Element FileSizeLimit jest wstrzykiwany do PageModel klas:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Gdy rozmiar pliku przekroczy limit, plik zostanie odrzucony:

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

Dopasuj wartość atrybutu nazwy do nazwy parametru metody POST

W formularzach innychRazor niż dane formularza POST lub bezpośrednio używających kodu JavaScript FormData nazwa określona w elememencie formularza lub FormData musi być zgodna z nazwą parametru w akcji kontrolera.

W poniższym przykładzie:

  • W przypadku używania <input> elementu name atrybut jest ustawiony na wartość battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • W przypadku używania FormData w języku JavaScript nazwa jest ustawiona na wartość battlePlans:

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

Użyj pasującej nazwy dla parametru metody C# (battlePlans):

  • Razor Dla metody obsługi stron Pages o nazwie Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Dla metody akcji kontrolera POST MVC:

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

Konfiguracja serwera i aplikacji

Limit długości ciała wieloczęściowego

MultipartBodyLengthLimit określa limit długości każdej treści wieloczęściowej. Sekcje formularzy, które przekraczają ten limit, zgłaszają InvalidDataException błąd podczas analizowania. Wartość domyślna to 134 217 728 (128 MB). Dostosowywanie limitu MultipartBodyLengthLimit przy użyciu ustawienia w programie Startup.ConfigureServices:

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

RequestFormLimitsAttribute służy do ustawiania MultipartBodyLengthLimit elementu dla pojedynczej strony lub akcji.

Razor W aplikacji Pages zastosuj filtr z konwencją w pliku 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 W aplikacji Pages lub aplikacji MVC zastosuj filtr do modelu strony lub metody akcji:

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

Kestrel maksymalny rozmiar treści żądania

W przypadku aplikacji hostowanych przez Kestrelprogram domyślny maksymalny rozmiar treści żądania to 30 000 000 bajtów, czyli około 28,6 MB. Dostosuj limit przy użyciu opcji serwera 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 Służy do ustawiania parametru MaxRequestBodySize dla pojedynczej strony lub akcji.

Razor W aplikacji Pages zastosuj filtr z konwencją w pliku 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 W aplikacji stron lub aplikacji MVC zastosuj filtr do klasy obsługi stron lub metody akcji:

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

Inne Kestrel limity

Inne Kestrel limity mogą dotyczyć aplikacji hostowanych przez Kestrelusługę :

IIS

Domyślny limit żądań (maxAllowedContentLength) wynosi 30 000 000 bajtów, czyli około 28,6 MB. Dostosuj limit w web.config pliku. W poniższym przykładzie limit jest ustawiony na 50 MB (52 428 800 bajtów):

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

To maxAllowedContentLength ustawienie dotyczy tylko usług IIS. Aby uzyskać więcej informacji, zobacz Limity żądań <requestLimits>.

Zwiększ maksymalny rozmiar treści żądania dla żądania HTTP, ustawiając wartość w pliku IISServerOptions.MaxRequestBodySizeStartup.ConfigureServices. W poniższym przykładzie limit jest ustawiony na 50 MB (52 428 800 bajtów):

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

Aby uzyskać więcej informacji, zobacz Host ASP.NET Core w systemie Windows z usługami IIS.

Rozwiązywanie problemów

Poniżej przedstawiono niektóre typowe problemy występujące podczas pracy z przekazywaniem plików i ich możliwymi rozwiązaniami.

Nie znaleziono błędu podczas wdrażania na serwerze usług IIS

Następujący błąd wskazuje, że przekazany plik przekracza skonfigurowaną długość zawartości serwera:

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

Aby uzyskać więcej informacji, zobacz sekcję IIS .

Błąd połączenia

Błąd połączenia i połączenie serwera resetowania prawdopodobnie wskazuje, że przekazany plik przekracza Kestrelmaksymalny rozmiar treści żądania. Aby uzyskać więcej informacji, zobacz sekcję maksymalnego rozmiaruKestrel treści żądania. Kestrel Limity połączeń klienta mogą również wymagać korekty.

Wyjątek odwołania o wartości null z plikiem IFormFile

Jeśli kontroler akceptuje przekazane pliki przy użyciu wartości IFormFile , ale wartość to null, upewnij się, że formularz HTML określa enctype wartość multipart/form-data. Jeśli ten atrybut nie jest ustawiony na <form> elemecie, przekazywanie pliku nie występuje, a żadne argumenty powiązane IFormFile to null. Upewnij się również, że przekazywanie nazewnictwa w danych formularza jest zgodne z nazewnictwem aplikacji.

Strumień był za długi

Przykłady w tym temacie polegają na MemoryStream przechowywaniu zawartości przekazanego pliku. Limit rozmiaru elementu MemoryStream to int.MaxValue. Jeśli scenariusz przekazywania plików aplikacji wymaga przechowywania zawartości pliku większej niż 50 MB, użyj alternatywnego podejścia, które nie polega na jednym MemoryStream do przechowywania zawartości przekazanego pliku.

Dodatkowe zasoby