Condividi tramite


Caricare file in ASP.NET Core

Di Rutger Storm

ASP.NET Core supporta il caricamento di uno o più file usando l'associazione di modelli memorizzati nel buffer per file più piccoli e lo streaming senza buffer per file di dimensioni maggiori.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Considerazioni sulla sicurezza

Prestare attenzione quando si fornisce agli utenti la possibilità di caricare file in un server. Gli utenti malintenzionati possono tentare di:

I passaggi di sicurezza che riducono la probabilità di un attacco riuscito sono:

  • Caricare file in un'area di caricamento di file dedicata, preferibilmente in un'unità non di sistema. Una posizione dedicata semplifica l'imposizione di restrizioni di sicurezza per i file caricati. Disabilitare le autorizzazioni di esecuzione per il percorso di caricamento del file.†
  • Non rendere persistenti i file caricati nello stesso albero di directory dell'app.†
  • Usare un nome file sicuro determinato dall'app. Non usare un nome di file fornito dall'utente o dal nome file non attendibile del file caricato.† HTML codificare il nome file non attendibile durante la visualizzazione. Ad esempio, registrare il nome del file o visualizzare nell'interfaccia utente (Razor codifica automaticamente l'output HTML).
  • Consenti solo estensioni di file approvate per la specifica di progettazione dell'app.†
  • Verificare che i controlli lato client vengano eseguiti sul server.† controlli lato client sono facili da aggirare.
  • Controllare le dimensioni di un file caricato. Impostare un limite massimo di dimensioni per impedire caricamenti di grandi dimensioni.†
  • Quando i file non devono essere sovrascritti da un file caricato con lo stesso nome, controllare il nome del file sul database o sull'archiviazione fisica prima di caricare il file.
  • Eseguire uno scanner antivirus/malware sul contenuto caricato prima che il file venga archiviato.

†L'app di esempio illustra un approccio che soddisfi i criteri.

Avviso

Il caricamento di codice dannoso in un sistema è spesso il primo passaggio per l'esecuzione di codice in grado di:

  • Acquisisci completamente il controllo di un sistema.
  • Eseguire l'overload di un sistema con il risultato che il sistema si arresta in modo anomalo.
  • Compromettere i dati di sistemi o utenti.
  • Applicare graffiti a un'interfaccia utente pubblica.

Per informazioni sulla riduzione della superficie di attacco quando si accettano file dagli utenti, vedere le risorse seguenti:

Per altre informazioni sull'implementazione di misure di sicurezza, inclusi esempi dell'app di esempio, vedere la sezione Convalida .

Scenari di archiviazione

Le opzioni di archiviazione comuni per i file includono:

  • Database

    • Per i caricamenti di file di piccole dimensioni , un database è spesso più veloce rispetto alle opzioni di archiviazione fisica (file system o condivisione di rete).
    • Un database è spesso più pratico delle opzioni di archiviazione fisica perché il recupero di un record di database per i dati utente può fornire simultaneamente il contenuto del file ,ad esempio un'immagine avatar.
    • Un database è potenzialmente meno costoso rispetto all'uso di un servizio di archiviazione dati cloud.
  • Archiviazione fisica (file system o condivisione di rete)

    • Per caricamenti di file di grandi dimensioni:
      • I limiti del database possono limitare le dimensioni del caricamento.
      • L'archiviazione fisica è spesso meno economica rispetto all'archiviazione in un database.
    • L'archiviazione fisica è potenzialmente meno costosa rispetto all'uso di un servizio di archiviazione dati cloud.
    • Il processo dell'app deve disporre delle autorizzazioni di lettura e scrittura per il percorso di archiviazione. Non concedere mai l'autorizzazione di esecuzione.
  • Il servizio di archiviazione dei dati cloud, ad esempio, Archiviazione BLOB di Azure.

    • I servizi offrono in genere una maggiore scalabilità e resilienza rispetto alle soluzioni locali che in genere sono soggette a singoli punti di errore.
    • I servizi sono potenzialmente più bassi negli scenari di infrastruttura di archiviazione di grandi dimensioni.

    Per altre informazioni, vedere Guida introduttiva: Usare .NET per creare un BLOB nell'archivio oggetti.

File di piccole e grandi dimensioni

La definizione di file di piccole e grandi dimensioni dipende dalle risorse di calcolo disponibili. Le app devono eseguire il benchmark dell'approccio di archiviazione usato per assicurarsi che possa gestire le dimensioni previste. Benchmark delle prestazioni di memoria, CPU, disco e database.

Anche se non è possibile specificare limiti specifici su ciò che è piccolo o grande per la distribuzione, ecco alcune delle impostazioni predefinite correlate di AspNetCore per FormOptions:

  • Per impostazione predefinita, HttpRequest.Form non memorizza nel buffer l'intero corpo della richiesta (BufferBody), ma memorizza nel buffer tutti i file di modulo multipart inclusi.
  • MultipartBodyLengthLimit è la dimensione massima per i file di modulo memorizzati nel buffer, il valore predefinito è 128 MB.
  • MemoryBufferThreshold indica quanto memorizzare nel buffer i file in memoria prima della transizione a un file di buffer su disco, il valore predefinito è 64 KB. MemoryBufferThreshold funge da limite tra file di piccole e grandi dimensioni che vengono generati o abbassati a seconda delle risorse e degli scenari delle app.

Per altre informazioni su FormOptions, vedere il codice sorgente.

Scenari di caricamento di file

Due approcci generali per il caricamento dei file sono il buffering e lo streaming.

Buffer

L'intero file viene letto in un oggetto IFormFile. IFormFile è una rappresentazione C# del file usato per elaborare o salvare il file.

Il disco e la memoria usati dai caricamenti di file dipendono dal numero e dalle dimensioni dei caricamenti simultanei dei file. Se un'app tenta di memorizzare nel buffer troppi caricamenti, il sito si arresta in modo anomalo quando esaurisce la memoria o lo spazio su disco. Se le dimensioni o la frequenza di caricamento dei file esauriscono le risorse dell'app, usare lo streaming.

Qualsiasi singolo file memorizzato nel buffer superiore a 64 KB viene spostato dalla memoria a un file temporaneo su disco.

I file temporanei per le richieste di dimensioni maggiori vengono scritti nel percorso denominato nella ASPNETCORE_TEMP variabile di ambiente. Se ASPNETCORE_TEMP non è definito, i file vengono scritti nella cartella temporanea dell'utente corrente.

Il buffering di file di piccole dimensioni è illustrato nelle sezioni seguenti di questo argomento:

Streaming

Il file viene ricevuto da una richiesta multipart e elaborato o salvato direttamente dall'app. Lo streaming non migliora significativamente le prestazioni. Lo streaming riduce le richieste di memoria o spazio su disco durante il caricamento dei file.

Lo streaming di file di grandi dimensioni è illustrato nella sezione Caricare file di grandi dimensioni con lo streaming .

Caricare file di piccole dimensioni con associazione di modelli memorizzati nel buffer nell'archiviazione fisica

Per caricare file di piccole dimensioni, usare un modulo multipart o costruire una richiesta POST usando JavaScript.

