Nahrání souborů v ASP.NET Core

Autor: Rutger Storm

ASP.NET Core podporuje nahrávání jednoho nebo více souborů pomocí vazby modelu ve vyrovnávací paměti pro menší soubory a neuložení streamovaných souborů u větších souborů.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Bezpečnostní aspekty

Při poskytování uživatelům, kteří mají možnost nahrávat soubory na server, buďte opatrní. Útočníci se mohou pokusit:

Bezpečnostní kroky, které snižují pravděpodobnost úspěšného útoku:

  • Nahrajte soubory do vyhrazené oblasti pro nahrávání souborů, nejlépe na systémovou jednotku. Vyhrazené umístění usnadňuje ukládání bezpečnostních omezení u nahraných souborů. Zakázání oprávnění ke spuštění v umístění pro nahrání souboru.†
  • Neuchovávat nahrané soubory ve stejném adresářovém stromu jako app.†
  • Použijte bezpečný název souboru určený aplikací. Nepoužívejte název souboru zadaný uživatelem ani nedůvěryhodným názvem souboru nahraného souboru.† HTML kódujte při jeho zobrazení nedůvěryhodný název souboru. Například protokolování názvu souboru nebo zobrazení v uživatelském rozhraní (Razor automaticky kóduje výstup HTML).
  • Povolit pouze schválené přípony souborů pro specifikaci návrhu aplikace.†
  • Ověřte, zda jsou kontroly na straně klienta provedeny na server.† Kontroly na straně klienta jsou snadné obejít.
  • Zkontrolujte velikost nahraného souboru. Nastavení maximálního limitu velikosti, aby se zabránilo velkým nahráváním.†
  • Pokud by soubory neměly být přepsány nahraným souborem se stejným názvem, před nahráním souboru zkontrolujte název souboru v databázi nebo fyzickém úložišti.
  • Před uložením souboru spusťte skener virů nebo malwaru na nahraný obsah.

†Vzoreková aplikace ukazuje přístup, který splňuje kritéria.

Upozorňující

Nahrání škodlivého kódu do systému je často prvním krokem ke spuštění kódu, který může:

  • Zcela získejte kontrolu nad systémem.
  • Přetěžte systém výsledkem chybového ukončení systému.
  • Ohrožení uživatelských nebo systémových dat
  • Použití graffiti na veřejné uživatelské rozhraní

Informace o omezení prostoru pro útok při přijímání souborů od uživatelů najdete v následujících zdrojích informací:

Další informace o implementaci bezpečnostních opatření, včetně příkladů z ukázkové aplikace, najdete v části Ověření .

Scénáře úložiště

Mezi běžné možnosti úložiště pro soubory patří:

  • Databáze

    • U malých nahrávek souborů je databáze často rychlejší než možnosti fyzického úložiště (systému souborů nebo síťové sdílené složky).
    • Databáze je často pohodlnější než možnosti fyzického úložiště, protože načtení záznamu databáze pro uživatelská data může současně poskytnout obsah souboru (například obrázek avataru).
    • Databáze je potenciálně levnější než použití cloudové služby úložiště dat.
  • Fyzické úložiště (systém souborů nebo síťová sdílená složka)

    • Pro velké nahrávání souborů:
      • Limity databáze můžou omezit velikost nahrávání.
      • Fyzické úložiště je často méně ekonomické než úložiště v databázi.
    • Fyzické úložiště je potenciálně levnější než použití cloudové služby úložiště dat.
    • Proces aplikace musí mít oprávnění ke čtení a zápisu do umístění úložiště. Nikdy neudělte oprávnění ke spuštění.
  • Cloudová služba úložiště dat, například Azure Blob Storage.

    • Služby obvykle nabízejí lepší škálovatelnost a odolnost před místními řešeními, která obvykle podléhají kritickým bodům selhání.
    • Služby jsou potenciálně nižší náklady ve scénářích infrastruktury velkých úložišť.

    Další informace najdete v tématu Rychlý start: Použití .NET k vytvoření objektu blob v úložišti objektů.

Malé a velké soubory

Definice malých a velkých souborů závisí na dostupných výpočetních prostředcích. Aplikace by měly testovat přístup k úložišti, který se používá k zajištění toho, aby zvládly očekávané velikosti. Proveďte srovnávací testy výkonu paměti, procesoru, disku a databáze.

Konkrétní hranice sice není možné zadat u toho, co je malé a velké pro vaše nasazení, ale tady jsou některé související výchozí hodnoty AspNetCore pro FormOptions:

  • Ve výchozím nastavení httpRequest.Form neukládá do vyrovnávací paměti celý text požadavku (BufferBody), ale uloží do vyrovnávací paměti všechny zahrnuté soubory formulářů s více částmi.
  • MultipartBodyLengthLimit je maximální velikost souborů ve formátu ve vyrovnávací paměti, výchozí hodnota je 128 MB.
  • MemoryBufferThreshold označuje, kolik souborů vyrovnávací paměti před přechodem na soubor vyrovnávací paměti na disku, výchozí hodnota je 64 kB. MemoryBufferThreshold funguje jako hranice mezi malými a velkými soubory, které se zvýší nebo sníží v závislosti na prostředcích a scénářích aplikací.

Další informace najdete FormOptionsve zdrojovém kódu.

Scénáře nahrávání souborů

Dva obecné přístupy pro nahrávání souborů jsou ukládání do vyrovnávací paměti a streamování.

Vyrovnávací paměti

Celý soubor se načte do souboru IFormFile. IFormFile je reprezentace souboru jazyka C#, který se používá ke zpracování nebo uložení souboru.

Disk a paměť používané nahráváním souborů závisí na počtu a velikosti souběžných nahrávání souborů. Pokud se aplikace pokusí uložit příliš mnoho nahrávek do vyrovnávací paměti, dojde k chybovému ukončení webu při nedostatku paměti nebo místa na disku. Pokud velikost nebo frekvence nahrávání souborů vyčerpává prostředky aplikace, použijte streamování.

Všechny soubory s jednou vyrovnávací pamětí větší než 64 kB se přesunou z paměti do dočasného souboru na disku.

Dočasné soubory pro větší požadavky se zapíšou do umístění pojmenovaného ASPNETCORE_TEMP v proměnné prostředí. Pokud ASPNETCORE_TEMP není definován, soubory se zapíšou do dočasné složky aktuálního uživatele.

Ukládání malých souborů do vyrovnávací paměti je popsáno v následujících částech tohoto tématu:

Streamování

Soubor se přijímá z žádosti o více částí a aplikace ho přímo zpracuje nebo uloží. Streamování výrazně nezlepší výkon. Streamování snižuje požadavky na paměť nebo místo na disku při nahrávání souborů.

Streamování velkých souborů je popsáno v části Nahrání velkých souborů pomocí oddílu streamování .

Nahrání malých souborů s vazbou modelu do vyrovnávací paměti do fyzického úložiště

Pokud chcete nahrát malé soubory, použijte formulář s více částmi nebo vytvořte požadavek POST pomocí JavaScriptu.

Následující příklad ukazuje použití Razor formuláře Pages k nahrání jednoho souboru (Pages/BufferedSingleFileUploadPhysical.cshtml v ukázkové aplikaci):

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

Následující příklad je podobný předchozímu příkladu s tím rozdílem, že:

  • JavaScript (Fetch API) slouží k odeslání dat formuláře.
  • Neexistuje žádné ověření.
<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>

K provedení formuláře POST v JavaScriptu pro klienty, kteří nepodporují rozhraní Fetch API, použijte jeden z následujících přístupů:

  • Použijte funkci Fetch Polyfill (například window.fetch polyfill (github/fetch)).

  • Použijte XMLHttpRequest. Příklad:

    <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 bylo možné podporovat nahrávání souborů, musí formuláře HTML určovat typ kódování (enctype) .multipart/form-data

files Vstupní prvek pro podporu nahrávání více souborů poskytuje multiple atribut elementu<input>:

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

