Przekazywanie plików w ASP.NET Core

Autor: Rutger Storm

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

Należy zachować ostrożność, zapewniając użytkownikom możliwość 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:

  • Przekaż pliki do dedykowanego obszaru przekazywania plików, najlepiej do dysku innego niż system. 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 katalogów co plik app.†
  • Użyj bezpiecznej nazwy pliku określonej przez aplikację. Nie używaj nazwy pliku podanej przez użytkownika ani niezaufanej nazwy pliku przekazanego pliku.† HTML kodują 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 są łatwe do obejścia.
  • Sprawdź rozmiar przekazanego pliku. Ustawianie maksymalnego limitu 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 przejmij kontrolę nad systemem.
  • Przeciąż system z wynikiem awarii systemu.
  • Naruszenie danych użytkownika lub systemu.
  • Zastosuj 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 (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 dotyczących dużej infrastruktury magazynu.

    Aby uzyskać więcej informacji, zobacz Szybki start: tworzenie obiektu blob w magazynie obiektów przy użyciu 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 być testami porównawczymi podejścia magazynu używanego do zapewnienia, że może obsłużyć oczekiwane rozmiary. Przeprowadź test porównawczy 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 danego wdrożenia, poniżej przedstawiono niektóre z powiązanych wartości domyślnych platformy 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 FormOptionsprogramu , 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 do 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 operacji 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 jest zdefiniowana, pliki są zapisywane w folderze tymczasowym bieżącego użytkownika.

Buforowanie małych plików opisano 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 zwiększa wydajności znacznie. 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 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 Pages 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 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 Fetch Polyfill (na przykład window.fetch polyfill (github/fetch)).

  • Użyj witryny 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 dotyczących 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 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 kodzie 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 IFormFilemetoda akcji może zaakceptować:

Uwaga

Powiązanie pasuje do plików formularza według nazwy. Na przykład wartość HTML name w <input type="file" name="formFile"> pliku musi być zgodna z granicą parametru/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 (Dopasowywanie 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ę dla 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 nazwa pliku nie jest podana, element UnauthorizedAccessException jest zgłaszany 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.

W innym przykładzie, 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ż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 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 3.1 pokazano, jak używać języka JavaScript do przesyłania strumieniowego pliku 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 ochrony przed fałszerzami ASP.NET Core w celu ustawienia cookie tokenu żą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 są stosowane jako filtry do modeli aplikacji strony i /StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysical w Startup.ConfigureServices korzystaniu z Razor konwencjiDisableFormValueModelBindingAttribute 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 A MultipartReader służy do odczytywania każdej sekcji. Dane klucza/wartości są przechowywane w obiekcie KeyValueAccumulator. Po odczytaniu sekcji wieloczęściowych zawartość KeyValueAccumulator elementu jest używana do powiązania danych formularza z typem modelu.

Pełna StreamingController.UploadDatabase metoda przesyłania strumieniowego do bazy danych z EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

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

Sprawdzanie poprawności

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 buforowanych plików przekazywanych w przykładowej aplikacji, zobacz metodę ProcessFormFileUtilities/FileHelpers.cs w pliku . W przypadku przetwarzania plików przesyłanych strumieniowo zobacz metodę ProcessStreamedFile w tym samym pliku.

Ostrzeżenie

Metody przetwarzania poprawności 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.

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

  • W pełni zrozumieć implementację.
  • Zmodyfikuj implementację odpowiednio dla środowiska i specyfikacji aplikacji.

Nigdy nie należy masowo implementować kodu 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ń jest zmniejszona z powodu skanowania plików, rozważ odciążenie pracy skanowania w usłudze w tle, prawdopodobnie usługi działającej na serwerze innym niż serwer aplikacji. Zazwyczaj przekazane pliki są przechowywane w obszarze poddanej kwarantannie, dopóki skaner wirusów w tle ich nie sprawdzi. Po przejściu pliku plik zostanie przeniesiony 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 względem 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
}

Walidacja 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 sygnatury plików, użyj bazy danych podpisów plików (wynik wyszukiwania Google) i oficjalnych specyfikacji plików. Skonsultowanie się z oficjalnymi specyfikacjami plików może zapewnić, ż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 parametru 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>
}

Poza elementem Razorzawsze HtmlEncode należy nazwać pliku zawartość żą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 (wskazanych 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 innych niżRazor 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 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 ciała wieloczęściowego

MultipartBodyLengthLimit określa limit długości każdego korpusu wieloczęściowego. Sekcje formularzy, które przekraczają ten limit, zgłaszają InvalidDataException błąd 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 pojedynczej strony lub akcji.

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

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 wartości MaxRequestBodySize dla pojedynczej strony lub akcji.

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

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 strony 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) to 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>).

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 z serwerem resetowania prawdopodobnie wskazuje, że przekazany plik przekracza Kestrelmaksymalny rozmiar treści żądania. Aby uzyskać więcej informacji, zobacz sekcję maksymalnego rozmiaru Kestrel 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 IFormFile wartości , 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 zabezpieczeń