L'esempio seguente illustra l'uso di un Razor modulo Pages per caricare un singolo file (Pages/BufferedSingleFileUploadPhysical.cshtml nell'app di esempio):

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

L'esempio seguente è analogo all'esempio precedente, ad eccezione del fatto che:

  • L'API Fetch (Fetch) di JavaScript viene usata per inviare i dati del modulo.
  • Non esiste alcuna convalida.
<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>

Per eseguire il modulo POST in JavaScript per i client che non supportano l'API Fetch, usare uno degli approcci seguenti:

  • Usare un polyfill fetch (ad esempio, window.fetch polyfill (github/fetch)).

  • Usare XMLHttpRequest. Ad esempio:

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

Per supportare i caricamenti di file, i moduli HTML devono specificare un tipo di codifica (enctype) di multipart/form-data.

Affinché un files elemento di input supporti il caricamento di più file, specificare l'attributo multiple nell'elemento <input> :

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

È possibile accedere ai singoli file caricati nel server tramite l'associazione di modelli tramite IFormFile. L'app di esempio illustra più caricamenti di file memorizzati nel buffer per scenari di archiviazione fisica e di database.

Avviso

Non utilizzare la FileName proprietà di IFormFile diverso da per la visualizzazione e la registrazione. Durante la visualizzazione o la registrazione, il codice HTML codifica il nome del file. Un utente malintenzionato può fornire un nome file dannoso, inclusi percorsi completi o percorsi relativi. Le applicazioni devono:

  • Rimuovere il percorso dal nome file fornito dall'utente.
  • Salvare il nome file con codifica HTML, rimosso dal percorso per l'interfaccia utente o la registrazione.
  • Generare un nuovo nome file casuale per l'archiviazione.

Il codice seguente rimuove il percorso dal nome file:

string untrustedFileName = Path.GetFileName(pathName);

Gli esempi forniti finora non prendono in considerazione le considerazioni sulla sicurezza. Per altre informazioni, vedere le sezioni seguenti e l'app di esempio:

Quando si caricano file usando l'associazione di modelli e IFormFile, il metodo di azione può accettare:

Nota

L'associazione corrisponde ai file di modulo in base al nome. Ad esempio, il valore HTML name in <input type="file" name="formFile"> deve corrispondere al parametro/proprietà C# associato (FormFile). Per altre informazioni, vedere la sezione Match name attribute value to parameter name of parameter name of POST method .For more information, see the Match name attribute value to parameter name name of parameter name of method section.

L'esempio seguente:

  • Scorre uno o più file caricati.
  • Usa Path.GetTempFileName per restituire un percorso completo per un file, incluso il nome del file.
  • Salva i file nel file system locale usando un nome di file generato dall'app.
  • Restituisce il numero totale e le dimensioni dei file caricati.
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 });
}

Usare Path.GetRandomFileName per generare un nome di file senza un percorso. Nell'esempio seguente il percorso viene ottenuto dalla configurazione:

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

Il percorso passato a FileStream deve includere il nome del file. Se il nome file non viene specificato, viene generata un'eccezione UnauthorizedAccessException in fase di esecuzione.

I file caricati usando la IFormFile tecnica vengono memorizzati nel buffer in memoria o su disco nel server prima dell'elaborazione. All'interno del metodo di azione, il IFormFile contenuto è accessibile come .Stream Oltre al file system locale, i file possono essere salvati in una condivisione di rete o in un servizio di archiviazione file, ad esempio Archiviazione BLOB di Azure.

Per un altro esempio che esegue un ciclo su più file per il caricamento e usa nomi di file sicuri, vedi Pages/BufferedMultipleFileUploadPhysical.cshtml.cs nell'app di esempio.

Avviso

Path.GetTempFileName genera un'eccezione IOException se vengono creati più di 65.535 file senza eliminare i file temporanei precedenti. Il limite di 65.535 file è un limite per server. Per altre informazioni su questo limite per il sistema operativo Windows, vedere le osservazioni negli argomenti seguenti:

Caricare file di piccole dimensioni con associazione di modelli memorizzati nel buffer in un database

Per archiviare i dati di file binari in un database usando Entity Framework, definire una Byte proprietà di matrice nell'entità:

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

Specificare una proprietà del modello di pagina per la classe che include un oggetto IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Nota

IFormFile può essere usato direttamente come parametro del metodo di azione o come proprietà del modello associato. Nell'esempio precedente viene utilizzata una proprietà del modello associato.

L'oggetto FileUpload Razor viene utilizzato nel formato Pages:

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

Quando il modulo è POSTed nel server, copiare l'oggetto IFormFile in un flusso e salvarlo come matrice di byte nel database. Nell'esempio seguente viene _dbContext archiviato il contesto del database dell'app:

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

L'esempio precedente è simile a uno scenario illustrato nell'app di esempio:

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

Avviso

Quando si archiviano dati binari all'interno di database relazionali, è necessario prestare attenzione, perché l'operazione può influire negativamente sulle prestazioni.

Non basarsi né considerare attendibile la FileName proprietà di IFormFile senza convalida. La FileName proprietà deve essere utilizzata solo a scopo di visualizzazione e solo dopo la codifica HTML.

Gli esempi forniti non prendono in considerazione le considerazioni sulla sicurezza. Per altre informazioni, vedere le sezioni seguenti e l'app di esempio:

Caricare file di grandi dimensioni con streaming

L'esempio 3.1 illustra come usare JavaScript per trasmettere un file a un'azione del controller. Il token antiforgery del file viene generato usando un attributo di filtro personalizzato e passato alle intestazioni HTTP del client anziché nel corpo della richiesta. Poiché il metodo di azione elabora direttamente i dati caricati, l'associazione del modello di modulo viene disabilitata da un altro filtro personalizzato. All'interno dell'azione, il contenuto del form viene letto tramite un MultipartReader, che legge ogni singola MultipartSection, elaborando il file o archiviandone il contenuto, come appropriato. Dopo aver letto le sezioni multipart, l'azione esegue il proprio binding di modelli.

La risposta della pagina iniziale carica il modulo e salva un token antiforgery in un cookie (tramite l'attributo GenerateAntiforgeryTokenCookieAttribute ). L'attributo usa il supporto antiforgery predefinito di ASP.NET Core per impostare un cookie oggetto con un token di richiesta:

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

Viene usato per disabilitare l'associazione DisableFormValueModelBindingAttribute di modelli:

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

Nell'app GenerateAntiforgeryTokenCookieAttribute di esempio e DisableFormValueModelBindingAttribute vengono applicati come filtri ai modelli di applicazione di pagine di /StreamedSingleFileUploadDb e /StreamedSingleFileUploadPhysical usando Startup.ConfigureServicesRazor le convenzioni 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());
            });
});

Poiché l'associazione di modelli non legge il modulo, i parametri associati dal modulo non vengono associati (query, route e intestazione continuano a funzionare). Il metodo di azione funziona direttamente con la Request proprietà . Per leggere ogni sezione, viene usato un MultipartReader. I dati chiave/valore vengono archiviati in un oggetto KeyValueAccumulator. Dopo la lettura delle sezioni multipart, il contenuto di KeyValueAccumulator viene usato per associare i dati del modulo a un tipo di modello.

Metodo completo StreamingController.UploadDatabase per lo streaming in un database con EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

Metodo completo StreamingController.UploadPhysical per lo streaming in una posizione fisica:

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

Nell'app di esempio i controlli di convalida vengono gestiti da FileHelpers.ProcessStreamedFile.

Convalida

La classe dell'app di FileHelpers esempio illustra diversi controlli per i caricamenti di file memorizzati nel IFormFile buffer e trasmessi. Per l'elaborazione IFormFile dei caricamenti di file memorizzati nel buffer nell'app di esempio, vedere il ProcessFormFile metodo nel Utilities/FileHelpers.cs file. Per l'elaborazione di file trasmessi, vedere il ProcessStreamedFile metodo nello stesso file.

Avviso

I metodi di elaborazione della convalida illustrati nell'app di esempio non analizzano il contenuto dei file caricati. Nella maggior parte degli scenari di produzione viene usata un'API antivirus/malware scanner nel file prima di rendere disponibile il file agli utenti o ad altri sistemi.

Sebbene l'esempio di argomento fornisca un esempio funzionante di tecniche di convalida, non implementare la FileHelpers classe in un'app di produzione, a meno che non si:

  • Comprendere appieno l'implementazione.
  • Modificare l'implementazione in base alle specifiche e all'ambiente dell'app.

Non implementare mai in modo indiscriminato il codice di sicurezza in un'app senza soddisfare questi requisiti.

Convalida del contenuto

Usare un'API di analisi di virus/malware di terze parti sul contenuto caricato.

L'analisi dei file richiede risorse server in scenari con volumi elevati. Se le prestazioni di elaborazione delle richieste sono diminuite a causa dell'analisi dei file, è consigliabile eseguire l'offload del lavoro di analisi in un servizio in background, possibilmente un servizio in esecuzione in un server diverso dal server dell'app. In genere, i file caricati vengono mantenuti in un'area in quarantena fino a quando lo scanner antivirus in background li controlla. Quando un file passa, il file viene spostato nel percorso di archiviazione file normale. Questi passaggi vengono in genere eseguiti insieme a un record di database che indica lo stato di analisi di un file. Usando un approccio di questo tipo, l'app e il server dell'app rimangono concentrati sulla risposta alle richieste.