Jednotlivé soubory nahrané na server lze získat přístup prostřednictvím vazby modelu pomocí IFormFile. Ukázková aplikace ukazuje několik nahrávání souborů do vyrovnávací paměti pro scénáře databáze a fyzického úložiště.

Upozorňující

FileName Nepoužívejte vlastnost jiné než IFormFile pro zobrazení a protokolování. Při zobrazení nebo protokolování kóduje kód HTML název souboru. Útočník může poskytnout škodlivý název souboru, včetně úplných cest nebo relativních cest. Aplikace by měly:

  • Odeberte cestu ze souboru zadaného uživatelem.
  • Uložte název souboru s kódováním HTML nebo odebranou cestou pro uživatelské rozhraní nebo protokolování.
  • Vygenerujte nový náhodný název souboru pro úložiště.

Následující kód odebere cestu z názvu souboru:

string untrustedFileName = Path.GetFileName(pathName);

Zatím uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:

Při nahrávání souborů pomocí vazby modelu a IFormFilemetoda akce může přijmout:

Poznámka

Vazba odpovídá souborům formuláře podle názvu. Například hodnota HTML name musí <input type="file" name="formFile"> odpovídat vázanémuFormFile parametru nebo vlastnosti jazyka C#. Další informace naleznete v části Match name attribute value to parameter name name of POST method section.

Následující příklad:

  • Smyčky procházejí jedním nebo více nahranými soubory.
  • Pomocí Path.GetTempFileName vrátí úplnou cestu k souboru, včetně názvu souboru.
  • Uloží soubory do místního systému souborů pomocí názvu souboru vygenerovaného aplikací.
  • Vrátí celkový počet a velikost nahraných souborů.
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 });
}

Slouží Path.GetRandomFileName k vygenerování názvu souboru bez cesty. V následujícím příkladu se cesta získá z konfigurace:

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

Cesta předaná do souboru FileStreammusí obsahovat název souboru. Pokud není zadaný název souboru, UnauthorizedAccessException vyvolá se za běhu.

Soubory nahrané pomocí IFormFile techniky se před zpracováním ukládají do vyrovnávací paměti nebo na disku na serveru. Uvnitř metody IFormFile akce je obsah přístupný jako Stream. Kromě místního systému souborů je možné soubory ukládat do sdílené síťové složky nebo do služby úložiště souborů, jako je azure Blob Storage.

Další příklad, který pro nahrání smyčuje více souborů a používá bezpečné názvy souborů, najdete Pages/BufferedMultipleFileUploadPhysical.cshtml.cs v ukázkové aplikaci.

Upozorňující

Path.GetTempFileName vyvolá chybu IOException , pokud se vytvoří více než 65 535 souborů bez odstranění předchozích dočasných souborů. Limit 65 535 souborů je limit pro jednotlivé servery. Další informace o tomto limitu operačního systému Windows najdete v poznámkách v následujících tématech:

Nahrání malých souborů s vazbou modelu ve vyrovnávací paměti do databáze

Pokud chcete ukládat data binárního souboru do databáze pomocí Entity Frameworku, definujte Byte vlastnost pole u entity:

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

Zadejte vlastnost modelu stránky pro třídu, která obsahuje :IFormFile

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Poznámka

IFormFile lze použít přímo jako parametr metody akce nebo jako vlastnost vázaného modelu. Předchozí příklad používá vlastnost vázaného modelu.

Používá se FileUpload ve formuláři Razor Stránky:

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

Když je formulář poSTed na server, zkopírujte IFormFile ho do datového proudu a uložte ho jako pole bajtů v databázi. V následujícím příkladu _dbContext uloží kontext databáze aplikace:

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

Předchozí příklad je podobný scénáři popsanému v ukázkové aplikaci:

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

Upozorňující

Při ukládání binárních dat v relačních databázích buďte opatrní, protože to může nepříznivě ovlivnit výkon.

Nespoléhejte na vlastnost IFormFile bez ověření ani ji nedůvěřujteFileName. Vlastnost FileName by měla být použita pouze pro účely zobrazení a pouze po kódování HTML.

Uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:

Nahrávání velkých souborů se streamováním

Příklad 3.1 ukazuje, jak pomocí JavaScriptu streamovat soubor do akce kontroleru. Antiforgery token souboru se vygeneruje pomocí atributu vlastního filtru a předá se hlavičkám HTTP klienta místo v textu požadavku. Vzhledem k tomu, že metoda akce zpracovává nahraná data přímo, je vazba modelu formuláře zakázána jiným vlastním filtrem. Obsah formuláře se v rámci akce čte pomocí MultipartReadersouboru, který čte jednotlivé soubory MultipartSectionnebo ukládá obsah podle potřeby. Po přečtení oddílů s více částmi provede akce vlastní vazbu modelu.

Počáteční odpověď stránky načte formulář a uloží antiforgery token do objektu cookie (prostřednictvím atributu GenerateAntiforgeryTokenCookieAttribute ). Tento atribut používá k nastavení cookie tokenu požadavku integrovanou podporu antiforgery ASP.NET Core:

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

Slouží DisableFormValueModelBindingAttribute k zakázání vazby 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)
    {
    }
}

V ukázkové aplikaci GenerateAntiforgeryTokenCookieAttribute a DisableFormValueModelBindingAttribute použijí se jako filtry pro modely /StreamedSingleFileUploadDb stránkovací aplikace a /StreamedSingleFileUploadPhysical při Startup.ConfigureServices používání Razor konvencí Pages:

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

Vzhledem k tomu, že vazba modelu nečte formulář, parametry vázané z formuláře se neváže (dotaz, trasa a hlavička budou nadále fungovat). Metoda akce funguje přímo s Request vlastností. A MultipartReader slouží ke čtení jednotlivých oddílů. Data klíče/hodnoty jsou uložena KeyValueAccumulatorv souboru . Po přečtení oddílů s více částmi se obsah formuláře KeyValueAccumulator použije k vytvoření vazby dat formuláře na typ modelu.

Úplná StreamingController.UploadDatabase metoda streamování do databáze pomocí 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));
        }
    }
}

Úplná StreamingController.UploadPhysical metoda streamování do fyzického umístění:

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

V ukázkové aplikaci se kontroly ověření zpracovávají FileHelpers.ProcessStreamedFile.

Ověření

Třída ukázkové aplikace FileHelpers ukazuje několik kontrol ukládání do IFormFile vyrovnávací paměti a nahrání streamovaných souborů. Informace o zpracování IFormFile nahrávání souborů ve vyrovnávací paměti v ukázkové aplikaci najdete v ProcessFormFile metodě v Utilities/FileHelpers.cs souboru. Pokud chcete zpracovávat streamované soubory, podívejte se na metodu ProcessStreamedFile ve stejném souboru.

Upozorňující

Metody zpracování ověřování, které jsou ukázány v ukázkové aplikaci, nekontroluje obsah nahraných souborů. Ve většině produkčních scénářů se v souboru před zpřístupněním souboru uživatelům nebo jiným systémům používá rozhraní API pro kontrolu virů a malwaru.

I když ukázka tématu poskytuje funkční příklad technik ověřování, neimplementujte FileHelpers třídu v produkční aplikaci, pokud:

  • Plně porozumíte implementaci.
  • Upravte implementaci podle potřeby pro prostředí a specifikace aplikace.

Nikdy nerozlišují implementaci bezpečnostního kódu v aplikaci bez řešení těchto požadavků.

Ověření obsahu

K nahrání obsahu použijte rozhraní API pro kontrolu virů nebo malwaru třetí strany.

Skenování souborů je náročné na serverové prostředky ve scénářích s velkým objemem. Pokud se sníží výkon zpracování požadavků z důvodu prohledávání souborů, zvažte snížení zátěže skenovací práce do služby na pozadí, případně služby spuštěné na serveru, který se liší od serveru aplikace. Nahrané soubory se obvykle uchovávají v karanténě, dokud je antivirový skener na pozadí nekontroluje. Když soubor projde, soubor se přesune do normálního umístění úložiště souborů. Tyto kroky se obvykle provádějí ve spojení se záznamem databáze, který označuje stav kontroly souboru. Díky takovému přístupu zůstává aplikace a aplikační server zaměřeny na reakci na požadavky.