Należy zachować ostrożność, zapewniając użytkownikom możliwość 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:

  • Przekaż pliki do dedykowanego obszaru przekazywania plików, najlepiej do dysku innego niż system. 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 katalogów co plik app.†
  • Użyj bezpiecznej nazwy pliku określonej przez aplikację. Nie używaj nazwy pliku podanej przez użytkownika ani niezaufanej nazwy pliku przekazanego pliku.† HTML kodują 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 są łatwe do obejścia.
  • Sprawdź rozmiar przekazanego pliku. Ustawianie maksymalnego limitu 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 przejmij kontrolę nad systemem.
  • Przeciąż system z wynikiem awarii systemu.
  • Naruszenie danych użytkownika lub systemu.
  • Zastosuj 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 (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ż 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 dotyczących dużej infrastruktury magazynu.

    Aby uzyskać więcej informacji, zobacz Szybki start: tworzenie obiektu blob w magazynie obiektów przy użyciu 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 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 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 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 opisano 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 zwiększa wydajności znacznie. 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 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 Pages 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 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 Fetch Polyfill (na przykład window.fetch polyfill (github/fetch)).

  • Użyj witryny 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 dotyczących 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 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 kodzie 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 IFormFilemetoda akcji może zaakceptować:

Uwaga

Powiązanie pasuje do plików formularza według nazwy. Na przykład wartość HTML name w <input type="file" name="formFile"> pliku musi być zgodna z granicą parametru/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 (Dopasowywanie 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ę dla 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 nazwa pliku nie jest podana, element UnauthorizedAccessException jest zgłaszany 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.

W innym przykładzie, 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ż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 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 używać języka JavaScript do przesyłania strumieniowego pliku 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 ochrony przed fałszerzami ASP.NET Core w celu ustawienia cookie tokenu żą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 są stosowane jako filtry do modeli aplikacji strony i /StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysical w Startup.ConfigureServices korzystaniu z Razor konwencjiDisableFormValueModelBindingAttribute 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 A MultipartReader służy do odczytywania każdej sekcji. Dane klucza/wartości są przechowywane w obiekcie KeyValueAccumulator. Po odczytaniu sekcji wieloczęściowych zawartość KeyValueAccumulator elementu jest używana do powiązania danych formularza z typem modelu.

Pełna StreamingController.UploadDatabase metoda przesyłania strumieniowego do bazy danych z EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

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

Sprawdzanie poprawności

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 buforowanych plików przekazywanych w przykładowej aplikacji, zobacz metodę ProcessFormFileUtilities/FileHelpers.cs w pliku . W przypadku przetwarzania plików przesyłanych strumieniowo zobacz metodę ProcessStreamedFile w tym samym pliku.

Ostrzeżenie

Metody przetwarzania poprawności 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.

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

  • W pełni zrozumieć implementację.
  • Zmodyfikuj implementację odpowiednio dla środowiska i specyfikacji aplikacji.

Nigdy nie należy masowo implementować kodu 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ń jest zmniejszona z powodu skanowania plików, rozważ odciążenie pracy skanowania w usłudze w tle, prawdopodobnie usługi działającej na serwerze innym niż serwer aplikacji. Zazwyczaj przekazane pliki są przechowywane w obszarze poddanej kwarantannie, dopóki skaner wirusów w tle ich nie sprawdzi. Po przejściu pliku plik zostanie przeniesiony 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 względem 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
}

Walidacja 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 sygnatury plików, użyj bazy danych podpisów plików (wynik wyszukiwania Google) i oficjalnych specyfikacji plików. Skonsultowanie się z oficjalnymi specyfikacjami plików może zapewnić, ż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 parametru 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>
}

Poza elementem Razorzawsze HtmlEncode należy nazwać pliku zawartość żą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 (wskazanych 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 innych niżRazor 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 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 ciała wieloczęściowego

MultipartBodyLengthLimit określa limit długości każdego korpusu wieloczęściowego. Sekcje formularzy, które przekraczają ten limit, zgłaszają InvalidDataException błąd 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 pojedynczej strony lub akcji.

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

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 wartości MaxRequestBodySize dla pojedynczej strony lub akcji.

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

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 strony 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) to 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 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 z serwerem resetowania prawdopodobnie wskazuje, że przekazany plik przekracza Kestrelmaksymalny rozmiar treści żądania. Aby uzyskać więcej informacji, zobacz sekcję maksymalnego rozmiaru Kestrel 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 IFormFile wartości , 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 zabezpieczeń