Convalida dell'estensione file

L'estensione del file caricato deve essere verificata in base a un elenco di estensioni consentite. Ad esempio:

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
}

Convalida della firma file

La firma di un file è determinata dai primi byte all'inizio di un file. Questi byte possono essere usati per indicare se l'estensione corrisponde al contenuto del file. L'app di esempio controlla le firme dei file per alcuni tipi di file comuni. Nell'esempio seguente viene verificata la firma del file per un'immagine JPEG sul file :

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

Per ottenere firme di file aggiuntive, usare un database di firme di file (risultato della ricerca Google) e specifiche ufficiali dei file. La consulenza delle specifiche ufficiali dei file può garantire che le firme selezionate siano valide.

Sicurezza dei nomi file

Non usare mai un nome di file fornito dal client per salvare un file nell'archiviazione fisica. Creare un nome file sicuro per il file usando Path.GetRandomFileName o Path.GetTempFileName per creare un percorso completo (incluso il nome file) per l'archiviazione temporanea.

Razor codifica automaticamente i valori delle proprietà per la visualizzazione. Il codice seguente è sicuro da usare:

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

Al di fuori di , sempre HtmlEncode il contenuto del Razornome file dalla richiesta di un utente.

Molte implementazioni devono includere un controllo dell'esistenza del file; in caso contrario, il file viene sovrascritto da un file con lo stesso nome. Fornire logica aggiuntiva per soddisfare le specifiche dell'app.

Convalida delle dimensioni

Limitare le dimensioni dei file caricati.

Nell'app di esempio le dimensioni del file sono limitate a 2 MB (indicate in byte). Il limite viene fornito tramite la configurazione dal appsettings.json file :

{
  "FileSizeLimit": 2097152
}

L'oggetto FileSizeLimit viene inserito nelle PageModel classi:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Quando una dimensione del file supera il limite, il file viene rifiutato:

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

Trova la corrispondenza del valore dell'attributo name al nome del parametro del metodo POST

Razor In moduli diversi dai dati del modulo POST o direttamente da JavaScriptFormData, il nome specificato nell'elemento del modulo o FormData deve corrispondere al nome del parametro nell'azione del controller.

Nell'esempio seguente :

  • Quando si usa un <input> elemento, l'attributo name viene impostato sul valore battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Quando si usa FormData in JavaScript, il nome viene impostato sul valore battlePlans:

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

Usare un nome corrispondente per il parametro del metodo C# (battlePlans):

  • Per un Razor metodo del gestore di pagine Pages denominato Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Per un metodo di azione del controller POST MVC:

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

Configurazione del server e dell'app

Limite di lunghezza del corpo multipart

MultipartBodyLengthLimit imposta il limite per la lunghezza di ogni corpo multipart. Le sezioni del modulo che superano questo limite generano un'eccezione durante l'analisi InvalidDataException . Il valore predefinito è 134.217.728 (128 MB). Personalizzare il limite usando l'impostazione MultipartBodyLengthLimit in Startup.ConfigureServices:

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

RequestFormLimitsAttribute viene utilizzato per impostare per MultipartBodyLengthLimit una singola pagina o azione.

In un'app Razor Pages applicare il filtro con una convenzione in Startup.ConfigureServices:

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

In un'app Pages o in un'app Razor MVC applicare il filtro al modello di pagina o al metodo di azione:

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

Kestrel dimensioni massime del corpo della richiesta

Per le app ospitate da Kestrel, la dimensione massima predefinita del corpo della richiesta è di 30.000.000 byte, ovvero circa 28,6 MB. Personalizzare il limite usando l'opzione server 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 viene usato per impostare MaxRequestBodySize per una singola pagina o azione.

In un'app Razor Pages applicare il filtro con una convenzione in Startup.ConfigureServices:

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

In un'app Razor pages o in un'app MVC applicare il filtro alla classe o al metodo action del gestore di pagine:

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

L'oggetto RequestSizeLimitAttribute può essere applicato anche usando la @attributeRazor direttiva :

@attribute [RequestSizeLimitAttribute(52428800)]

Altri Kestrel limiti

Altri Kestrel limiti possono essere applicati per le app ospitate da Kestrel:

IIS

Il limite di richieste predefinito (maxAllowedContentLength) è 30.000.000 byte, ovvero circa 28,6 MB. Personalizzare il limite nel web.config file. Nell'esempio seguente il limite è impostato su 50 MB (52.428.800 byte):

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

L'impostazione maxAllowedContentLength si applica solo a IIS. Per altre informazioni, vedere Limiti delle <requestLimits>richieste.

Risoluzione dei problemi

Di seguito sono trattati alcuni problemi comuni riscontrati durante il caricamento di file, con le soluzioni possibili corrispondenti.

Errore Non trovato durante la distribuzione in un server IIS

L'errore seguente indica che il file caricato supera la lunghezza del contenuto configurato del server:

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

Per altre informazioni, vedere la sezione IIS .

Errore di connessione

Un errore di connessione e una connessione al server di reimpostazione indica probabilmente che il file caricato supera le Kestreldimensioni massime del corpo della richiesta. Per altre informazioni, vedere la sezione dimensioni massime del corpo della Kestrel richiesta. Kestrel i limiti di connessione client possono richiedere anche una regolazione.

Eccezione per riferimento Null con IFormFile

Se il controller accetta i file caricati usando IFormFile ma il valore è null, verificare che il modulo HTML specifichi un enctype valore di multipart/form-data. Se questo attributo non è impostato sull'elemento <form> , il caricamento del file non si verifica e gli argomenti associati IFormFile sono null. Verificare anche che la denominazione di caricamento nei dati del modulo corrisponda alla denominazione dell'app.

Stream era troppo lungo

Gli esempi in questo argomento si basano su MemoryStream per contenere il contenuto del file caricato. Il limite di dimensioni di un MemoryStream oggetto è int.MaxValue. Se lo scenario di caricamento dei file dell'app richiede di contenere contenuto di file di dimensioni superiori a 50 MB, usare un approccio alternativo che non si basa su un singolo MemoryStream file per contenere il contenuto di un file caricato.

ASP.NET Core supporta il caricamento di uno o più file usando l'associazione di modelli memorizzati nel buffer per file più piccoli e lo streaming senza buffer per file di dimensioni maggiori.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Considerazioni sulla sicurezza

Prestare attenzione quando si fornisce agli utenti la possibilità di caricare file in un server. Gli utenti malintenzionati possono tentare di:

I passaggi di sicurezza che riducono la probabilità di un attacco riuscito sono:

  • Caricare file in un'area di caricamento di file dedicata, preferibilmente in un'unità non di sistema. Una posizione dedicata semplifica l'imposizione di restrizioni di sicurezza per i file caricati. Disabilitare le autorizzazioni di esecuzione per il percorso di caricamento del file.†
  • Non rendere persistenti i file caricati nello stesso albero di directory dell'app.†
  • Usare un nome file sicuro determinato dall'app. Non usare un nome di file fornito dall'utente o dal nome file non attendibile del file caricato.† HTML codificare il nome file non attendibile durante la visualizzazione. Ad esempio, registrare il nome del file o visualizzare nell'interfaccia utente (Razor codifica automaticamente l'output HTML).
  • Consenti solo estensioni di file approvate per la specifica di progettazione dell'app.†
  • Verificare che i controlli lato client vengano eseguiti sul server.† controlli lato client sono facili da aggirare.
  • Controllare le dimensioni di un file caricato. Impostare un limite massimo di dimensioni per impedire caricamenti di grandi dimensioni.†
  • Quando i file non devono essere sovrascritti da un file caricato con lo stesso nome, controllare il nome del file sul database o sull'archiviazione fisica prima di caricare il file.
  • Eseguire uno scanner antivirus/malware sul contenuto caricato prima che il file venga archiviato.

†L'app di esempio illustra un approccio che soddisfi i criteri.

Avviso

Il caricamento di codice dannoso in un sistema è spesso il primo passaggio per l'esecuzione di codice in grado di:

  • Acquisisci completamente il controllo di un sistema.
  • Eseguire l'overload di un sistema con il risultato che il sistema si arresta in modo anomalo.
  • Compromettere i dati di sistemi o utenti.
  • Applicare graffiti a un'interfaccia utente pubblica.