Ověření přípony souboru

Přípona nahraného souboru by se měla kontrolovat v seznamu povolených přípon. Příklad:

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
}

Ověření podpisu souboru

Podpis souboru je určen několika prvními bajty na začátku souboru. Tyto bajty lze použít k označení, jestli přípona odpovídá obsahu souboru. Ukázková aplikace kontroluje podpisy souborů u několika běžných typů souborů. V následujícím příkladu je podpis souboru pro obrázek JPEG zkontrolován proti souboru:

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

Chcete-li získat další podpisy souborů, použijte databázi podpisů souborů (výsledek vyhledávání Google) a oficiální specifikace souborů. Specifikace oficiálního souboru konzultace mohou zajistit, aby vybrané podpisy byly platné.

Zabezpečení názvu souboru

Nikdy nepoužívejte název souboru zadaného klientem k uložení souboru do fyzického úložiště. Vytvořte pro soubor bezpečný název souboru pomocí Path.GetRandomFileName nebo Path.GetTempFileName a vytvořte úplnou cestu (včetně názvu souboru) pro dočasné úložiště.

Razor automaticky kóduje hodnoty vlastností pro zobrazení. Následující kód je bezpečný pro použití:

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

RazorMimo text , vždy HtmlEncode název souboru obsahu z žádosti uživatele.

Mnoho implementací musí obsahovat kontrolu, že soubor existuje; v opačném případě se soubor přepíše souborem se stejným názvem. Zadejte další logiku, která vyhovuje specifikacím vaší aplikace.

Ověření velikosti

Omezte velikost nahraných souborů.

V ukázkové aplikaci je velikost souboru omezená na 2 MB (uvedené v bajtech). Limit se poskytuje prostřednictvím konfigurace ze appsettings.json souboru:

{
  "FileSizeLimit": 2097152
}

Vloží se FileSizeLimit do PageModel tříd:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Pokud velikost souboru překročí limit, soubor se odmítne:

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

Shoda s hodnotou atributu name s názvem parametru metody POST

V jinýchRazor formách, než jsou data formuláře POST nebo přímo používají JavaScript FormData , název zadaný v elementu formuláře nebo FormData se musí shodovat s názvem parametru v akci kontroleru.

V následujícím příkladu:

  • Při použití elementu <input>name je atribut nastaven na hodnotu battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Při použití FormData v JavaScriptu je název nastaven na hodnotu battlePlans:

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

Pro parametr metody jazyka C# použijte odpovídající název (battlePlans):

  • Pro metodu obslužné Razor rutiny stránky Pages s názvem Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Metoda akce kontroleru MVC POST:

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

Konfigurace serveru a aplikace

Omezení délky těla s více částmi

MultipartBodyLengthLimit nastaví limit pro délku každého vícedílného těla. Oddíly formuláře, které tento limit překračují, při analýze vyvolá výjimku InvalidDataException . Výchozí hodnota je 134 217 728 (128 MB). Přizpůsobení limitu pomocí nastavení vStartup.ConfigureServices:MultipartBodyLengthLimit

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

RequestFormLimitsAttribute slouží k nastavení MultipartBodyLengthLimit jedné stránky nebo akce.

Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices:

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

Razor V aplikaci Pages nebo aplikaci MVC použijte filtr na model stránky nebo metodu akce:

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

Kestrel maximální velikost textu požadavku

U aplikací hostovaných ve Kestrelvýchozím nastavení je maximální velikost textu požadavku 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit pomocí možnosti Serveru 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 slouží k nastavení MaxRequestBodySize pro jednu stránku nebo akci.

Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices:

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

Razor V aplikaci stránky nebo aplikaci MVC použijte filtr na třídu obslužné rutiny stránky nebo metodu akce:

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

Lze RequestSizeLimitAttribute také použít direktivu @attributeRazor :

@attribute [RequestSizeLimitAttribute(52428800)]

Další Kestrel limity

Jiné Kestrel limity se můžou vztahovat na aplikace hostované:Kestrel

IIS

Výchozí limit požadavků (maxAllowedContentLength) je 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit v web.config souboru. V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):

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

Nastavení maxAllowedContentLength platí jenom pro službu IIS. Další informace naleznete v tématu Omezení <requestLimits>požadavků .

Odstranění potíží

Níže jsou uvedeny některé běžné problémy, ke kterým dochází při práci s nahráváním souborů a jejich možných řešení.

Chyba Nenalezena při nasazení na server služby IIS

Následující chyba značí, že nahraný soubor překračuje nakonfigurovanou délku obsahu serveru:

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

Další informace najdete v části IIS .

Chyba připojení

Chyba připojení a připojení k resetování serveru pravděpodobně značí, že nahraný soubor překračuje Kestrelmaximální velikost textu požadavku. Další informace najdete v Kestrel části Maximální velikost textu požadavku. Kestrel Limity připojení klienta mohou vyžadovat také úpravu.

Výjimka odkazu null se souborem IFormFile

Pokud kontroler přijímá nahrané soubory pomocí IFormFile , ale hodnota je null, potvrďte, že formulář HTML určuje enctype hodnotu multipart/form-data. Pokud tento atribut není nastaven na <form> elementu, nahraje se soubor a žádné vázané IFormFile argumenty jsou null. Ověřte také, že pojmenování nahrávání v datech formuláře odpovídá pojmenování aplikace.

Stream byl příliš dlouhý.

Příklady v tomto tématu se spoléhají na MemoryStream uložení obsahu nahraného souboru. Limit velikosti je MemoryStreamint.MaxValue. Pokud scénář nahrávání souborů aplikace vyžaduje uchovávání obsahu souborů větší než 50 MB, použijte alternativní přístup, který nespoléhá na jediný MemoryStream obsah nahraného souboru.

ASP.NET Core podporuje nahrávání jednoho nebo více souborů pomocí vazby modelu ve vyrovnávací paměti pro menší soubory a neuložení streamovaných souborů u větších souborů.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Bezpečnostní aspekty

Při poskytování uživatelům, kteří mají možnost nahrávat soubory na server, buďte opatrní. Útočníci se mohou pokusit:

Bezpečnostní kroky, které snižují pravděpodobnost úspěšného útoku:

  • Nahrajte soubory do vyhrazené oblasti pro nahrávání souborů, nejlépe na systémovou jednotku. Vyhrazené umístění usnadňuje ukládání bezpečnostních omezení u nahraných souborů. Zakázání oprávnění ke spuštění v umístění pro nahrání souboru.†
  • Neuchovávat nahrané soubory ve stejném adresářovém stromu jako app.†
  • Použijte bezpečný název souboru určený aplikací. Nepoužívejte název souboru zadaný uživatelem ani nedůvěryhodným názvem souboru nahraného souboru.† HTML kódujte při jeho zobrazení nedůvěryhodný název souboru. Například protokolování názvu souboru nebo zobrazení v uživatelském rozhraní (Razor automaticky kóduje výstup HTML).
  • Povolit pouze schválené přípony souborů pro specifikaci návrhu aplikace.†
  • Ověřte, zda jsou kontroly na straně klienta provedeny na server.† Kontroly na straně klienta jsou snadné obejít.
  • Zkontrolujte velikost nahraného souboru. Nastavení maximálního limitu velikosti, aby se zabránilo velkým nahráváním.†
  • Pokud by soubory neměly být přepsány nahraným souborem se stejným názvem, před nahráním souboru zkontrolujte název souboru v databázi nebo fyzickém úložišti.
  • Před uložením souboru spusťte skener virů nebo malwaru na nahraný obsah.

†Vzoreková aplikace ukazuje přístup, který splňuje kritéria.

Upozorňující