Należy zachować ostrożność, zapewniając użytkownikom możliwość 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:

  • Przekaż pliki do dedykowanego obszaru przekazywania plików, najlepiej do dysku innego niż system. 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 katalogów co plik app.†
  • Użyj bezpiecznej nazwy pliku określonej przez aplikację. Nie używaj nazwy pliku podanej przez użytkownika ani niezaufanej nazwy pliku przekazanego pliku.† HTML kodują 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 są łatwe do obejścia.
  • Sprawdź rozmiar przekazanego pliku. Ustawianie maksymalnego limitu 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 przejmij kontrolę nad systemem.
  • Przeciąż system z wynikiem awarii systemu.
  • Naruszenie danych użytkownika lub systemu.
  • Zastosuj 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 (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ż 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 dotyczących dużej infrastruktury magazynu.

    Aby uzyskać więcej informacji, zobacz Szybki start: tworzenie obiektu blob w magazynie obiektów przy użyciu platformy .NET. W tym temacie pokazano UploadFromFileAsyncelement , ale UploadFromStreamAsync można go użyć do zapisania obiektu FileStream 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 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 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 opisano 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 zwiększa wydajności znacznie. 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 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 Pages 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 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 Fetch Polyfill (na przykład window.fetch polyfill (github/fetch)).

  • Użyj witryny 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 dotyczących 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 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 kodzie 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 IFormFilemetoda akcji może zaakceptować:

Uwaga

Powiązanie pasuje do plików formularza według nazwy. Na przykład wartość HTML name w <input type="file" name="formFile"> pliku musi być zgodna z granicą parametru/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 (Dopasowywanie 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ę dla 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 nazwa pliku nie jest podana, element UnauthorizedAccessException jest zgłaszany 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.

W innym przykładzie, 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ż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 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 używać języka JavaScript do przesyłania strumieniowego pliku 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 ochrony przed fałszerzami ASP.NET Core w celu ustawienia cookie tokenu żą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 są stosowane jako filtry do modeli aplikacji strony i /StreamedSingleFileUploadDb/StreamedSingleFileUploadPhysical w Startup.ConfigureServices korzystaniu z Razor konwencjiDisableFormValueModelBindingAttribute 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 A MultipartReader służy do odczytywania każdej sekcji. Dane klucza/wartości są przechowywane w obiekcie KeyValueAccumulator. Po odczytaniu sekcji wieloczęściowych zawartość KeyValueAccumulator elementu jest używana do powiązania danych formularza z typem modelu.

Pełna StreamingController.UploadDatabase metoda przesyłania strumieniowego do bazy danych z EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

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

Sprawdzanie poprawności

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 buforowanych plików przekazywanych w przykładowej aplikacji, zobacz metodę ProcessFormFileUtilities/FileHelpers.cs w pliku . W przypadku przetwarzania plików przesyłanych strumieniowo zobacz metodę ProcessStreamedFile w tym samym pliku.

Ostrzeżenie

Metody przetwarzania poprawności 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.

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

  • W pełni zrozumieć implementację.
  • Zmodyfikuj implementację odpowiednio dla środowiska i specyfikacji aplikacji.

Nigdy nie należy masowo implementować kodu 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ń jest zmniejszona z powodu skanowania plików, rozważ odciążenie pracy skanowania w usłudze w tle, prawdopodobnie usługi działającej na serwerze innym niż serwer aplikacji. Zazwyczaj przekazane pliki są przechowywane w obszarze poddanej kwarantannie, dopóki skaner wirusów w tle ich nie sprawdzi. Po przejściu pliku plik zostanie przeniesiony 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 względem 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
}

Walidacja 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 sygnatury plików, użyj bazy danych podpisów plików (wynik wyszukiwania Google) i oficjalnych specyfikacji plików. Skonsultowanie się z oficjalnymi specyfikacjami plików może zapewnić, ż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 parametru 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>
}

Poza elementem Razorzawsze HtmlEncode należy nazwać pliku zawartość żą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 (wskazanych 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 innych niżRazor 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 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 ciała wieloczęściowego

MultipartBodyLengthLimit określa limit długości każdego korpusu wieloczęściowego. Sekcje formularzy, które przekraczają ten limit, zgłaszają InvalidDataException błąd 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 pojedynczej strony lub akcji.

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

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 wartości MaxRequestBodySize dla pojedynczej strony lub akcji.

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

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 strony 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 Kestrelprogram :

IIS

Domyślny limit żądań (maxAllowedContentLength) to 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 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 z serwerem resetowania prawdopodobnie wskazuje, że przekazany plik przekracza Kestrelmaksymalny rozmiar treści żądania. Aby uzyskać więcej informacji, zobacz sekcję maksymalnego rozmiaru Kestrel 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 IFormFile wartości , 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.

Dodatkowe zasoby