Per informazioni sulla riduzione della superficie di attacco quando si accettano file dagli utenti, vedere le risorse seguenti:

Per altre informazioni sull'implementazione di misure di sicurezza, inclusi esempi dell'app di esempio, vedere la sezione Convalida .

Scenari di archiviazione

Le opzioni di archiviazione comuni per i file includono:

  • Database

    • Per i caricamenti di file di piccole dimensioni, un database è spesso più veloce rispetto alle opzioni di archiviazione fisica (file system o condivisione di rete).
    • Un database è spesso più pratico delle opzioni di archiviazione fisica perché il recupero di un record di database per i dati utente può fornire simultaneamente il contenuto del file ,ad esempio un'immagine avatar.
    • Un database è potenzialmente meno costoso rispetto all'uso di un servizio di archiviazione dati.
  • Archiviazione fisica (file system o condivisione di rete)

    • Per caricamenti di file di grandi dimensioni:
      • I limiti del database possono limitare le dimensioni del caricamento.
      • L'archiviazione fisica è spesso meno economica rispetto all'archiviazione in un database.
    • L'archiviazione fisica è potenzialmente meno costosa rispetto all'uso di un servizio di archiviazione dati.
    • Il processo dell'app deve disporre delle autorizzazioni di lettura e scrittura per il percorso di archiviazione. Non concedere mai l'autorizzazione di esecuzione.
  • Servizio di archiviazione dati (ad esempio, Archiviazione BLOB di Azure)

    • I servizi offrono in genere una maggiore scalabilità e resilienza rispetto alle soluzioni locali che in genere sono soggette a singoli punti di errore.
    • I servizi sono potenzialmente più bassi negli scenari di infrastruttura di archiviazione di grandi dimensioni.

    Per altre informazioni, vedere Guida introduttiva: Usare .NET per creare un BLOB nell'archivio oggetti.

Scenari di caricamento di file

Due approcci generali per il caricamento dei file sono il buffering e lo streaming.

Buffer

L'intero file viene letto in un IFormFileoggetto , ovvero una rappresentazione C# del file usato per elaborare o salvare il file.

Le risorse (disco, memoria) usate dai caricamenti di file dipendono dal numero e dalle dimensioni dei caricamenti simultanei dei file. Se un'app tenta di memorizzare nel buffer troppi caricamenti, il sito si arresta in modo anomalo quando esaurisce la memoria o lo spazio su disco. Se le dimensioni o la frequenza di caricamento dei file esauriscono le risorse dell'app, usare lo streaming.

Nota

Qualsiasi singolo file memorizzato nel buffer superiore a 64 KB viene spostato dalla memoria a un file temporaneo su disco.

Il buffering di file di piccole dimensioni è illustrato nelle sezioni seguenti di questo argomento:

Streaming

Il file viene ricevuto da una richiesta multipart e elaborato o salvato direttamente dall'app. Lo streaming non migliora significativamente le prestazioni. Lo streaming riduce le richieste di memoria o spazio su disco durante il caricamento dei file.

Lo streaming di file di grandi dimensioni è illustrato nella sezione Caricare file di grandi dimensioni con lo streaming .

Caricare file di piccole dimensioni con associazione di modelli memorizzati nel buffer nell'archiviazione fisica

Per caricare file di piccole dimensioni, usare un modulo multipart o costruire una richiesta POST usando JavaScript.

L'esempio seguente illustra l'uso di un Razor modulo Pages per caricare un singolo file (Pages/BufferedSingleFileUploadPhysical.cshtml nell'app di esempio):

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

L'esempio seguente è analogo all'esempio precedente, ad eccezione del fatto che:

  • L'API Fetch (Fetch) di JavaScript viene usata per inviare i dati del modulo.
  • Non esiste alcuna convalida.
<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>

Per eseguire il modulo POST in JavaScript per i client che non supportano l'API Fetch, usare uno degli approcci seguenti:

  • Usare un polyfill fetch (ad esempio, window.fetch polyfill (github/fetch)).

  • Usare XMLHttpRequest. Ad esempio:

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

Per supportare i caricamenti di file, i moduli HTML devono specificare un tipo di codifica (enctype) di multipart/form-data.

Affinché un files elemento di input supporti il caricamento di più file, specificare l'attributo multiple nell'elemento <input> :

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

È possibile accedere ai singoli file caricati nel server tramite l'associazione di modelli tramite IFormFile. L'app di esempio illustra più caricamenti di file memorizzati nel buffer per scenari di archiviazione fisica e di database.

Avviso

Non utilizzare la FileName proprietà di IFormFile diverso da per la visualizzazione e la registrazione. Durante la visualizzazione o la registrazione, il codice HTML codifica il nome del file. Un utente malintenzionato può fornire un nome file dannoso, inclusi percorsi completi o percorsi relativi. Le applicazioni devono:

  • Rimuovere il percorso dal nome file fornito dall'utente.
  • Salvare il nome file con codifica HTML, rimosso dal percorso per l'interfaccia utente o la registrazione.
  • Generare un nuovo nome file casuale per l'archiviazione.

Il codice seguente rimuove il percorso dal nome file:

string untrustedFileName = Path.GetFileName(pathName);

Gli esempi forniti finora non prendono in considerazione le considerazioni sulla sicurezza. Per altre informazioni, vedere le sezioni seguenti e l'app di esempio:

Quando si caricano file usando l'associazione di modelli e IFormFile, il metodo di azione può accettare:

Nota

L'associazione corrisponde ai file di modulo in base al nome. Ad esempio, il valore HTML name in <input type="file" name="formFile"> deve corrispondere al parametro/proprietà C# associato (FormFile). Per altre informazioni, vedere la sezione Match name attribute value to parameter name of parameter name of POST method .For more information, see the Match name attribute value to parameter name name of parameter name of method section.

L'esempio seguente:

  • Scorre uno o più file caricati.
  • Usa Path.GetTempFileName per restituire un percorso completo per un file, incluso il nome del file.
  • Salva i file nel file system locale usando un nome di file generato dall'app.
  • Restituisce il numero totale e le dimensioni dei file caricati.
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 });
}

Usare Path.GetRandomFileName per generare un nome di file senza un percorso. Nell'esempio seguente il percorso viene ottenuto dalla configurazione:

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

Il percorso passato a FileStream deve includere il nome del file. Se il nome file non viene specificato, viene generata un'eccezione UnauthorizedAccessException in fase di esecuzione.

I file caricati usando la IFormFile tecnica vengono memorizzati nel buffer in memoria o su disco nel server prima dell'elaborazione. All'interno del metodo di azione, il IFormFile contenuto è accessibile come .Stream Oltre al file system locale, i file possono essere salvati in una condivisione di rete o in un servizio di archiviazione file, ad esempio Archiviazione BLOB di Azure.

Per un altro esempio che esegue un ciclo su più file per il caricamento e usa nomi di file sicuri, vedi Pages/BufferedMultipleFileUploadPhysical.cshtml.cs nell'app di esempio.

Avviso

Path.GetTempFileName genera un'eccezione IOException se vengono creati più di 65.535 file senza eliminare i file temporanei precedenti. Il limite di 65.535 file è un limite per server. Per altre informazioni su questo limite per il sistema operativo Windows, vedere le osservazioni negli argomenti seguenti:

Caricare file di piccole dimensioni con associazione di modelli memorizzati nel buffer in un database

Per archiviare i dati di file binari in un database usando Entity Framework, definire una Byte proprietà di matrice nell'entità:

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

Specificare una proprietà del modello di pagina per la classe che include un oggetto IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Nota

IFormFile può essere usato direttamente come parametro del metodo di azione o come proprietà del modello associato. Nell'esempio precedente viene utilizzata una proprietà del modello associato.

L'oggetto FileUpload Razor viene utilizzato nel formato Pages:

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

Quando il modulo è POSTed nel server, copiare l'oggetto IFormFile in un flusso e salvarlo come matrice di byte nel database. Nell'esempio seguente viene _dbContext archiviato il contesto del database dell'app:

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

L'esempio precedente è simile a uno scenario illustrato nell'app di esempio:

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

Avviso