Nahrání škodlivého kódu do systému je často prvním krokem ke spuštění kódu, který může:

  • Zcela získejte kontrolu nad systémem.
  • Přetěžte systém výsledkem chybového ukončení systému.
  • Ohrožení uživatelských nebo systémových dat
  • Použití graffiti na veřejné uživatelské rozhraní

Informace o omezení prostoru pro útok při přijímání souborů od uživatelů najdete v následujících zdrojích informací:

Další informace o implementaci bezpečnostních opatření, včetně příkladů z ukázkové aplikace, najdete v části Ověření .

Scénáře úložiště

Mezi běžné možnosti úložiště pro soubory patří:

  • Databáze

    • U malých nahrávek souborů je databáze často rychlejší než možnosti fyzického úložiště (systému souborů nebo síťové sdílené složky).
    • Databáze je často pohodlnější než možnosti fyzického úložiště, protože načtení záznamu databáze pro uživatelská data může současně poskytnout obsah souboru (například obrázek avataru).
    • Databáze je potenciálně levnější než použití služby úložiště dat.
  • Fyzické úložiště (systém souborů nebo síťová sdílená složka)

    • Pro velké nahrávání souborů:
      • Limity databáze můžou omezit velikost nahrávání.
      • Fyzické úložiště je často méně ekonomické než úložiště v databázi.
    • Fyzické úložiště je potenciálně levnější než použití služby úložiště dat.
    • Proces aplikace musí mít oprávnění ke čtení a zápisu do umístění úložiště. Nikdy neudělte oprávnění ke spuštění.
  • Služba úložiště dat (například Azure Blob Storage)

    • Služby obvykle nabízejí lepší škálovatelnost a odolnost před místními řešeními, která obvykle podléhají kritickým bodům selhání.
    • Služby jsou potenciálně nižší náklady ve scénářích infrastruktury velkých úložišť.

    Další informace najdete v tématu Rychlý start: Použití .NET k vytvoření objektu blob v úložišti objektů.

Scénáře nahrávání souborů

Dva obecné přístupy pro nahrávání souborů jsou ukládání do vyrovnávací paměti a streamování.

Vyrovnávací paměti

Celý soubor se načte do IFormFilesouboru, což je reprezentace souboru jazyka C#, který se používá ke zpracování nebo uložení souboru.

Prostředky (disk, paměť) používané nahráváním souborů závisí na počtu a velikosti souběžných nahrávání souborů. Pokud se aplikace pokusí uložit příliš mnoho nahrávek do vyrovnávací paměti, dojde k chybovému ukončení webu při nedostatku paměti nebo místa na disku. Pokud velikost nebo frekvence nahrávání souborů vyčerpává prostředky aplikace, použijte streamování.

Poznámka

Všechny soubory s jednou vyrovnávací pamětí větší než 64 kB se přesunou z paměti do dočasného souboru na disku.

Ukládání malých souborů do vyrovnávací paměti je popsáno v následujících částech tohoto tématu:

Streamování

Soubor se přijímá z žádosti o více částí a aplikace ho přímo zpracuje nebo uloží. Streamování výrazně nezlepší výkon. Streamování snižuje požadavky na paměť nebo místo na disku při nahrávání souborů.

Streamování velkých souborů je popsáno v části Nahrání velkých souborů pomocí oddílu streamování .

Nahrání malých souborů s vazbou modelu do vyrovnávací paměti do fyzického úložiště

Pokud chcete nahrát malé soubory, použijte formulář s více částmi nebo vytvořte požadavek POST pomocí JavaScriptu.

Následující příklad ukazuje použití Razor formuláře Pages k nahrání jednoho souboru (Pages/BufferedSingleFileUploadPhysical.cshtml v ukázkové aplikaci):

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

Následující příklad je podobný předchozímu příkladu s tím rozdílem, že:

  • JavaScript (Fetch API) slouží k odeslání dat formuláře.
  • Neexistuje žádné ověření.
<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>

K provedení formuláře POST v JavaScriptu pro klienty, kteří nepodporují rozhraní Fetch API, použijte jeden z následujících přístupů:

  • Použijte funkci Fetch Polyfill (například window.fetch polyfill (github/fetch)).

  • Použijte XMLHttpRequest. Příklad:

    <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 bylo možné podporovat nahrávání souborů, musí formuláře HTML určovat typ kódování (enctype) .multipart/form-data

files Vstupní prvek pro podporu nahrávání více souborů poskytuje multiple atribut elementu<input>:

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

Jednotlivé soubory nahrané na server lze získat přístup prostřednictvím vazby modelu pomocí IFormFile. Ukázková aplikace ukazuje několik nahrávání souborů do vyrovnávací paměti pro scénáře databáze a fyzického úložiště.

Upozorňující

FileName Nepoužívejte vlastnost jiné než IFormFile pro zobrazení a protokolování. Při zobrazení nebo protokolování kóduje kód HTML název souboru. Útočník může poskytnout škodlivý název souboru, včetně úplných cest nebo relativních cest. Aplikace by měly:

  • Odeberte cestu ze souboru zadaného uživatelem.
  • Uložte název souboru s kódováním HTML nebo odebranou cestou pro uživatelské rozhraní nebo protokolování.
  • Vygenerujte nový náhodný název souboru pro úložiště.

Následující kód odebere cestu z názvu souboru:

string untrustedFileName = Path.GetFileName(pathName);

Zatím uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:

Při nahrávání souborů pomocí vazby modelu a IFormFilemetoda akce může přijmout:

Poznámka

Vazba odpovídá souborům formuláře podle názvu. Například hodnota HTML name musí <input type="file" name="formFile"> odpovídat vázanémuFormFile parametru nebo vlastnosti jazyka C#. Další informace naleznete v části Match name attribute value to parameter name name of POST method section.

Následující příklad:

  • Smyčky procházejí jedním nebo více nahranými soubory.
  • Pomocí Path.GetTempFileName vrátí úplnou cestu k souboru, včetně názvu souboru.
  • Uloží soubory do místního systému souborů pomocí názvu souboru vygenerovaného aplikací.
  • Vrátí celkový počet a velikost nahraných souborů.
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 });
}

Slouží Path.GetRandomFileName k vygenerování názvu souboru bez cesty. V následujícím příkladu se cesta získá z konfigurace:

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

Cesta předaná do souboru FileStreammusí obsahovat název souboru. Pokud není zadaný název souboru, UnauthorizedAccessException vyvolá se za běhu.

Soubory nahrané pomocí IFormFile techniky se před zpracováním ukládají do vyrovnávací paměti nebo na disku na serveru. Uvnitř metody IFormFile akce je obsah přístupný jako Stream. Kromě místního systému souborů je možné soubory ukládat do sdílené síťové složky nebo do služby úložiště souborů, jako je azure Blob Storage.

Další příklad, který pro nahrání smyčuje více souborů a používá bezpečné názvy souborů, najdete Pages/BufferedMultipleFileUploadPhysical.cshtml.cs v ukázkové aplikaci.

Upozorňující

Path.GetTempFileName vyvolá chybu IOException , pokud se vytvoří více než 65 535 souborů bez odstranění předchozích dočasných souborů. Limit 65 535 souborů je limit pro jednotlivé servery. Další informace o tomto limitu operačního systému Windows najdete v poznámkách v následujících tématech:

Nahrání malých souborů s vazbou modelu ve vyrovnávací paměti do databáze

Pokud chcete ukládat data binárního souboru do databáze pomocí Entity Frameworku, definujte Byte vlastnost pole u entity:

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

Zadejte vlastnost modelu stránky pro třídu, která obsahuje :IFormFile

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Poznámka

IFormFile lze použít přímo jako parametr metody akce nebo jako vlastnost vázaného modelu. Předchozí příklad používá vlastnost vázaného modelu.

Používá se FileUpload ve formuláři Razor Stránky:

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

Když je formulář poSTed na server, zkopírujte IFormFile ho do datového proudu a uložte ho jako pole bajtů v databázi. V následujícím příkladu _dbContext uloží kontext databáze aplikace:

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