Quando si archiviano dati binari all'interno di database relazionali, è necessario prestare attenzione, perché l'operazione può influire negativamente sulle prestazioni.

Non basarsi né considerare attendibile la FileName proprietà di IFormFile senza convalida. La FileName proprietà deve essere utilizzata solo a scopo di visualizzazione e solo dopo la codifica HTML.

Gli esempi forniti non prendono in considerazione le considerazioni sulla sicurezza. Per altre informazioni, vedere le sezioni seguenti e l'app di esempio:

Caricare file di grandi dimensioni con streaming

L'esempio seguente illustra come usare JavaScript per trasmettere un file a un'azione del controller. Il token antiforgery del file viene generato usando un attributo di filtro personalizzato e passato alle intestazioni HTTP del client anziché nel corpo della richiesta. Poiché il metodo di azione elabora direttamente i dati caricati, l'associazione del modello di modulo viene disabilitata da un altro filtro personalizzato. All'interno dell'azione, il contenuto del form viene letto tramite un MultipartReader, che legge ogni singola MultipartSection, elaborando il file o archiviandone il contenuto, come appropriato. Dopo aver letto le sezioni multipart, l'azione esegue il proprio binding di modelli.

La risposta della pagina iniziale carica il modulo e salva un token antiforgery in un cookie (tramite l'attributo GenerateAntiforgeryTokenCookieAttribute ). L'attributo usa il supporto antiforgery predefinito di ASP.NET Core per impostare un cookie oggetto con un token di richiesta:

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

Viene usato per disabilitare l'associazione DisableFormValueModelBindingAttribute di modelli:

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

Nell'app GenerateAntiforgeryTokenCookieAttribute di esempio e DisableFormValueModelBindingAttribute vengono applicati come filtri ai modelli di applicazione di pagine di /StreamedSingleFileUploadDb e /StreamedSingleFileUploadPhysical usando Startup.ConfigureServicesRazor le convenzioni 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());
            });
});

Poiché l'associazione di modelli non legge il modulo, i parametri associati dal modulo non vengono associati (query, route e intestazione continuano a funzionare). Il metodo di azione funziona direttamente con la Request proprietà . Per leggere ogni sezione, viene usato un MultipartReader. I dati chiave/valore vengono archiviati in un oggetto KeyValueAccumulator. Dopo la lettura delle sezioni multipart, il contenuto di KeyValueAccumulator viene usato per associare i dati del modulo a un tipo di modello.

Metodo completo StreamingController.UploadDatabase per lo streaming in un database con EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

Metodo completo StreamingController.UploadPhysical per lo streaming in una posizione fisica:

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

Nell'app di esempio i controlli di convalida vengono gestiti da FileHelpers.ProcessStreamedFile.

Convalida

La classe dell'app di FileHelpers esempio illustra diversi controlli per i caricamenti di file memorizzati nel IFormFile buffer e trasmessi. Per l'elaborazione IFormFile dei caricamenti di file memorizzati nel buffer nell'app di esempio, vedere il ProcessFormFile metodo nel Utilities/FileHelpers.cs file. Per l'elaborazione di file trasmessi, vedere il ProcessStreamedFile metodo nello stesso file.

Avviso

I metodi di elaborazione della convalida illustrati nell'app di esempio non analizzano il contenuto dei file caricati. Nella maggior parte degli scenari di produzione viene usata un'API antivirus/malware scanner nel file prima di rendere disponibile il file agli utenti o ad altri sistemi.

Sebbene l'esempio di argomento fornisca un esempio funzionante di tecniche di convalida, non implementare la FileHelpers classe in un'app di produzione, a meno che non si:

  • Comprendere appieno l'implementazione.
  • Modificare l'implementazione in base alle specifiche e all'ambiente dell'app.

Non implementare mai in modo indiscriminato il codice di sicurezza in un'app senza soddisfare questi requisiti.

Convalida del contenuto

Usare un'API di analisi di virus/malware di terze parti sul contenuto caricato.

L'analisi dei file richiede risorse server in scenari con volumi elevati. Se le prestazioni di elaborazione delle richieste sono diminuite a causa dell'analisi dei file, è consigliabile eseguire l'offload del lavoro di analisi in un servizio in background, possibilmente un servizio in esecuzione in un server diverso dal server dell'app. In genere, i file caricati vengono mantenuti in un'area in quarantena fino a quando lo scanner antivirus in background li controlla. Quando un file passa, il file viene spostato nel percorso di archiviazione file normale. Questi passaggi vengono in genere eseguiti insieme a un record di database che indica lo stato di analisi di un file. Usando un approccio di questo tipo, l'app e il server dell'app rimangono concentrati sulla risposta alle richieste.

Convalida dell'estensione file

L'estensione del file caricato deve essere verificata in base a un elenco di estensioni consentite. Ad esempio:

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
}

Convalida della firma file

La firma di un file è determinata dai primi byte all'inizio di un file. Questi byte possono essere usati per indicare se l'estensione corrisponde al contenuto del file. L'app di esempio controlla le firme dei file per alcuni tipi di file comuni. Nell'esempio seguente viene verificata la firma del file per un'immagine JPEG sul file :

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

Per ottenere firme di file aggiuntive, usare un database di firme di file (risultato della ricerca Google) e specifiche ufficiali dei file. La consulenza delle specifiche ufficiali dei file può garantire che le firme selezionate siano valide.

Sicurezza dei nomi file

Non usare mai un nome di file fornito dal client per salvare un file nell'archiviazione fisica. Creare un nome file sicuro per il file usando Path.GetRandomFileName o Path.GetTempFileName per creare un percorso completo (incluso il nome file) per l'archiviazione temporanea.

Razor codifica automaticamente i valori delle proprietà per la visualizzazione. Il codice seguente è sicuro da usare:

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

Al di fuori di , sempre HtmlEncode il contenuto del Razornome file dalla richiesta di un utente.

Molte implementazioni devono includere un controllo dell'esistenza del file; in caso contrario, il file viene sovrascritto da un file con lo stesso nome. Fornire logica aggiuntiva per soddisfare le specifiche dell'app.

Convalida delle dimensioni

Limitare le dimensioni dei file caricati.

Nell'app di esempio le dimensioni del file sono limitate a 2 MB (indicate in byte). Il limite viene fornito tramite la configurazione dal appsettings.json file :

{
  "FileSizeLimit": 2097152
}

L'oggetto FileSizeLimit viene inserito nelle PageModel classi:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Quando una dimensione del file supera il limite, il file viene rifiutato:

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

Trova la corrispondenza del valore dell'attributo name al nome del parametro del metodo POST

Razor In moduli diversi dai dati del modulo POST o direttamente da JavaScriptFormData, il nome specificato nell'elemento del modulo o FormData deve corrispondere al nome del parametro nell'azione del controller.

Nell'esempio seguente :

  • Quando si usa un <input> elemento, l'attributo name viene impostato sul valore battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Quando si usa FormData in JavaScript, il nome viene impostato sul valore battlePlans:

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

Usare un nome corrispondente per il parametro del metodo C# (battlePlans):

  • Per un Razor metodo del gestore di pagine Pages denominato Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Per un metodo di azione del controller POST MVC:

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

Configurazione del server e dell'app

Limite di lunghezza del corpo multipart

MultipartBodyLengthLimit imposta il limite per la lunghezza di ogni corpo multipart. Le sezioni del modulo che superano questo limite generano un'eccezione durante l'analisi InvalidDataException . Il valore predefinito è 134.217.728 (128 MB). Personalizzare il limite usando l'impostazione MultipartBodyLengthLimit in Startup.ConfigureServices:

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

RequestFormLimitsAttribute viene utilizzato per impostare per MultipartBodyLengthLimit una singola pagina o azione.

In un'app Razor Pages applicare il filtro con una convenzione in Startup.ConfigureServices:

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

In un'app Pages o in un'app Razor MVC applicare il filtro al modello di pagina o al metodo di azione:

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

Kestrel dimensioni massime del corpo della richiesta

Per le app ospitate da Kestrel, la dimensione massima predefinita del corpo della richiesta è di 30.000.000 byte, ovvero circa 28,6 MB. Personalizzare il limite usando l'opzione server 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 viene usato per impostare MaxRequestBodySize per una singola pagina o azione.

In un'app Razor Pages applicare il filtro con una convenzione in Startup.ConfigureServices:

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

In un'app Razor pages o in un'app MVC applicare il filtro alla classe o al metodo action del gestore di pagine:

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

L'oggetto RequestSizeLimitAttribute può essere applicato anche usando la @attributeRazor direttiva :

@attribute [RequestSizeLimitAttribute(52428800)]

Altri Kestrel limiti

Altri Kestrel limiti possono essere applicati per le app ospitate da Kestrel:

IIS

Il limite di richieste predefinito (maxAllowedContentLength) è 30.000.000 byte, ovvero circa 28,6 MB. Personalizzare il limite nel web.config file. Nell'esempio seguente il limite è impostato su 50 MB (52.428.800 byte):

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

L'impostazione maxAllowedContentLength si applica solo a IIS. Per altre informazioni, vedere Limiti delle <requestLimits>richieste.

Aumentare le dimensioni massime del corpo della richiesta HTTP impostando IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices. Nell'esempio seguente il limite è impostato su 50 MB (52.428.800 byte):

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

Per altre informazioni, vedere Host ASP.NET Core on Windows with IIS (Host ASP.NET Core in Windows con IIS).

Risoluzione dei problemi

Di seguito sono trattati alcuni problemi comuni riscontrati durante il caricamento di file, con le soluzioni possibili corrispondenti.

Errore Non trovato durante la distribuzione in un server IIS

L'errore seguente indica che il file caricato supera la lunghezza del contenuto configurato del server:

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

Per altre informazioni, vedere la sezione IIS .

Errore di connessione

Un errore di connessione e una connessione al server di reimpostazione indica probabilmente che il file caricato supera le Kestreldimensioni massime del corpo della richiesta. Per altre informazioni, vedere la sezione dimensioni massime del corpo della Kestrel richiesta. Kestrel i limiti di connessione client possono richiedere anche una regolazione.

Eccezione per riferimento Null con IFormFile

Se il controller accetta i file caricati usando IFormFile ma il valore è null, verificare che il modulo HTML specifichi un enctype valore di multipart/form-data. Se questo attributo non è impostato sull'elemento <form> , il caricamento del file non si verifica e gli argomenti associati IFormFile sono null. Verificare anche che la denominazione di caricamento nei dati del modulo corrisponda alla denominazione dell'app.

Stream era troppo lungo

Gli esempi in questo argomento si basano su MemoryStream per contenere il contenuto del file caricato. Il limite di dimensioni di un MemoryStream oggetto è int.MaxValue. Se lo scenario di caricamento dei file dell'app richiede di contenere contenuto di file di dimensioni superiori a 50 MB, usare un approccio alternativo che non si basa su un singolo MemoryStream file per contenere il contenuto di un file caricato.

ASP.NET Core supporta il caricamento di uno o più file usando l'associazione di modelli memorizzati nel buffer per file più piccoli e lo streaming senza buffer per file di dimensioni maggiori.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Considerazioni sulla sicurezza

Prestare attenzione quando si fornisce agli utenti la possibilità di caricare file in un server. Gli utenti malintenzionati possono tentare di:

I passaggi di sicurezza che riducono la probabilità di un attacco riuscito sono:

  • Caricare file in un'area di caricamento di file dedicata, preferibilmente in un'unità non di sistema. Una posizione dedicata semplifica l'imposizione di restrizioni di sicurezza per i file caricati. Disabilitare le autorizzazioni di esecuzione per il percorso di caricamento del file.†
  • Non rendere persistenti i file caricati nello stesso albero di directory dell'app.†
  • Usare un nome file sicuro determinato dall'app. Non usare un nome di file fornito dall'utente o dal nome file non attendibile del file caricato.† HTML codificare il nome file non attendibile durante la visualizzazione. Ad esempio, registrare il nome del file o visualizzare nell'interfaccia utente (Razor codifica automaticamente l'output HTML).
  • Consenti solo estensioni di file approvate per la specifica di progettazione dell'app.†
  • Verificare che i controlli lato client vengano eseguiti sul server.† controlli lato client sono facili da aggirare.
  • Controllare le dimensioni di un file caricato. Impostare un limite massimo di dimensioni per impedire caricamenti di grandi dimensioni.†
  • Quando i file non devono essere sovrascritti da un file caricato con lo stesso nome, controllare il nome del file sul database o sull'archiviazione fisica prima di caricare il file.
  • Eseguire uno scanner antivirus/malware sul contenuto caricato prima che il file venga archiviato.

†L'app di esempio illustra un approccio che soddisfi i criteri.

Avviso

Il caricamento di codice dannoso in un sistema è spesso il primo passaggio per l'esecuzione di codice in grado di:

  • Acquisisci completamente il controllo di un sistema.
  • Eseguire l'overload di un sistema con il risultato che il sistema si arresta in modo anomalo.
  • Compromettere i dati di sistemi o utenti.
  • Applicare graffiti a un'interfaccia utente pubblica.

Per informazioni sulla riduzione della superficie di attacco quando si accettano file dagli utenti, vedere le risorse seguenti:

Per altre informazioni sull'implementazione di misure di sicurezza, inclusi esempi dell'app di esempio, vedere la sezione Convalida .

Scenari di archiviazione

Le opzioni di archiviazione comuni per i file includono:

  • Database

    • Per i caricamenti di file di piccole dimensioni, un database è spesso più veloce rispetto alle opzioni di archiviazione fisica (file system o condivisione di rete).
    • Un database è spesso più pratico delle opzioni di archiviazione fisica perché il recupero di un record di database per i dati utente può fornire simultaneamente il contenuto del file ,ad esempio un'immagine avatar.
    • Un database è potenzialmente meno costoso rispetto all'uso di un servizio di archiviazione dati.
  • Archiviazione fisica (file system o condivisione di rete)

    • Per caricamenti di file di grandi dimensioni:
      • I limiti del database possono limitare le dimensioni del caricamento.
      • L'archiviazione fisica è spesso meno economica rispetto all'archiviazione in un database.
    • L'archiviazione fisica è potenzialmente meno costosa rispetto all'uso di un servizio di archiviazione dati.
    • Il processo dell'app deve disporre delle autorizzazioni di lettura e scrittura per il percorso di archiviazione. Non concedere mai l'autorizzazione di esecuzione.
  • Servizio di archiviazione dati (ad esempio, Archiviazione BLOB di Azure)

    • I servizi offrono in genere una maggiore scalabilità e resilienza rispetto alle soluzioni locali che in genere sono soggette a singoli punti di errore.
    • I servizi sono potenzialmente più bassi negli scenari di infrastruttura di archiviazione di grandi dimensioni.

    Per altre informazioni, vedere Guida introduttiva: Usare .NET per creare un BLOB nell'archivio oggetti. L'argomento illustra UploadFromFileAsync, ma UploadFromStreamAsync può essere usato per salvare un FileStream oggetto nell'archivio BLOB quando si usa un oggetto Stream.

Scenari di caricamento di file

Due approcci generali per il caricamento dei file sono il buffering e lo streaming.

Buffer

L'intero file viene letto in un IFormFileoggetto , ovvero una rappresentazione C# del file usato per elaborare o salvare il file.

Le risorse (disco, memoria) usate dai caricamenti di file dipendono dal numero e dalle dimensioni dei caricamenti simultanei dei file. Se un'app tenta di memorizzare nel buffer troppi caricamenti, il sito si arresta in modo anomalo quando esaurisce la memoria o lo spazio su disco. Se le dimensioni o la frequenza di caricamento dei file esauriscono le risorse dell'app, usare lo streaming.

Nota

Qualsiasi singolo file memorizzato nel buffer superiore a 64 KB viene spostato dalla memoria a un file temporaneo su disco.

Il buffering di file di piccole dimensioni è illustrato nelle sezioni seguenti di questo argomento:

Streaming

Il file viene ricevuto da una richiesta multipart e elaborato o salvato direttamente dall'app. Lo streaming non migliora significativamente le prestazioni. Lo streaming riduce le richieste di memoria o spazio su disco durante il caricamento dei file.

Lo streaming di file di grandi dimensioni è illustrato nella sezione Caricare file di grandi dimensioni con lo streaming .