Předchozí příklad je podobný scénáři popsanému v ukázkové aplikaci:

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

Upozorňující

Při ukládání binárních dat v relačních databázích buďte opatrní, protože to může nepříznivě ovlivnit výkon.

Nespoléhejte na vlastnost IFormFile bez ověření ani ji nedůvěřujteFileName. Vlastnost FileName by měla být použita pouze pro účely zobrazení a pouze po kódování HTML.

Uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:

Nahrávání velkých souborů se streamováním

Následující příklad ukazuje, jak pomocí JavaScriptu streamovat soubor do akce kontroleru. Antiforgery token souboru se vygeneruje pomocí atributu vlastního filtru a předá se hlavičkám HTTP klienta místo v textu požadavku. Vzhledem k tomu, že metoda akce zpracovává nahraná data přímo, je vazba modelu formuláře zakázána jiným vlastním filtrem. Obsah formuláře se v rámci akce čte pomocí MultipartReadersouboru, který čte jednotlivé soubory MultipartSectionnebo ukládá obsah podle potřeby. Po přečtení oddílů s více částmi provede akce vlastní vazbu modelu.

Počáteční odpověď stránky načte formulář a uloží antiforgery token do objektu cookie (prostřednictvím atributu GenerateAntiforgeryTokenCookieAttribute ). Tento atribut používá k nastavení cookie tokenu požadavku integrovanou podporu antiforgery ASP.NET Core:

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

Slouží DisableFormValueModelBindingAttribute k zakázání vazby 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)
    {
    }
}

V ukázkové aplikaci GenerateAntiforgeryTokenCookieAttribute a DisableFormValueModelBindingAttribute použijí se jako filtry pro modely /StreamedSingleFileUploadDb stránkovací aplikace a /StreamedSingleFileUploadPhysical při Startup.ConfigureServices používání Razor konvencí Pages:

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

Vzhledem k tomu, že vazba modelu nečte formulář, parametry vázané z formuláře se neváže (dotaz, trasa a hlavička budou nadále fungovat). Metoda akce funguje přímo s Request vlastností. A MultipartReader slouží ke čtení jednotlivých oddílů. Data klíče/hodnoty jsou uložena KeyValueAccumulatorv souboru . Po přečtení oddílů s více částmi se obsah formuláře KeyValueAccumulator použije k vytvoření vazby dat formuláře na typ modelu.

Úplná StreamingController.UploadDatabase metoda streamování do databáze pomocí 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));
        }
    }
}

Úplná StreamingController.UploadPhysical metoda streamování do fyzického umístění:

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

V ukázkové aplikaci se kontroly ověření zpracovávají FileHelpers.ProcessStreamedFile.

Ověření

Třída ukázkové aplikace FileHelpers ukazuje několik kontrol ukládání do vyrovnávací paměti IFormFile a nahrání streamovaných souborů. Informace o zpracování IFormFile nahrávání souborů ve vyrovnávací paměti v ukázkové aplikaci najdete v ProcessFormFile metodě v Utilities/FileHelpers.cs souboru. Pokud chcete zpracovávat streamované soubory, podívejte se na metodu ProcessStreamedFile ve stejném souboru.

Upozorňující

Metody zpracování ověřování, které jsou ukázány v ukázkové aplikaci, nekontroluje obsah nahraných souborů. Ve většině produkčních scénářů se v souboru před zpřístupněním souboru uživatelům nebo jiným systémům používá rozhraní API pro kontrolu virů a malwaru.

I když ukázka tématu poskytuje funkční příklad technik ověřování, neimplementujte FileHelpers třídu v produkční aplikaci, pokud:

  • Plně porozumíte implementaci.
  • Upravte implementaci podle potřeby pro prostředí a specifikace aplikace.

Nikdy nerozlišují implementaci bezpečnostního kódu v aplikaci bez řešení těchto požadavků.

Ověření obsahu

K nahrání obsahu použijte rozhraní API pro kontrolu virů nebo malwaru třetí strany.

Skenování souborů je náročné na serverové prostředky ve scénářích s velkým objemem. Pokud se sníží výkon zpracování požadavků z důvodu prohledávání souborů, zvažte snížení zátěže skenovací práce do služby na pozadí, případně služby spuštěné na serveru, který se liší od serveru aplikace. Nahrané soubory se obvykle uchovávají v karanténě, dokud je antivirový skener na pozadí nekontroluje. Když soubor projde, soubor se přesune do normálního umístění úložiště souborů. Tyto kroky se obvykle provádějí ve spojení se záznamem databáze, který označuje stav kontroly souboru. Díky takovému přístupu zůstává aplikace a aplikační server zaměřeny na reakci na požadavky.

Ověření přípony souboru

Přípona nahraného souboru by se měla kontrolovat v seznamu povolených přípon. Příklad:

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
}

Ověření podpisu souboru

Podpis souboru je určen několika prvními bajty na začátku souboru. Tyto bajty lze použít k označení, jestli přípona odpovídá obsahu souboru. Ukázková aplikace kontroluje podpisy souborů u několika běžných typů souborů. V následujícím příkladu je podpis souboru pro obrázek JPEG zkontrolován proti souboru:

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

Chcete-li získat další podpisy souborů, použijte databázi podpisů souborů (výsledek vyhledávání Google) a oficiální specifikace souborů. Specifikace oficiálního souboru konzultace mohou zajistit, aby vybrané podpisy byly platné.

Zabezpečení názvu souboru

Nikdy nepoužívejte název souboru zadaného klientem k uložení souboru do fyzického úložiště. Vytvořte pro soubor bezpečný název souboru pomocí Path.GetRandomFileName nebo Path.GetTempFileName a vytvořte úplnou cestu (včetně názvu souboru) pro dočasné úložiště.

Razor automaticky kóduje hodnoty vlastností pro zobrazení. Následující kód je bezpečný pro použití:

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

RazorMimo text , vždy HtmlEncode název souboru obsahu z žádosti uživatele.

Mnoho implementací musí obsahovat kontrolu, že soubor existuje; v opačném případě se soubor přepíše souborem se stejným názvem. Zadejte další logiku, která vyhovuje specifikacím vaší aplikace.

Ověření velikosti

Omezte velikost nahraných souborů.

V ukázkové aplikaci je velikost souboru omezená na 2 MB (uvedené v bajtech). Limit se poskytuje prostřednictvím konfigurace ze appsettings.json souboru:

{
  "FileSizeLimit": 2097152
}

Vloží se FileSizeLimit do PageModel tříd:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Pokud velikost souboru překročí limit, soubor se odmítne:

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

Shoda s hodnotou atributu name s názvem parametru metody POST

V jinýchRazor formách, než jsou data formuláře POST nebo přímo používají JavaScript FormData , název zadaný v elementu formuláře nebo FormData se musí shodovat s názvem parametru v akci kontroleru.

V následujícím příkladu:

  • Při použití elementu <input>name je atribut nastaven na hodnotu battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Při použití FormData v JavaScriptu je název nastaven na hodnotu battlePlans:

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

Pro parametr metody jazyka C# použijte odpovídající název (battlePlans):

  • Pro metodu obslužné Razor rutiny stránky Pages s názvem Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Metoda akce kontroleru MVC POST:

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

Konfigurace serveru a aplikace

Omezení délky těla s více částmi

MultipartBodyLengthLimit nastaví limit pro délku každého vícedílného těla. Oddíly formuláře, které tento limit překračují, při analýze vyvolá výjimku InvalidDataException . Výchozí hodnota je 134 217 728 (128 MB). Přizpůsobení limitu pomocí nastavení vStartup.ConfigureServices:MultipartBodyLengthLimit

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

RequestFormLimitsAttribute slouží k nastavení MultipartBodyLengthLimit jedné stránky nebo akce.

Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices:

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

Razor V aplikaci Pages nebo aplikaci MVC použijte filtr na model stránky nebo metodu akce:

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