Caricare file di piccole dimensioni con associazione di modelli memorizzati nel buffer nell'archiviazione fisica

Per caricare file di piccole dimensioni, usare un modulo multipart o costruire una richiesta POST usando JavaScript.

L'esempio seguente illustra l'uso di un Razor modulo Pages per caricare un singolo file (Pages/BufferedSingleFileUploadPhysical.cshtml nell'app di esempio):

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

L'esempio seguente è analogo all'esempio precedente, ad eccezione del fatto che:

  • L'API Fetch (Fetch) di JavaScript viene usata per inviare i dati del modulo.
  • Non esiste alcuna convalida.
<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>

Per eseguire il modulo POST in JavaScript per i client che non supportano l'API Fetch, usare uno degli approcci seguenti:

  • Usare un polyfill fetch (ad esempio, window.fetch polyfill (github/fetch)).

  • Usare XMLHttpRequest. Ad esempio:

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

Per supportare i caricamenti di file, i moduli HTML devono specificare un tipo di codifica (enctype) di multipart/form-data.

Affinché un files elemento di input supporti il caricamento di più file, specificare l'attributo multiple nell'elemento <input> :

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

È possibile accedere ai singoli file caricati nel server tramite l'associazione di modelli tramite IFormFile. L'app di esempio illustra più caricamenti di file memorizzati nel buffer per scenari di archiviazione fisica e di database.

Avviso

Non utilizzare la FileName proprietà di IFormFile diverso da per la visualizzazione e la registrazione. Durante la visualizzazione o la registrazione, il codice HTML codifica il nome del file. Un utente malintenzionato può fornire un nome file dannoso, inclusi percorsi completi o percorsi relativi. Le applicazioni devono:

  • Rimuovere il percorso dal nome file fornito dall'utente.
  • Salvare il nome file con codifica HTML, rimosso dal percorso per l'interfaccia utente o la registrazione.
  • Generare un nuovo nome file casuale per l'archiviazione.

Il codice seguente rimuove il percorso dal nome file:

string untrustedFileName = Path.GetFileName(pathName);

Gli esempi forniti finora non prendono in considerazione le considerazioni sulla sicurezza. Per altre informazioni, vedere le sezioni seguenti e l'app di esempio:

Quando si caricano file usando l'associazione di modelli e IFormFile, il metodo di azione può accettare:

Nota

L'associazione corrisponde ai file di modulo in base al nome. Ad esempio, il valore HTML name in <input type="file" name="formFile"> deve corrispondere al parametro/proprietà C# associato (FormFile). Per altre informazioni, vedere la sezione Match name attribute value to parameter name of parameter name of POST method .For more information, see the Match name attribute value to parameter name name of parameter name of method section.

L'esempio seguente:

  • Scorre uno o più file caricati.
  • Usa Path.GetTempFileName per restituire un percorso completo per un file, incluso il nome del file.
  • Salva i file nel file system locale usando un nome di file generato dall'app.
  • Restituisce il numero totale e le dimensioni dei file caricati.
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 });
}

Usare Path.GetRandomFileName per generare un nome di file senza un percorso. Nell'esempio seguente il percorso viene ottenuto dalla configurazione:

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

Il percorso passato a FileStream deve includere il nome del file. Se il nome file non viene specificato, viene generata un'eccezione UnauthorizedAccessException in fase di esecuzione.

I file caricati usando la IFormFile tecnica vengono memorizzati nel buffer in memoria o su disco nel server prima dell'elaborazione. All'interno del metodo di azione, il IFormFile contenuto è accessibile come .Stream Oltre al file system locale, i file possono essere salvati in una condivisione di rete o in un servizio di archiviazione file, ad esempio Archiviazione BLOB di Azure.

Per un altro esempio che esegue un ciclo su più file per il caricamento e usa nomi di file sicuri, vedi Pages/BufferedMultipleFileUploadPhysical.cshtml.cs nell'app di esempio.

Avviso

Path.GetTempFileName genera un'eccezione IOException se vengono creati più di 65.535 file senza eliminare i file temporanei precedenti. Il limite di 65.535 file è un limite per server. Per altre informazioni su questo limite per il sistema operativo Windows, vedere le osservazioni negli argomenti seguenti:

Caricare file di piccole dimensioni con associazione di modelli memorizzati nel buffer in un database

Per archiviare i dati di file binari in un database usando Entity Framework, definire una Byte proprietà di matrice nell'entità:

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

Specificare una proprietà del modello di pagina per la classe che include un oggetto IFormFile:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Nota

IFormFile può essere usato direttamente come parametro del metodo di azione o come proprietà del modello associato. Nell'esempio precedente viene utilizzata una proprietà del modello associato.

L'oggetto FileUpload Razor viene utilizzato nel formato Pages:

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

Quando il modulo è POSTed nel server, copiare l'oggetto IFormFile in un flusso e salvarlo come matrice di byte nel database. Nell'esempio seguente viene _dbContext archiviato il contesto del database dell'app:

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

L'esempio precedente è simile a uno scenario illustrato nell'app di esempio:

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

Avviso

Quando si archiviano dati binari all'interno di database relazionali, è necessario prestare attenzione, perché l'operazione può influire negativamente sulle prestazioni.

Non basarsi né considerare attendibile la FileName proprietà di IFormFile senza convalida. La FileName proprietà deve essere utilizzata solo a scopo di visualizzazione e solo dopo la codifica HTML.

Gli esempi forniti non prendono in considerazione le considerazioni sulla sicurezza. Per altre informazioni, vedere le sezioni seguenti e l'app di esempio:

Caricare file di grandi dimensioni con streaming

L'esempio seguente illustra come usare JavaScript per trasmettere un file a un'azione del controller. Il token antiforgery del file viene generato usando un attributo di filtro personalizzato e passato alle intestazioni HTTP del client anziché nel corpo della richiesta. Poiché il metodo di azione elabora direttamente i dati caricati, l'associazione del modello di modulo viene disabilitata da un altro filtro personalizzato. All'interno dell'azione, il contenuto del form viene letto tramite un MultipartReader, che legge ogni singola MultipartSection, elaborando il file o archiviandone il contenuto, come appropriato. Dopo aver letto le sezioni multipart, l'azione esegue il proprio binding di modelli.

La risposta della pagina iniziale carica il modulo e salva un token antiforgery in un cookie (tramite l'attributo GenerateAntiforgeryTokenCookieAttribute ). L'attributo usa il supporto antiforgery predefinito di ASP.NET Core per impostare un cookie oggetto con un token di richiesta:

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

Viene usato per disabilitare l'associazione DisableFormValueModelBindingAttribute di modelli:

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

Nell'app GenerateAntiforgeryTokenCookieAttribute di esempio e DisableFormValueModelBindingAttribute vengono applicati come filtri ai modelli di applicazione di pagine di /StreamedSingleFileUploadDb e /StreamedSingleFileUploadPhysical usando Startup.ConfigureServicesRazor le convenzioni 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);

Poiché l'associazione di modelli non legge il modulo, i parametri associati dal modulo non vengono associati (query, route e intestazione continuano a funzionare). Il metodo di azione funziona direttamente con la Request proprietà . Per leggere ogni sezione, viene usato un MultipartReader. I dati chiave/valore vengono archiviati in un oggetto KeyValueAccumulator. Dopo la lettura delle sezioni multipart, il contenuto di KeyValueAccumulator viene usato per associare i dati del modulo a un tipo di modello.

Metodo completo StreamingController.UploadDatabase per lo streaming in un database con EF Core:

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

        return BadRequest(ModelState);
    }

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

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

    var section = await reader.ReadNextSectionAsync();

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

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

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

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

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

                    return BadRequest(ModelState);
                }

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

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

                    formAccumulator.Append(key, value);

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

                        return BadRequest(ModelState);
                    }
                }
            }
        }

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

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

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

        return BadRequest(ModelState);
    }

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

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

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

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

MultipartRequestHelper (Utilities/MultipartRequestHelper.cs):

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

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

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

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

            return boundary;
        }

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

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

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

Metodo completo StreamingController.UploadPhysical per lo streaming in una posizione fisica:

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

Nell'app di esempio i controlli di convalida vengono gestiti da FileHelpers.ProcessStreamedFile.

Convalida