Kestrel maximální velikost textu požadavku

U aplikací hostovaných ve Kestrelvýchozím nastavení je maximální velikost textu požadavku 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit pomocí možnosti Serveru 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 slouží k nastavení MaxRequestBodySize pro jednu stránku nebo akci.

Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices:

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

Razor V aplikaci stránky nebo aplikaci MVC použijte filtr na třídu obslužné rutiny stránky nebo metodu akce:

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

Lze RequestSizeLimitAttribute také použít direktivu @attributeRazor :

@attribute [RequestSizeLimitAttribute(52428800)]

Další Kestrel limity

Jiné Kestrel limity se můžou vztahovat na aplikace hostované:Kestrel

IIS

Výchozí limit požadavků (maxAllowedContentLength) je 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit v web.config souboru. V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):

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

Nastavení maxAllowedContentLength platí jenom pro službu IIS. Další informace naleznete v tématu Omezení <requestLimits>požadavků .

Zvětšete maximální velikost textu požadavku HTTP nastavením v Startup.ConfigureServices.IISServerOptions.MaxRequestBodySize V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):

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

Další informace naleznete v tématu Hostitel ASP.NET Core ve Windows se službou IIS.

Odstranění potíží

Níže jsou uvedeny některé běžné problémy, ke kterým dochází při práci s nahráváním souborů a jejich možných řešení.

Chyba Nenalezena při nasazení na server služby IIS

Následující chyba značí, že nahraný soubor překračuje nakonfigurovanou délku obsahu serveru:

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

Další informace najdete v části IIS .

Chyba připojení

Chyba připojení a připojení k resetování serveru pravděpodobně značí, že nahraný soubor překračuje Kestrelmaximální velikost textu požadavku. Další informace najdete v Kestrel části Maximální velikost textu požadavku. Kestrel Limity připojení klienta mohou vyžadovat také úpravu.

Výjimka odkazu null se souborem IFormFile

Pokud kontroler přijímá nahrané soubory pomocí IFormFile , ale hodnota je null, potvrďte, že formulář HTML určuje enctype hodnotu multipart/form-data. Pokud tento atribut není nastaven na <form> elementu, nahraje se soubor a žádné vázané IFormFile argumenty jsou null. Ověřte také, že pojmenování nahrávání v datech formuláře odpovídá pojmenování aplikace.

Stream byl příliš dlouhý.

Příklady v tomto tématu se spoléhají na MemoryStream uložení obsahu nahraného souboru. Limit velikosti je MemoryStreamint.MaxValue. Pokud scénář nahrávání souborů aplikace vyžaduje uchovávání obsahu souborů větší než 50 MB, použijte alternativní přístup, který nespoléhá na jediný MemoryStream obsah nahraného souboru.

ASP.NET Core podporuje nahrávání jednoho nebo více souborů pomocí vazby modelu ve vyrovnávací paměti pro menší soubory a neuložení streamovaných souborů u větších souborů.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Bezpečnostní aspekty

Při poskytování uživatelům, kteří mají možnost nahrávat soubory na server, buďte opatrní. Útočníci se mohou pokusit:

Bezpečnostní kroky, které snižují pravděpodobnost úspěšného útoku:

  • Nahrajte soubory do vyhrazené oblasti pro nahrávání souborů, nejlépe na systémovou jednotku. Vyhrazené umístění usnadňuje ukládání bezpečnostních omezení u nahraných souborů. Zakázání oprávnění ke spuštění v umístění pro nahrání souboru.†
  • Neuchovávat nahrané soubory ve stejném adresářovém stromu jako app.†
  • Použijte bezpečný název souboru určený aplikací. Nepoužívejte název souboru zadaný uživatelem ani nedůvěryhodným názvem souboru nahraného souboru.† HTML kódujte při jeho zobrazení nedůvěryhodný název souboru. Například protokolování názvu souboru nebo zobrazení v uživatelském rozhraní (Razor automaticky kóduje výstup HTML).
  • Povolit pouze schválené přípony souborů pro specifikaci návrhu aplikace.†
  • Ověřte, zda jsou kontroly na straně klienta provedeny na server.† Kontroly na straně klienta jsou snadné obejít.
  • Zkontrolujte velikost nahraného souboru. Nastavení maximálního limitu velikosti, aby se zabránilo velkým nahráváním.†
  • Pokud by soubory neměly být přepsány nahraným souborem se stejným názvem, před nahráním souboru zkontrolujte název souboru v databázi nebo fyzickém úložišti.
  • Před uložením souboru spusťte skener virů nebo malwaru na nahraný obsah.

†Vzoreková aplikace ukazuje přístup, který splňuje kritéria.

Upozorňující

Nahrání škodlivého kódu do systému je často prvním krokem ke spuštění kódu, který může:

  • Zcela získejte kontrolu nad systémem.
  • Přetěžte systém výsledkem chybového ukončení systému.
  • Ohrožení uživatelských nebo systémových dat
  • Použití graffiti na veřejné uživatelské rozhraní

Informace o omezení prostoru pro útok při přijímání souborů od uživatelů najdete v následujících zdrojích informací:

Další informace o implementaci bezpečnostních opatření, včetně příkladů z ukázkové aplikace, najdete v části Ověření .

Scénáře úložiště

Mezi běžné možnosti úložiště pro soubory patří:

  • Databáze

    • U malých nahrávek souborů je databáze často rychlejší než možnosti fyzického úložiště (systému souborů nebo síťové sdílené složky).
    • Databáze je často pohodlnější než možnosti fyzického úložiště, protože načtení záznamu databáze pro uživatelská data může současně poskytnout obsah souboru (například obrázek avataru).
    • Databáze je potenciálně levnější než použití služby úložiště dat.
  • Fyzické úložiště (systém souborů nebo síťová sdílená složka)

    • Pro velké nahrávání souborů:
      • Limity databáze můžou omezit velikost nahrávání.
      • Fyzické úložiště je často méně ekonomické než úložiště v databázi.
    • Fyzické úložiště je potenciálně levnější než použití služby úložiště dat.
    • Proces aplikace musí mít oprávnění ke čtení a zápisu do umístění úložiště. Nikdy neudělte oprávnění ke spuštění.
  • Služba úložiště dat (například Azure Blob Storage)

    • Služby obvykle nabízejí lepší škálovatelnost a odolnost před místními řešeními, která obvykle podléhají kritickým bodům selhání.
    • Služby jsou potenciálně nižší náklady ve scénářích infrastruktury velkých úložišť.

    Další informace najdete v tématu Rychlý start: Použití .NET k vytvoření objektu blob v úložišti objektů. Téma ukazuje UploadFromFileAsync, ale UploadFromStreamAsync lze použít k uložení FileStream do úložiště objektů blob při práci s Stream.

Scénáře nahrávání souborů

Dva obecné přístupy pro nahrávání souborů jsou ukládání do vyrovnávací paměti a streamování.

Vyrovnávací paměti

Celý soubor se načte do IFormFilesouboru, což je reprezentace souboru jazyka C#, který se používá ke zpracování nebo uložení souboru.

Prostředky (disk, paměť) používané nahráváním souborů závisí na počtu a velikosti souběžných nahrávání souborů. Pokud se aplikace pokusí uložit příliš mnoho nahrávek do vyrovnávací paměti, dojde k chybovému ukončení webu při nedostatku paměti nebo místa na disku. Pokud velikost nebo frekvence nahrávání souborů vyčerpává prostředky aplikace, použijte streamování.

Poznámka

Všechny soubory s jednou vyrovnávací pamětí větší než 64 kB se přesunou z paměti do dočasného souboru na disku.

Ukládání malých souborů do vyrovnávací paměti je popsáno v následujících částech tohoto tématu:

Streamování

Soubor se přijímá z žádosti o více částí a aplikace ho přímo zpracuje nebo uloží. Streamování výrazně nezlepší výkon. Streamování snižuje požadavky na paměť nebo místo na disku při nahrávání souborů.

Streamování velkých souborů je popsáno v části Nahrání velkých souborů pomocí oddílu streamování .

Nahrání malých souborů s vazbou modelu do vyrovnávací paměti do fyzického úložiště

Pokud chcete nahrát malé soubory, použijte formulář s více částmi nebo vytvořte požadavek POST pomocí JavaScriptu.

Následující příklad ukazuje použití Razor formuláře Pages k nahrání jednoho souboru (Pages/BufferedSingleFileUploadPhysical.cshtml v ukázkové aplikaci):

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

Následující příklad je podobný předchozímu příkladu s tím rozdílem, že:

  • JavaScript (Fetch API) slouží k odeslání dat formuláře.
  • Neexistuje žádné ověření.
<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>

K provedení formuláře POST v JavaScriptu pro klienty, kteří nepodporují rozhraní Fetch API, použijte jeden z následujících přístupů:

  • Použijte funkci Fetch Polyfill (například window.fetch polyfill (github/fetch)).

  • Použijte XMLHttpRequest. Příklad:

    <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 bylo možné podporovat nahrávání souborů, musí formuláře HTML určovat typ kódování (enctype) .multipart/form-data

files Vstupní prvek pro podporu nahrávání více souborů poskytuje multiple atribut elementu<input>:

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

Jednotlivé soubory nahrané na server lze získat přístup prostřednictvím vazby modelu pomocí IFormFile. Ukázková aplikace ukazuje několik nahrávání souborů do vyrovnávací paměti pro scénáře databáze a fyzického úložiště.

Upozorňující

FileName Nepoužívejte vlastnost jiné než IFormFile pro zobrazení a protokolování. Při zobrazení nebo protokolování kóduje kód HTML název souboru. Útočník může poskytnout škodlivý název souboru, včetně úplných cest nebo relativních cest. Aplikace by měly:

  • Odeberte cestu ze souboru zadaného uživatelem.
  • Uložte název souboru s kódováním HTML nebo odebranou cestou pro uživatelské rozhraní nebo protokolování.
  • Vygenerujte nový náhodný název souboru pro úložiště.

Následující kód odebere cestu z názvu souboru:

string untrustedFileName = Path.GetFileName(pathName);

Zatím uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:

Při nahrávání souborů pomocí vazby modelu a IFormFilemetoda akce může přijmout:

Poznámka

Vazba odpovídá souborům formuláře podle názvu. Například hodnota HTML name musí <input type="file" name="formFile"> odpovídat vázanémuFormFile parametru nebo vlastnosti jazyka C#. Další informace naleznete v části Match name attribute value to parameter name name of POST method section.

Následující příklad:

  • Smyčky procházejí jedním nebo více nahranými soubory.
  • Pomocí Path.GetTempFileName vrátí úplnou cestu k souboru, včetně názvu souboru.
  • Uloží soubory do místního systému souborů pomocí názvu souboru vygenerovaného aplikací.
  • Vrátí celkový počet a velikost nahraných souborů.
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 });
}

Slouží Path.GetRandomFileName k vygenerování názvu souboru bez cesty. V následujícím příkladu se cesta získá z konfigurace:

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

Cesta předaná do souboru FileStreammusí obsahovat název souboru. Pokud není zadaný název souboru, UnauthorizedAccessException vyvolá se za běhu.

Soubory nahrané pomocí IFormFile techniky se před zpracováním ukládají do vyrovnávací paměti nebo na disku na serveru. Uvnitř metody IFormFile akce je obsah přístupný jako Stream. Kromě místního systému souborů je možné soubory ukládat do sdílené síťové složky nebo do služby úložiště souborů, jako je azure Blob Storage.

Další příklad, který pro nahrání smyčuje více souborů a používá bezpečné názvy souborů, najdete Pages/BufferedMultipleFileUploadPhysical.cshtml.cs v ukázkové aplikaci.

Upozorňující

Path.GetTempFileName vyvolá chybu IOException , pokud se vytvoří více než 65 535 souborů bez odstranění předchozích dočasných souborů. Limit 65 535 souborů je limit pro jednotlivé servery. Další informace o tomto limitu operačního systému Windows najdete v poznámkách v následujících tématech:

Nahrání malých souborů s vazbou modelu ve vyrovnávací paměti do databáze

Pokud chcete ukládat data binárního souboru do databáze pomocí Entity Frameworku, definujte Byte vlastnost pole u entity:

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

Zadejte vlastnost modelu stránky pro třídu, která obsahuje :IFormFile

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Poznámka

IFormFile lze použít přímo jako parametr metody akce nebo jako vlastnost vázaného modelu. Předchozí příklad používá vlastnost vázaného modelu.

Používá se FileUpload ve formuláři Razor Stránky:

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

Když je formulář poSTed na server, zkopírujte IFormFile ho do datového proudu a uložte ho jako pole bajtů v databázi. V následujícím příkladu _dbContext uloží kontext databáze aplikace:

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

Předchozí příklad je podobný scénáři popsanému v ukázkové aplikaci:

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

Upozorňující

Při ukládání binárních dat v relačních databázích buďte opatrní, protože to může nepříznivě ovlivnit výkon.

Nespoléhejte na vlastnost IFormFile bez ověření ani ji nedůvěřujteFileName. Vlastnost FileName by měla být použita pouze pro účely zobrazení a pouze po kódování HTML.

Uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:

Nahrávání velkých souborů se streamováním

Následující příklad ukazuje, jak pomocí JavaScriptu streamovat soubor do akce kontroleru. Antiforgery token souboru se vygeneruje pomocí atributu vlastního filtru a předá se hlavičkám HTTP klienta místo v textu požadavku. Vzhledem k tomu, že metoda akce zpracovává nahraná data přímo, je vazba modelu formuláře zakázána jiným vlastním filtrem. Obsah formuláře se v rámci akce čte pomocí MultipartReadersouboru, který čte jednotlivé soubory MultipartSectionnebo ukládá obsah podle potřeby. Po přečtení oddílů s více částmi provede akce vlastní vazbu modelu.

Počáteční odpověď stránky načte formulář a uloží antiforgery token do objektu cookie (prostřednictvím atributu GenerateAntiforgeryTokenCookieAttribute ). Tento atribut používá k nastavení cookie tokenu požadavku integrovanou podporu antiforgery ASP.NET Core:

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

Slouží DisableFormValueModelBindingAttribute k zakázání vazby 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)
    {
    }
}

V ukázkové aplikaci GenerateAntiforgeryTokenCookieAttribute a DisableFormValueModelBindingAttribute použijí se jako filtry pro modely /StreamedSingleFileUploadDb stránkovací aplikace a /StreamedSingleFileUploadPhysical při Startup.ConfigureServices používání Razor konvencí Pages:

services.AddMvc()
    .AddRazorPagesOptions(options =>
        {
            options.Conventions
                .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
                    model =>
                    {
                        model.Filters.Add(
                            new GenerateAntiforgeryTokenCookieAttribute());
                        model.Filters.Add(
                            new DisableFormValueModelBindingAttribute());
                    });
            options.Conventions
                .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
                    model =>
                    {
                        model.Filters.Add(
                            new GenerateAntiforgeryTokenCookieAttribute());
                        model.Filters.Add(
                            new DisableFormValueModelBindingAttribute());
                    });
        })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Vzhledem k tomu, že vazba modelu nečte formulář, parametry vázané z formuláře se neváže (dotaz, trasa a hlavička budou nadále fungovat). Metoda akce funguje přímo s Request vlastností. A MultipartReader slouží ke čtení jednotlivých oddílů. Data klíče/hodnoty jsou uložena KeyValueAccumulatorv souboru . Po přečtení oddílů s více částmi se obsah formuláře KeyValueAccumulator použije k vytvoření vazby dat formuláře na typ modelu.

Úplná StreamingController.UploadDatabase metoda streamování do databáze pomocí 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));
        }
    }
}

Úplná StreamingController.UploadPhysical metoda streamování do fyzického umístění:

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