La classe dell'app di FileHelpers esempio illustra diversi controlli per i caricamenti di file memorizzati nel IFormFile buffer e trasmessi. Per l'elaborazione IFormFile dei caricamenti di file memorizzati nel buffer nell'app di esempio, vedere il ProcessFormFile metodo nel Utilities/FileHelpers.cs file. Per l'elaborazione di file trasmessi, vedere il ProcessStreamedFile metodo nello stesso file.

Avviso

I metodi di elaborazione della convalida illustrati nell'app di esempio non analizzano il contenuto dei file caricati. Nella maggior parte degli scenari di produzione viene usata un'API antivirus/malware scanner nel file prima di rendere disponibile il file agli utenti o ad altri sistemi.

Sebbene l'esempio di argomento fornisca un esempio funzionante di tecniche di convalida, non implementare la FileHelpers classe in un'app di produzione, a meno che non si:

  • Comprendere appieno l'implementazione.
  • Modificare l'implementazione in base alle specifiche e all'ambiente dell'app.

Non implementare mai in modo indiscriminato il codice di sicurezza in un'app senza soddisfare questi requisiti.

Convalida del contenuto

Usare un'API di analisi di virus/malware di terze parti sul contenuto caricato.

L'analisi dei file richiede risorse server in scenari con volumi elevati. Se le prestazioni di elaborazione delle richieste sono diminuite a causa dell'analisi dei file, è consigliabile eseguire l'offload del lavoro di analisi in un servizio in background, possibilmente un servizio in esecuzione in un server diverso dal server dell'app. In genere, i file caricati vengono mantenuti in un'area in quarantena fino a quando lo scanner antivirus in background li controlla. Quando un file passa, il file viene spostato nel percorso di archiviazione file normale. Questi passaggi vengono in genere eseguiti insieme a un record di database che indica lo stato di analisi di un file. Usando un approccio di questo tipo, l'app e il server dell'app rimangono concentrati sulla risposta alle richieste.

Convalida dell'estensione file

L'estensione del file caricato deve essere verificata in base a un elenco di estensioni consentite. Ad esempio:

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
}

Convalida della firma file

La firma di un file è determinata dai primi byte all'inizio di un file. Questi byte possono essere usati per indicare se l'estensione corrisponde al contenuto del file. L'app di esempio controlla le firme dei file per alcuni tipi di file comuni. Nell'esempio seguente viene verificata la firma del file per un'immagine JPEG sul file :

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

Per ottenere firme di file aggiuntive, usare un database di firme di file (risultato della ricerca Google) e specifiche ufficiali dei file. La consulenza delle specifiche ufficiali dei file può garantire che le firme selezionate siano valide.

Sicurezza dei nomi file

Non usare mai un nome di file fornito dal client per salvare un file nell'archiviazione fisica. Creare un nome file sicuro per il file usando Path.GetRandomFileName o Path.GetTempFileName per creare un percorso completo (incluso il nome file) per l'archiviazione temporanea.

Razor codifica automaticamente i valori delle proprietà per la visualizzazione. Il codice seguente è sicuro da usare:

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

Al di fuori di , sempre HtmlEncode il contenuto del Razornome file dalla richiesta di un utente.

Molte implementazioni devono includere un controllo dell'esistenza del file; in caso contrario, il file viene sovrascritto da un file con lo stesso nome. Fornire logica aggiuntiva per soddisfare le specifiche dell'app.

Convalida delle dimensioni

Limitare le dimensioni dei file caricati.

Nell'app di esempio le dimensioni del file sono limitate a 2 MB (indicate in byte). Il limite viene fornito tramite la configurazione dal appsettings.json file :

{
  "FileSizeLimit": 2097152
}

L'oggetto FileSizeLimit viene inserito nelle PageModel classi:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Quando una dimensione del file supera il limite, il file viene rifiutato:

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

Trova la corrispondenza del valore dell'attributo name al nome del parametro del metodo POST

Razor In moduli diversi dai dati del modulo POST o direttamente da JavaScriptFormData, il nome specificato nell'elemento del modulo o FormData deve corrispondere al nome del parametro nell'azione del controller.

Nell'esempio seguente :

  • Quando si usa un <input> elemento, l'attributo name viene impostato sul valore battlePlans:

    <input type="file" name="battlePlans" multiple>
    
  • Quando si usa FormData in JavaScript, il nome viene impostato sul valore battlePlans:

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

Usare un nome corrispondente per il parametro del metodo C# (battlePlans):

  • Per un Razor metodo del gestore di pagine Pages denominato Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Per un metodo di azione del controller POST MVC:

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

Configurazione del server e dell'app

Limite di lunghezza del corpo multipart

MultipartBodyLengthLimit imposta il limite per la lunghezza di ogni corpo multipart. Le sezioni del modulo che superano questo limite generano un'eccezione durante l'analisi InvalidDataException . Il valore predefinito è 134.217.728 (128 MB). Personalizzare il limite usando l'impostazione MultipartBodyLengthLimit in Startup.ConfigureServices:

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

RequestFormLimitsAttribute viene utilizzato per impostare per MultipartBodyLengthLimit una singola pagina o azione.

In un'app Razor Pages applicare il filtro con una convenzione in Startup.ConfigureServices:

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

In un'app Pages o in un'app Razor MVC applicare il filtro al modello di pagina o al metodo di azione:

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

Kestrel dimensioni massime del corpo della richiesta

Per le app ospitate da Kestrel, la dimensione massima predefinita del corpo della richiesta è di 30.000.000 byte, ovvero circa 28,6 MB. Personalizzare il limite usando l'opzione server 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 viene usato per impostare MaxRequestBodySize per una singola pagina o azione.

In un'app Razor Pages applicare il filtro con una convenzione in Startup.ConfigureServices:

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

In un'app Razor pages o in un'app MVC applicare il filtro alla classe o al metodo action del gestore di pagine:

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

Altri Kestrel limiti

Altri Kestrel limiti possono essere applicati per le app ospitate da Kestrel:

IIS

Il limite di richieste predefinito (maxAllowedContentLength) è 30.000.000 byte, ovvero circa 28,6 MB. Personalizzare il limite nel web.config file. Nell'esempio seguente il limite è impostato su 50 MB (52.428.800 byte):

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

L'impostazione maxAllowedContentLength si applica solo a IIS. Per altre informazioni, vedere Limiti delle <requestLimits>richieste.

Aumentare le dimensioni massime del corpo della richiesta HTTP impostando IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices. Nell'esempio seguente il limite è impostato su 50 MB (52.428.800 byte):

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

Per altre informazioni, vedere Host ASP.NET Core on Windows with IIS (Host ASP.NET Core in Windows con IIS).

Risoluzione dei problemi

Di seguito sono trattati alcuni problemi comuni riscontrati durante il caricamento di file, con le soluzioni possibili corrispondenti.

Errore Non trovato durante la distribuzione in un server IIS

L'errore seguente indica che il file caricato supera la lunghezza del contenuto configurato del server:

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

Per altre informazioni, vedere la sezione IIS .

Errore di connessione

Un errore di connessione e una connessione al server di reimpostazione indica probabilmente che il file caricato supera le Kestreldimensioni massime del corpo della richiesta. Per altre informazioni, vedere la sezione dimensioni massime del corpo della Kestrel richiesta. Kestrel i limiti di connessione client possono richiedere anche una regolazione.

Eccezione per riferimento Null con IFormFile

Se il controller accetta i file caricati usando IFormFile ma il valore è null, verificare che il modulo HTML specifichi un enctype valore di multipart/form-data. Se questo attributo non è impostato sull'elemento <form> , il caricamento del file non si verifica e gli argomenti associati IFormFile sono null. Verificare anche che la denominazione di caricamento nei dati del modulo corrisponda alla denominazione dell'app.

Stream era troppo lungo

Gli esempi in questo argomento si basano su MemoryStream per contenere il contenuto del file caricato. Il limite di dimensioni di un MemoryStream oggetto è int.MaxValue. Se lo scenario di caricamento dei file dell'app richiede di contenere contenuto di file di dimensioni superiori a 50 MB, usare un approccio alternativo che non si basa su un singolo MemoryStream file per contenere il contenuto di un file caricato.

Risorse aggiuntive