V ukázkové aplikaci se kontroly ověření zpracovávají FileHelpers.ProcessStreamedFile.

Ověření

Třída ukázkové aplikace FileHelpers ukazuje několik kontrol ukládání do vyrovnávací paměti IFormFile a nahrání streamovaných souborů. Informace o zpracování IFormFile nahrávání souborů ve vyrovnávací paměti v ukázkové aplikaci najdete v ProcessFormFile metodě v Utilities/FileHelpers.cs souboru. Pokud chcete zpracovávat streamované soubory, podívejte se na metodu ProcessStreamedFile ve stejném souboru.

Upozorňující

Metody zpracování ověřování, které jsou ukázány v ukázkové aplikaci, nekontroluje obsah nahraných souborů. Ve většině produkčních scénářů se v souboru před zpřístupněním souboru uživatelům nebo jiným systémům používá rozhraní API pro kontrolu virů a malwaru.

I když ukázka tématu poskytuje funkční příklad technik ověřování, neimplementujte FileHelpers třídu v produkční aplikaci, pokud:

  • Plně porozumíte implementaci.
  • Upravte implementaci podle potřeby pro prostředí a specifikace aplikace.

Nikdy nerozlišují implementaci bezpečnostního kódu v aplikaci bez řešení těchto požadavků.

Ověření obsahu

K nahrání obsahu použijte rozhraní API pro kontrolu virů nebo malwaru třetí strany.

Skenování souborů je náročné na serverové prostředky ve scénářích s velkým objemem. Pokud se sníží výkon zpracování požadavků z důvodu prohledávání souborů, zvažte snížení zátěže skenovací práce do služby na pozadí, případně služby spuštěné na serveru, který se liší od serveru aplikace. Nahrané soubory se obvykle uchovávají v karanténě, dokud je antivirový skener na pozadí nekontroluje. Když soubor projde, soubor se přesune do normálního umístění úložiště souborů. Tyto kroky se obvykle provádějí ve spojení se záznamem databáze, který označuje stav kontroly souboru. Díky takovému přístupu zůstává aplikace a aplikační server zaměřeny na reakci na požadavky.

Ověření přípony souboru

Přípona nahraného souboru by se měla kontrolovat v seznamu povolených přípon. Příklad:

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
}

Ověření podpisu souboru

Podpis souboru je určen několika prvními bajty na začátku souboru. Tyto bajty lze použít k označení, jestli přípona odpovídá obsahu souboru. Ukázková aplikace kontroluje podpisy souborů u několika běžných typů souborů. V následujícím příkladu je podpis souboru pro obrázek JPEG zkontrolován proti souboru:

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

Chcete-li získat další podpisy souborů, použijte databázi podpisů souborů (výsledek vyhledávání Google) a oficiální specifikace souborů. Specifikace oficiálního souboru konzultace mohou zajistit, aby vybrané podpisy byly platné.

Zabezpečení názvu souboru

Nikdy nepoužívejte název souboru zadaného klientem k uložení souboru do fyzického úložiště. Vytvořte pro soubor bezpečný název souboru pomocí Path.GetRandomFileName nebo Path.GetTempFileName a vytvořte úplnou cestu (včetně názvu souboru) pro dočasné úložiště.

Razor automaticky kóduje hodnoty vlastností pro zobrazení. Následující kód je bezpečný pro použití:

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

RazorMimo text , vždy HtmlEncode název souboru obsahu z žádosti uživatele.

Mnoho implementací musí obsahovat kontrolu, že soubor existuje; v opačném případě se soubor přepíše souborem se stejným názvem. Zadejte další logiku, která vyhovuje specifikacím vaší aplikace.

Ověření velikosti

Omezte velikost nahraných souborů.

V ukázkové aplikaci je velikost souboru omezená na 2 MB (uvedené v bajtech). Limit se poskytuje prostřednictvím konfigurace ze appsettings.json souboru:

{
  "FileSizeLimit": 2097152
}

Vloží se FileSizeLimit do PageModel tříd:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Pokud velikost souboru překročí limit, soubor se odmítne:

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

Shoda s hodnotou atributu name s názvem parametru metody POST

V jinýchRazor formách, než jsou data formuláře POST nebo přímo používají JavaScript FormData , název zadaný v elementu formuláře nebo FormData se musí shodovat s názvem parametru v akci kontroleru.

V následujícím příkladu:

  • Při použití elementu <input>name je atribut nastaven na hodnotu battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Při použití FormData v JavaScriptu je název nastaven na hodnotu battlePlans:

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

Pro parametr metody jazyka C# použijte odpovídající název (battlePlans):

  • Pro metodu obslužné Razor rutiny stránky Pages s názvem Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Metoda akce kontroleru MVC POST:

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

Konfigurace serveru a aplikace

Omezení délky těla s více částmi

MultipartBodyLengthLimit nastaví limit pro délku každého vícedílného těla. Oddíly formuláře, které tento limit překračují, při analýze vyvolá výjimku InvalidDataException . Výchozí hodnota je 134 217 728 (128 MB). Přizpůsobení limitu pomocí nastavení vStartup.ConfigureServices:MultipartBodyLengthLimit

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

RequestFormLimitsAttribute slouží k nastavení MultipartBodyLengthLimit jedné stránky nebo akce.

Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices:

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

Razor V aplikaci Pages nebo aplikaci MVC použijte filtr na model stránky nebo metodu akce:

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

Kestrel maximální velikost textu požadavku

U aplikací hostovaných ve Kestrelvýchozím nastavení je maximální velikost textu požadavku 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit pomocí možnosti Serveru 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 slouží k nastavení MaxRequestBodySize pro jednu stránku nebo akci.

Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices:

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

Razor V aplikaci stránky nebo aplikaci MVC použijte filtr na třídu obslužné rutiny stránky nebo metodu akce:

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

Další Kestrel limity

Jiné Kestrel limity se můžou vztahovat na aplikace hostované:Kestrel

IIS

Výchozí limit požadavků (maxAllowedContentLength) je 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit v web.config souboru. V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):

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

Nastavení maxAllowedContentLength platí jenom pro službu IIS. Další informace naleznete v tématu Omezení <requestLimits>požadavků .

Zvětšete maximální velikost textu požadavku HTTP nastavením v Startup.ConfigureServices.IISServerOptions.MaxRequestBodySize V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):

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

Další informace naleznete v tématu Hostitel ASP.NET Core ve Windows se službou IIS.

Odstranění potíží

Níže jsou uvedeny některé běžné problémy, ke kterým dochází při práci s nahráváním souborů a jejich možných řešení.

Chyba Nenalezena při nasazení na server služby IIS

Následující chyba značí, že nahraný soubor překračuje nakonfigurovanou délku obsahu serveru:

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

Další informace najdete v části IIS .

Chyba připojení

Chyba připojení a připojení k resetování serveru pravděpodobně značí, že nahraný soubor překračuje Kestrelmaximální velikost textu požadavku. Další informace najdete v Kestrel části Maximální velikost textu požadavku. Kestrel Limity připojení klienta mohou vyžadovat také úpravu.

Výjimka odkazu null se souborem IFormFile

Pokud kontroler přijímá nahrané soubory pomocí IFormFile , ale hodnota je null, potvrďte, že formulář HTML určuje enctype hodnotu multipart/form-data. Pokud tento atribut není nastaven na <form> elementu, nahraje se soubor a žádné vázané IFormFile argumenty jsou null. Ověřte také, že pojmenování nahrávání v datech formuláře odpovídá pojmenování aplikace.

Stream byl příliš dlouhý.

Příklady v tomto tématu se spoléhají na MemoryStream uložení obsahu nahraného souboru. Limit velikosti je MemoryStreamint.MaxValue. Pokud scénář nahrávání souborů aplikace vyžaduje uchovávání obsahu souborů větší než 50 MB, použijte alternativní přístup, který nespoléhá na jediný MemoryStream obsah nahraného souboru.

Další prostředky