Teilen über


Hochladen von Dateien in ASP.NET Core

Von Rutger Storm

ASP.NET Core unterstützt das Hochladen einer oder mehrerer Dateien über die gepufferte Modellbindung für kleinere Dateien und ungepuffertes Streaming für größere Dateien.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Sicherheitsüberlegungen

Gehen Sie mit Bedacht vor, wenn Sie Benutzern die Möglichkeit geben, Dateien auf einen Server hochzuladen. Angreifer versuchen möglicherweise Folgendes:

  • Ausführen von Denial-of-Service-Angriffen
  • Hochladen von Viren oder Schadsoftware
  • Gefährden von Netzwerken und Servern auf andere Weise

Folgende Schritte können Sie dabei unterstützen, die Wahrscheinlichkeit eines erfolgreichen Angriffs zu verringern:

  • Laden Sie Dateien in einen dedizierten Bereich zum Hochladen von Dateien hoch, vorzugsweise auf ein Nicht-Systemlaufwerk. Ein dedizierter Speicherort erleichtert es, Sicherheitsbeschränkungen für hochgeladene Dateien zu erzwingen. Deaktivieren Sie Ausführungsberechtigungen für den Speicherort für hochgeladene Dateien.†
  • Speichern Sie hochgeladene Dateien nicht persistent in der Verzeichnisstruktur, in der sich auch die App befindet.†
  • Wählen Sie einen sicheren von der App festgelegten Dateinamen. Verwenden Sie keinen benutzerseitig angegebenen Dateinamen oder den nicht vertrauenswürdigen Dateinamen der hochgeladenen Datei.† Codieren Sie den nicht vertrauenswürdigen Dateinamen mit HTML, wenn er angezeigt wird. Beispiele dafür wären etwa die Protokollierung des Dateinamens oder die Anzeige auf der Benutzeroberfläche. (Razor codiert Ausgaben automatisch mit HTML.)
  • Lassen Sie nur genehmigte Dateierweiterungen für die Entwurfsspezifikation der App zu.†
  • Stellen Sie sicher, dass clientseitige Überprüfungen auf dem Server erfolgen.† Clientseitige Überprüfungen sind leicht zu umgehen.
  • Überprüfen Sie die Größe einer hochgeladenen Datei. Legen Sie einen Grenzwert für die maximale Größe fest, um große Uploads zu verhindern.†
  • Wenn Dateien nicht durch eine hochgeladene Datei mit demselben Namen überschrieben werden sollen, vergleichen Sie den Dateinamen mit der Datenbank oder dem physischen Speicher, bevor Sie die Datei hochladen.
  • Wenden Sie auf die hochgeladenen Inhalte einen Scanner auf Viren und Schadsoftware an, ehe die Datei gespeichert wird.

†Die Beispiel-App veranschaulicht einen Ansatz, der die Kriterien erfüllt.

Warnung

Das Hochladen von schädlichem Code auf ein System ist häufig der erste Schritt, um Code mit der folgenden Absicht auszuführen:

  • Erlangen der vollständigen Kontrolle über ein System.
  • Überlasten eines Systems mit dem Ziel eines Systemausfalls.
  • Kompromittieren von Benutzer- oder Systemdaten
  • Anwenden von Graffiti auf eine öffentliche Benutzeroberfläche.

Wie Sie die Angriffsoberfläche beim Akzeptieren von Dateien von Benutzenden reduzieren, erfahren Sie in den folgenden Artikeln:

Weitere Informationen zur Implementierung von Sicherheitsmaßnahmen, einschließlich Beispiele aus der Beispielanwendung, finden Sie im Abschnitt Validierung.

Speicherszenarien

Zu den allgemeinen Speicheroptionen für Dateien gehören u. a.:

  • Datenbank

    • Beim Hochladen kleiner Dateien ist eine Datenbank oft schneller als physische Speicheroptionen (Dateisystem oder Netzwerkfreigabe).
    • Eine Datenbank ist oft praktischer als physische Speicheroptionen, da das Abrufen eines Datenbank-Datensatzes für Benutzerdaten gleichzeitig den Dateiinhalt (z. B. ein Avatarbild) bereitstellen kann.
    • Eine Datenbank ist potenziell kostengünstiger als die Nutzung eines cloudbasierten Datenspeicherdiensts.
  • Physischer Speicher (Dateisystem oder Netzwerkfreigabe)

    • Für das Hochladen großer Dateien:
      • Für die Datenbank geltende Grenzwerte können die Größe des Uploads einschränken.
      • Physischer Speicher ist oft teurer als Datenbankspeicher.
    • Physischer Speicher ist potenziell teurer als die Nutzung eines cloudbasierten Datenspeicherdiensts.
    • Der Prozess der App muss Lese- und Schreibberechtigungen für den Speicherort haben. Erteilen Sie niemals die Ausführungsberechtigung.
  • Cloudbasierter Datenspeicherdienst (z. B. Azure Blob Storage).

    • Dienste bieten in der Regel eine bessere Skalierbarkeit und Resilienz gegenüber lokalen Lösungen, die in der Regel Single Points of Failure aufweisen.
    • Dienste sind bei Szenarien mit großen Speicherinfrastrukturen potenziell kostengünstiger.

    Weitere Informationen finden Sie unter Schnellstart: Erstellen eines Blobs im Objektspeicher mithilfe von .NET.

Kleine und große Dateien

Die Definition von kleinen und großen Dateien hängt von den verfügbaren Computeressourcen ab. Apps sollten einen Benchmark für den verwendeten Speicheransatz erstellen, um sicherzustellen, dass die erwarteten Größen bewältigt werden können. Erstellen Sie einen Benchmark für Arbeitsspeicher, CPU, Datenträger und Datenbankleistung.

Es können zwar keine spezifischen Grenzen für kleine und große Dateien in Ihrer Bereitstellung angegeben werden, doch Sie finden hier einige der zugehörigen Standardeinstellungen von ASP.NET Core für FormOptions (API-Dokumentation):

  • Standardmäßig puffert HttpRequest.Form nicht den gesamten Anforderungstext (BufferBody), aber alle ggf. enthaltenen mehrteiligen Formulardateien.
  • MultipartBodyLengthLimit ist die maximale Größe für gepufferte Formulardateien (Standardwert: 128 MB).
  • MemoryBufferThreshold gibt an, bis zu welcher Größe Dateien im Arbeitsspeicher gepuffert werden sollen, bevor zu einer Pufferdatei auf dem Datenträger übergegangen wird (Standardwert: 64 KB). MemoryBufferThreshold fungiert als Grenze zwischen kleinen und großen Dateien und wird je nach App-Ressourcen und Szenarien erhöht oder verringert.

Weitere Informationen zu FormOptions finden Sie im FormOptions-Kurs in der ASP.NET Core-Referenzquelle.

Hinweis

Dokumentationslinks zur .NET-Referenzquelle laden in der Regel den Standardbranch des Repositorys, der die aktuelle Entwicklung für das nächste Release von .NET darstellt. Um ein Tag für ein bestimmtes Release auszuwählen, wählen Sie diesen mit der Dropdownliste Switch branches or tags (Branches oder Tags wechseln) aus. Weitere Informationen finden Sie unter How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) (Auswählen eines Versionstags von ASP.NET Core-Quellcode (dotnet/AspNetCore.Docs #26205)).

Szenarien für das Hochladen von Dateien

Zwei allgemeine Ansätze für das Hochladen von Dateien sind Pufferung und Streaming.

Pufferung

Die gesamte Datei wird in IFormFile gelesen. IFormFile ist eine C#-Darstellung der Datei, die zum Verarbeiten oder Speichern der Datei verwendet wird.

Die beim Hochladen von Dateien verwendeten Datenträger- und Arbeitsspeicherressourcen sind abhängig von Anzahl und Größe gleichzeitig hochgeladener Dateien. Wenn eine App versucht, zu viele Uploads zu puffern, stürzt die Website ab, sobald der Arbeitsspeicher oder Speicherplatz auf dem Datenträger ausgelastet ist. Wenn die Größe oder Häufigkeit von Dateiuploads die Ressourcen der App auslastet, wählen Sie Streaming.

Jede einzelne gepufferte Datei, die 64 KB überschreitet, wird aus dem Arbeitsspeicher in eine temporäre Datei auf dem Datenträger verschoben.

Temporäre Dateien für umfangreichere Anforderungen werden an den Speicherort geschrieben, der in der Umgebungsvariablen ASPNETCORE_TEMP angegeben ist. Ist ASPNETCORE_TEMP nicht definiert, werden die Dateien in den temporären Ordner des aktuellen Benutzers bzw. der aktuellen Benutzerin geschrieben.

Die Pufferung kleiner Dateien wird in den folgenden Abschnitten dieses Themas behandelt:

Streaming

Die Datei wird über eine mehrteilige Anforderung empfangen und von der App direkt verarbeitet oder gespeichert. Streaming verbessert die Leistung nicht wesentlich. Streaming reduziert beim Hochladen von Dateien die Anforderungen an den Arbeitsspeicher oder Speicherplatz auf dem Datenträger.

Das Streamen großer Dateien wird im Abschnitt Hochladen großer Dateien mit Streaming beschrieben.

Hochladen kleiner Dateien mit gepufferten Modellbindungen in physischen Speicher

Zum Hochladen kleiner Dateien können Sie ein mehrteiliges Formular verwenden oder über JavaScript eine POST-Anforderung erstellen.

Das folgende Beispiel veranschaulicht die Verwendung eines Razor Pages-Formulars zum Hochladen einer einzelnen Datei (Pages/BufferedSingleFileUploadPhysical.cshtml in der Beispiel-App):

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

Das folgende Beispiel ist analog zum vorherigen Beispiel, mit der Ausnahme, dass:

  • Die (Fetch-API) von JavaScript zum Senden der Formulardaten verwendet wird.
  • Keine Validierung erfolgt.
<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>

Um den POST-Befehl für das Formular in JavaScript für Clients auszuführen, die die Fetch-API nicht unterstützen, wählen Sie einen der folgenden Ansätze:

  • Verwenden Sie Fetch Polyfill (Beispiel: window.fetch polyfill (github/fetch)).

  • Verwenden Sie XMLHttpRequest. Beispiel:

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

HTML-Formulare müssen den Codierungstyp (enctype) multipart/form-data angeben, damit Dateiuploads unterstützt werden.

Für ein Eingabeelement des Typs files, welches das Hochladen mehrerer Dateien unterstützt, geben Sie das Attribut multiple für das Element <input> an:

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

Auf die einzelnen Dateien, die auf den Server geladen werden, kann über eine Modellbindung mittels IFormFilezugegriffen werden. Die Beispiel-App veranschaulicht mehrere gepufferte Dateiuploads für Szenarien mit Datenbank und physischem Speicher.

Warnung

Verwenden Sie die Eigenschaft FileName von IFormFile, ausschließlich für die Anzeige und Protokollierung. Codieren Sie den Dateinamen für die Anzeige und Protokollierung mit HTML. Ein Angreifer kann einen bösartigen Dateinamen bereitstellen, einschließlich vollständiger oder relativer Pfade. Anwendungen sollten folgende Aktionen ausführen:

  • den Pfad aus dem vom Benutzer angegebenen Dateinamen entfernen
  • den mit HTML codierten Dateinamen, aus dem der Pfad entfernt wurde, für die Benutzeroberfläche oder Protokollierung speichern
  • einen neuen zufälligen Dateinamen für die Speicherung generieren

Mit dem folgenden Code wird der Pfad aus dem Dateinamen entfernt:

string untrustedFileName = Path.GetFileName(pathName);

Bei den bisher vorgestellten Beispielen werden keine Sicherheitsaspekte berücksichtigt. Weitere Informationen finden Sie in den folgenden Abschnitten und in der Beispiel-App:

Beim Hochladen von Dateien mit Modellbindung und IFormFile kann die Aktionsmethode Folgendes akzeptieren:

Hinweis

Zur Bindung werden Formulardateien anhand des Namens abgeglichen. So muss beispielsweise der HTML-Wert name in <input type="file" name="formFile"> mit der C#-Parameter-/Eigenschaftsbindung übereinstimmen (FormFile). Weitere Informationen finden Sie im Abschnitt Abgleichen des Werts des Namensattributs mit dem Parameternamen in der POST-Methode.

Im Beispiel unten geschieht Folgendes:

  • Durchläuft mindestens eine hochgeladene Datei.
  • Verwendet Path.GetTempFileName, um einen vollständigen Pfad für eine Datei samt Dateinamen zurückzugeben.
  • Speichert die Dateien im lokalen Dateisystem mit einem von der App generierten Dateinamen.
  • Gibt die Gesamtanzahl und Größe der hochgeladenen Dateien zurück.
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 });
}

Verwenden Sie Path.GetRandomFileName, um einen Dateinamen ohne Pfad zu generieren. Im folgenden Beispiel wird der Pfad aus der Konfiguration abgerufen:

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

Der an FileStream übergebene Pfad muss den Dateinamen enthalten. Ist dies nicht der Fall, wird zur Laufzeit eine UnauthorizedAccessException ausgelöst.

Dateien, die über die IFormFile-Technik hochgeladen werden, werden vor der Verarbeitung im Arbeitsspeicher oder auf einem Datenträger des Servers gepuffert. Innerhalb der Aktionsmethode können Sie über einen Stream auf die IFormFile-Inhalte zugreifen. Zusätzlich zum lokalen Dateisystem können Dateien in einer Netzwerkfreigabe oder einem Dateispeicherdienst gespeichert werden, wie beispielsweise Azure Blob Storage.

Ein weiteres Beispiel, das mehrere hochzuladende Dateien in einer Schleife durchläuft und sichere Dateinamen verwendet, finden Sie in der Beispiel-App unter Pages/BufferedMultipleFileUploadPhysical.cshtml.cs.

Warnung

Path.GetTempFileName löst eine IOException aus, wenn mehr als 65.535 Dateien erstellt werden, ohne alte temporäre Dateien zu löschen. Der Grenzwert von 65.535 Dateien gilt pro Server. Weitere Informationen zu diesem Grenzwert für Windows-Betriebssysteme finden Sie in den Hinweisen in den folgenden Themen:

Hochladen kleiner Dateien mit gepufferten Modellbindungen in eine Datenbank

Zum Speichern von Binärdateidaten in einer Datenbank über das Entity Framework definieren Sie für die Entität eine Arrayeigenschaft des Typs Byte:

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

Geben Sie eine Seitenmodelleigenschaft für die Klasse an, die eine IFormFile enthält:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Hinweis

IFormFile kann wie oben dargestellt direkt als Parameter einer Aktionsmethode oder als gebundene Modelleigenschaft verwendet werden. Im vorherigen Beispiel wird eine gebundene Modelleigenschaft verwendet.

FileUpload wird im Razor Pages-Formular verwendet:

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

Wenn das Formular per POST an den Server gesendet wird, kopieren Sie die IFormFile in einen Stream, und speichern Sie ihn als Bytearray in der Datenbank. Im folgenden Beispiel speichert _dbContext den Datenbankkontext der 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();
}

Das vorherige Beispiel ähnelt einem Szenario, das in der Beispiel-App veranschaulicht wird:

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

Warnung

Speichern Sie Binärdaten in relationalen Datenbanken mit Bedacht, da sie Auswirkungen auf die Leistung haben können.

Verlassen Sie sich nicht ohne Validierung auf die FileName-Eigenschaft IFormFile, bzw. vertrauen Sie ihr nicht. Die FileName-Eigenschaft darf nur für Anzeigezwecke und erst nach der HTML-Codierung verwendet werden.

Bei den vorgestellten Beispielen werden keine Sicherheitsaspekte berücksichtigt. Weitere Informationen finden Sie in den folgenden Abschnitten und in der Beispiel-App:

Hochladen von großen Dateien mittels Streaming

Das 3.1-Beispiel zeigt, wie JavaScript verwendet wird, um eine Datei an eine Controlleraktion zu streamen. Das Fälschungssicherheitstoken einer Datei wird mithilfe eines benutzerdefinierten Filterattributs generiert und an die HTTP-Header des Clients anstelle des Anforderungstexts übergeben. Da die Aktionsmethode die hochgeladenen Daten direkt verarbeitet, wird die Modellbindung des Formulars von einem anderen benutzerdefinierten Filter deaktiviert. Innerhalb der Aktion werden die Inhalte des Formulars über MultipartReader gelesen. Dieses Element liest jede einzelne MultipartSection-Klasse, wodurch die Datei verarbeitet wird oder die Inhalte angemessen gespeichert werden. Nachdem alle mehrteiligen Abschnitte gelesen wurden, führt die Aktion ihre eigene Modellbindung aus.

Die Antwort der Startseite lädt das Formular und speichert das Fälschungssicherheitstoken (über das GenerateAntiforgeryTokenCookieAttribute-Attribut) in einem Cookie (cookie). Das Attribut nutzt die in ASP.NET Core integrierte Unterstützung der Fälschungssicherheit, um ein Cookie (cookie) mit einem Anforderungstoken festzulegen:

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

Das DisableFormValueModelBindingAttribute wird zum Deaktivieren der Modellbindung verwendet:

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

In der Beispiel-App werden GenerateAntiforgeryTokenCookieAttribute und DisableFormValueModelBindingAttribute als Filter auf die Seitenanwendungsmodelle von /StreamedSingleFileUploadDb und /StreamedSingleFileUploadPhysical in Startup.ConfigureServices unter Verwendung der Razor Pages-Konventionen angewendet:

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

Da die Modellbindung das Formular nicht liest, werden Parameter, die über das Formular gebunden werden, nicht gebunden (Abfrage, Route und Header funktionieren weiterhin). Die Aktionsmethode arbeitet direkt mit der Request-Eigenschaft zusammen. Ein MultipartReader wird verwendet, um die verschiedenen Abschnitte zu lesen. Schlüssel-Wert-Daten werden in einem KeyValueAccumulator gespeichert. Nachdem die mehrteiligen Abschnitte gelesen wurden, werden die Inhalte von KeyValueAccumulator verwendet, um die Formulardaten an einen Modelltyp zu binden.

Die vollständige StreamingController.UploadDatabase-Methode für das Streaming an eine Datenbank mit EF Core sieht wie folgt aus:

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

Die vollständige StreamingController.UploadPhysical-Methode für das Streaming an einen physischen Speicherort:

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

In der Beispiel-App werden Validierungsprüfungen von FileHelpers.ProcessStreamedFile übernommen.

Überprüfen

Die FileHelpers-Klasse der Beispiel-App veranschaulicht eine Reihe von Prüfungen für gepufferte IFormFile- und gestreamte Dateiuploads. Informationen zur Verarbeitung von Dateiuploads mit IFormFile-Pufferung in der Beispiel-App finden Sie in der Datei Utilities/FileHelpers.cs in der ProcessFormFile-Methode. Informationen zum Verarbeiten gestreamter Dateien finden Sie in der ProcessStreamedFile-Methode in der gleichen Datei.

Warnung

Die in der Beispiel-App demonstrierten Validierungsverarbeitungsmethoden untersuchen nicht den Inhalt hochgeladener Dateien. In den meisten Produktionsszenarien wird eine API zum Scannen auf Viren/Schadsoftware auf die Datei angewendet, bevor die Datei Benutzern oder anderen Systemen zur Verfügung gestellt wird.

Obwohl das Themenbeispiel ein funktionierendes Beispiel für Validierungstechniken darstellt, implementieren Sie die FileHelpers-Klasse nur unter folgenden Voraussetzungen in einer Produktions-App:

  • Sie verstehen die Implementierung vollständig.
  • Sie ändern die Implementierung entsprechend der Umgebung und den Spezifikationen der App.

Implementieren Sie niemals willkürlich Sicherheitscode in einer App, wenn Sie diese Anforderungen nicht erfüllen.

Validierung von Inhalten

Wenden Sie für hochgeladene Inhalte eine API zum Scannen auf Viren/Schadsoftware von Drittanbietern an.

Das Scannen von Dateien stellt in Szenarien mit hohem Verarbeitungsvolumen hohe Anforderungen an die Serverressourcen. Wenn die Leistung bei der Verarbeitung von Anforderungen durch das Scannen von Dateien beeinträchtigt wird, erwägen Sie, die Scanaufgaben an einen Hintergrunddienst auszulagern, möglicherweise an einen Dienst, der auf einem anderen Server als dem mit der App ausgeführt wird. Üblicherweise werden hochgeladene Dateien in einem Quarantänebereich aufbewahrt, bis der Hintergrundvirenscanner sie prüft. Wenn eine Datei die Prüfung besteht, wird sie an den normalen Speicherort verschoben. Diese Schritte erfolgen in der Regel in Verbindung mit einem Datenbank-Datensatz, der den Scanstatus einer Datei angibt. Bei einem solchen Ansatz bleiben App und App-Server auf die Bearbeitung von Anforderungen ausgerichtet.

Validierung von Dateierweiterungen

Die Erweiterung der hochgeladenen Datei muss mit einer Liste zulässiger Erweiterungen abgeglichen werden. Beispiel:

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
}

Validierung der Dateisignatur

Die Signatur einer Datei wird durch die ersten Bytes am Anfang einer Datei bestimmt. Diese Bytes können verwendet werden, um anzugeben, ob die Erweiterung dem Inhalt der Datei entspricht. Die Beispiel-App überprüft Dateisignaturen auf gängige Dateitypen. Im folgenden Beispiel wird die Dateisignatur eines JPEG-Bilds mit der Datei abgeglichen:

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

Um zusätzliche Dateisignaturen zu erhalten, verwenden Sie eine Datenbank für Dateisignaturen (Google-Suchergebnis) und offizielle Dateispezifikationen. Die Überprüfung der offiziellen Dateispezifikationen kann dazu beitragen, dass die ausgewählten Signaturen gültig sind.

Sicherheit von Dateinamen

Verwenden Sie niemals einen vom Kunden angegebenen Dateinamen zum Speichern einer Datei in physischem Speicher. Erstellen Sie mithilfe von Path.GetRandomFileName oder Path.GetTempFileName einen sicheren Dateinamen für die Datei, um einen vollständigen Pfad (einschließlich des Dateinamens) für die temporäre Speicherung zu erstellen.

Razor versieht anzuzeigende Eigenschaftswerte automatisch mit HTML-Codierung. Der folgende Code kann sicher verwendet werden:

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

Wenden Sie außerhalb von Razor stets HtmlEncode auf Dateinameninhalte aus Benutzeranforderungen an.

Bei vielen Implementierungen muss geprüft werden, ob die Datei existiert. Andernfalls wird die Datei durch eine gleichnamige Datei überschrieben. Stellen Sie zusätzliche Logik bereit, um die Vorgaben Ihrer App zu erfüllen.

Validierung der Größe

Begrenzen Sie die Größe hochgeladener Dateien.

In der Beispiel-App ist die Größe der Datei auf 2 MB begrenzt (angegeben in Bytes). Der Grenzwert wird mittels Konfiguration in der Datei appsettings.json angegeben:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit wird in PageModel-Klassen eingefügt:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Wenn eine Dateigröße den Grenzwert überschreitet, wird die Datei abgelehnt:

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

Vergleichen des Werts des Namensattributs mit dem Parameternamen der POST-Methode

In Razor-fremden Formularen, die Formulardaten per POST übermitteln oder direkt FormData von JavaScript verwenden, muss der im Formularelement oder in FormData angegebene Name dem Namen des Parameters in der Aktion des Controllers entsprechen.

Im folgenden Beispiel:

  • Wenn ein <input>-Element verwendet wird, wird das name-Attribut auf den Wert battlePlans festgelegt:

    <input type="file" name="battlePlans" multiple>
    
  • Bei Verwendung von FormData in JavaScript wird der Name auf den Wert battlePlans festgelegt:

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

Verwenden Sie einen übereinstimmenden Namen für den Parameter der C#-Methode (battlePlans):

  • Für eine Razor Pages-Seitenhandlermethode namens Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Für eine MVC POST-Controlleraktionsmethode:

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

Server- und App-Konfiguration

Grenzwert der Länge von mehrteiligem Text

MultipartBodyLengthLimit legt den Grenzwert der Länge jedes mehrteiligen Texts fest. Formularabschnitte, die diesen Grenzwert überschreiten, lösen beim Analysieren eine InvalidDataException aus. Der Standardwert ist 134.217.728 (128 MB). Passen Sie den Grenzwert mithilfe der Einstellung MultipartBodyLengthLimit in Startup.ConfigureServices an:

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

RequestFormLimitsAttribute dient zum Festlegen des MultipartBodyLengthLimit für eine einzelne Seite oder Aktion.

Wenden Sie in einer Razor Pages-Anwendung den Filter mit einer Konvention in Startup.ConfigureServices an:

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

Wenden Sie in einer Razor Pages- oder MVC-App den Filter auf das Seitenmodell oder auf die Aktionsmethode an:

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

Maximale Größe des Anforderungstexts für Kestrel

Die maximale Größe des Anforderungstexts beträgt für von Kestrel gehostete Apps standardmäßig 30.000.000 Bytes, also ungefähr 28,6 MB. Passen Sie den Grenzwert mithilfe der Kestrel-Serveroption MaxRequestBodySize an:

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 dient zum Festlegen von MaxRequestBodySize für eine einzelne Seite oder Aktion.

Wenden Sie in einer Razor Pages-Anwendung den Filter mit einer Konvention in Startup.ConfigureServices an:

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

Wenden Sie in einer Razor Pages- oder MVC-App den Filter auf die Seitenhandlerklasse oder Aktionsmethode an:

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

RequestSizeLimitAttribute kann auch mithilfe der Razor-Anweisung @attribute angewendet werden:

@attribute [RequestSizeLimitAttribute(52428800)]

Weitere Grenzwerte für Kestrel

Für von Kestrel gehostete Apps können noch andere Kestrel-Grenzwerte gelten:

IIS

Der Standardgrenzwert für Anforderungen (maxAllowedContentLength) beträgt standardmäßig 30.000.000 Bytes, also ungefähr 28,6 MB. Passen Sie den Grenzwert in der Datei web.config an. Im folgenden Beispiel wird der Grenzwert auf 50 MB (52.428.800 Bytes) festgelegt:

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

Die Einstellung maxAllowedContentLength gilt nur für IIS. Weitere Informationen finden Sie unter Request Limits <requestLimits>.

Problembehandlung

Nachfolgend werden einige häufig auftretenden Probleme aufgeführt, die entstehen können, wenn Dateien hochgeladen werden. Außerdem wird erläutert, wie Sie diese Probleme beheben können.

Fehler „Nicht gefunden“ bei Bereitstellung auf einem IIS-Server

Der folgende Fehler gibt an, dass die hochgeladene Datei die konfigurierte Inhaltslänge des Servers überschreitet:

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

Weitere Informationen finden Sie im Abschnitt IIS.

Verbindungsfehler

Ein Verbindungsfehler und eine zurückgesetzte Serververbindung deuten wahrscheinlich darauf hin, dass die hochgeladene Datei die maximale Anforderungstextgröße von Kestrel überschreitet. Weitere Informationen finden Sie im Abschnitt Maximale Größe des Anforderungstexts für Kestrel. Die Verbindungsgrenzwerte für Kestrel-Clients müssen ggf. ebenfalls angepasst werden.

Ausnahme bei möglichem NULL-Verweis mit IFormFile

Wenn der Controller hochgeladene Dateien mit IFormFile akzeptiert, der Wert aber null ist, bestätigen Sie, dass das HTML-Formular den enctype-Wert multipart/form-data angibt. Wenn dieses Attribut für das <form>-Element festgelegt ist, werden keine Dateien hochgeladen, und alle gebundenen IFormFile-Argumente sind null. Bestätigen Sie auch, dass die Uploadbenennung in den Formulardaten mit der Benennung der App übereinstimmt.

Stream war zu lang.

Bei den Beispielen in diesem Thema wird davon ausgegangen, dass MemoryStream den Inhalt der hochgeladenen Datei enthält. Die maximale Größe für einen MemoryStream beträgt int.MaxValue. Wenn das Dateiuploadszenario der App das Speichern von Dateiinhalten mit einer Größe über 50 MB erfordert, verwenden Sie einen alternativen Ansatz, der nicht auf einem einzelnen MemoryStream zum Speichern des Inhalts einer hochgeladenen Datei basiert.

ASP.NET Core unterstützt das Hochladen einer oder mehrerer Dateien über die gepufferte Modellbindung für kleinere Dateien und ungepuffertes Streaming für größere Dateien.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Sicherheitsüberlegungen

Gehen Sie mit Bedacht vor, wenn Sie Benutzern die Möglichkeit geben, Dateien auf einen Server hochzuladen. Angreifer versuchen möglicherweise Folgendes:

  • Ausführen von Denial-of-Service-Angriffen
  • Hochladen von Viren oder Schadsoftware
  • Gefährden von Netzwerken und Servern auf andere Weise

Folgende Schritte können Sie dabei unterstützen, die Wahrscheinlichkeit eines erfolgreichen Angriffs zu verringern:

  • Laden Sie Dateien in einen dedizierten Bereich zum Hochladen von Dateien hoch, vorzugsweise auf ein Nicht-Systemlaufwerk. Ein dedizierter Speicherort erleichtert es, Sicherheitsbeschränkungen für hochgeladene Dateien zu erzwingen. Deaktivieren Sie Ausführungsberechtigungen für den Speicherort für hochgeladene Dateien.†
  • Speichern Sie hochgeladene Dateien nicht persistent in der Verzeichnisstruktur, in der sich auch die App befindet.†
  • Wählen Sie einen sicheren von der App festgelegten Dateinamen. Verwenden Sie keinen benutzerseitig angegebenen Dateinamen oder den nicht vertrauenswürdigen Dateinamen der hochgeladenen Datei.† Codieren Sie den nicht vertrauenswürdigen Dateinamen mit HTML, wenn er angezeigt wird. Beispiele dafür wären etwa die Protokollierung des Dateinamens oder die Anzeige auf der Benutzeroberfläche. (Razor codiert Ausgaben automatisch mit HTML.)
  • Lassen Sie nur genehmigte Dateierweiterungen für die Entwurfsspezifikation der App zu.†
  • Stellen Sie sicher, dass clientseitige Überprüfungen auf dem Server erfolgen.† Clientseitige Überprüfungen sind leicht zu umgehen.
  • Überprüfen Sie die Größe einer hochgeladenen Datei. Legen Sie einen Grenzwert für die maximale Größe fest, um große Uploads zu verhindern.†
  • Wenn Dateien nicht durch eine hochgeladene Datei mit demselben Namen überschrieben werden sollen, vergleichen Sie den Dateinamen mit der Datenbank oder dem physischen Speicher, bevor Sie die Datei hochladen.
  • Wenden Sie auf die hochgeladenen Inhalte einen Scanner auf Viren und Schadsoftware an, ehe die Datei gespeichert wird.

†Die Beispiel-App veranschaulicht einen Ansatz, der die Kriterien erfüllt.

Warnung

Das Hochladen von schädlichem Code auf ein System ist häufig der erste Schritt, um Code mit der folgenden Absicht auszuführen:

  • Erlangen der vollständigen Kontrolle über ein System.
  • Überlasten eines Systems mit dem Ziel eines Systemausfalls.
  • Kompromittieren von Benutzer- oder Systemdaten
  • Anwenden von Graffiti auf eine öffentliche Benutzeroberfläche.

Wie Sie die Angriffsoberfläche beim Akzeptieren von Dateien von Benutzenden reduzieren, erfahren Sie in den folgenden Artikeln:

Weitere Informationen zur Implementierung von Sicherheitsmaßnahmen, einschließlich Beispiele aus der Beispielanwendung, finden Sie im Abschnitt Validierung.

Speicherszenarien

Zu den allgemeinen Speicheroptionen für Dateien gehören u. a.:

  • Datenbank

    • Beim Hochladen kleiner Dateien ist eine Datenbank oft schneller als physische Speicheroptionen (Dateisystem oder Netzwerkfreigabe).
    • Eine Datenbank ist oft praktischer als physische Speicheroptionen, da das Abrufen eines Datenbank-Datensatzes für Benutzerdaten gleichzeitig den Dateiinhalt (z. B. ein Avatarbild) bereitstellen kann.
    • Eine Datenbank ist potenziell kostengünstiger als die Nutzung eines Datenspeicherdiensts.
  • Physischer Speicher (Dateisystem oder Netzwerkfreigabe)

    • Für das Hochladen großer Dateien:
      • Für die Datenbank geltende Grenzwerte können die Größe des Uploads einschränken.
      • Physischer Speicher ist oft teurer als Datenbankspeicher.
    • Physischer Speicher ist potenziell teurer als die Nutzung eines Datenspeicherdiensts.
    • Der Prozess der App muss Lese- und Schreibberechtigungen für den Speicherort haben. Erteilen Sie niemals die Ausführungsberechtigung.
  • Datenspeicherdienst (z. B. Azure Blob Storage)

    • Dienste bieten in der Regel eine bessere Skalierbarkeit und Resilienz gegenüber lokalen Lösungen, die in der Regel Single Points of Failure aufweisen.
    • Dienste sind bei Szenarien mit großen Speicherinfrastrukturen potenziell kostengünstiger.

    Weitere Informationen finden Sie unter Schnellstart: Erstellen eines Blobs im Objektspeicher mithilfe von .NET.

Szenarien für das Hochladen von Dateien

Zwei allgemeine Ansätze für das Hochladen von Dateien sind Pufferung und Streaming.

Pufferung

Die gesamte Datei wird in eine IFormFile eingelesen, die eine C#-Darstellung der Datei ist, die zum Verarbeiten oder Speichern der Datei verwendet wird.

Welche Ressourcen (Datenträger, Arbeitsspeicher) für das Hochladen von Dateien verwendet werden, ist von der Anzahl und Größe gleichzeitig hochgeladener Dateien abhängig. Wenn eine App versucht, zu viele Uploads zu puffern, stürzt die Website ab, sobald der Arbeitsspeicher oder Speicherplatz auf dem Datenträger ausgelastet ist. Wenn die Größe oder Häufigkeit von Dateiuploads die Ressourcen der App auslastet, wählen Sie Streaming.

Hinweis

Jede einzelne gepufferte Datei, die 64 KB überschreitet, wird aus dem Arbeitsspeicher in eine temporäre Datei auf dem Datenträger verschoben.

Die Pufferung kleiner Dateien wird in den folgenden Abschnitten dieses Themas behandelt:

Streaming

Die Datei wird über eine mehrteilige Anforderung empfangen und von der App direkt verarbeitet oder gespeichert. Streaming verbessert die Leistung nicht wesentlich. Streaming reduziert beim Hochladen von Dateien die Anforderungen an den Arbeitsspeicher oder Speicherplatz auf dem Datenträger.

Das Streamen großer Dateien wird im Abschnitt Hochladen großer Dateien mit Streaming beschrieben.

Hochladen kleiner Dateien mit gepufferten Modellbindungen in physischen Speicher

Zum Hochladen kleiner Dateien können Sie ein mehrteiliges Formular verwenden oder über JavaScript eine POST-Anforderung erstellen.

Das folgende Beispiel veranschaulicht die Verwendung eines Razor Pages-Formulars zum Hochladen einer einzelnen Datei (Pages/BufferedSingleFileUploadPhysical.cshtml in der Beispiel-App):

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

Das folgende Beispiel ist analog zum vorherigen Beispiel, mit der Ausnahme, dass:

  • Die (Fetch-API) von JavaScript zum Senden der Formulardaten verwendet wird.
  • Keine Validierung erfolgt.
<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>

Um den POST-Befehl für das Formular in JavaScript für Clients auszuführen, die die Fetch-API nicht unterstützen, wählen Sie einen der folgenden Ansätze:

  • Verwenden Sie Fetch Polyfill (Beispiel: window.fetch polyfill (github/fetch)).

  • Verwenden Sie XMLHttpRequest. Beispiel:

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

HTML-Formulare müssen den Codierungstyp (enctype) multipart/form-data angeben, damit Dateiuploads unterstützt werden.

Für ein Eingabeelement des Typs files, welches das Hochladen mehrerer Dateien unterstützt, geben Sie das Attribut multiple für das Element <input> an:

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

Auf die einzelnen Dateien, die auf den Server geladen werden, kann über eine Modellbindung mittels IFormFilezugegriffen werden. Die Beispiel-App veranschaulicht mehrere gepufferte Dateiuploads für Szenarien mit Datenbank und physischem Speicher.

Warnung

Verwenden Sie die Eigenschaft FileName von IFormFile, ausschließlich für die Anzeige und Protokollierung. Codieren Sie den Dateinamen für die Anzeige und Protokollierung mit HTML. Ein Angreifer kann einen bösartigen Dateinamen bereitstellen, einschließlich vollständiger oder relativer Pfade. Anwendungen sollten folgende Aktionen ausführen:

  • den Pfad aus dem vom Benutzer angegebenen Dateinamen entfernen
  • den mit HTML codierten Dateinamen, aus dem der Pfad entfernt wurde, für die Benutzeroberfläche oder Protokollierung speichern
  • einen neuen zufälligen Dateinamen für die Speicherung generieren

Mit dem folgenden Code wird der Pfad aus dem Dateinamen entfernt:

string untrustedFileName = Path.GetFileName(pathName);

Bei den bisher vorgestellten Beispielen werden keine Sicherheitsaspekte berücksichtigt. Weitere Informationen finden Sie in den folgenden Abschnitten und in der Beispiel-App:

Beim Hochladen von Dateien mit Modellbindung und IFormFile kann die Aktionsmethode Folgendes akzeptieren:

Hinweis

Zur Bindung werden Formulardateien anhand des Namens abgeglichen. So muss beispielsweise der HTML-Wert name in <input type="file" name="formFile"> mit der C#-Parameter-/Eigenschaftsbindung übereinstimmen (FormFile). Weitere Informationen finden Sie im Abschnitt Abgleichen des Werts des Namensattributs mit dem Parameternamen in der POST-Methode.

Im Beispiel unten geschieht Folgendes:

  • Durchläuft mindestens eine hochgeladene Datei.
  • Verwendet Path.GetTempFileName, um einen vollständigen Pfad für eine Datei samt Dateinamen zurückzugeben.
  • Speichert die Dateien im lokalen Dateisystem mit einem von der App generierten Dateinamen.
  • Gibt die Gesamtanzahl und Größe der hochgeladenen Dateien zurück.
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 });
}

Verwenden Sie Path.GetRandomFileName, um einen Dateinamen ohne Pfad zu generieren. Im folgenden Beispiel wird der Pfad aus der Konfiguration abgerufen:

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

Der an FileStream übergebene Pfad muss den Dateinamen enthalten. Ist dies nicht der Fall, wird zur Laufzeit eine UnauthorizedAccessException ausgelöst.

Dateien, die über die IFormFile-Technik hochgeladen werden, werden vor der Verarbeitung im Arbeitsspeicher oder auf einem Datenträger des Servers gepuffert. Innerhalb der Aktionsmethode können Sie über einen Stream auf die IFormFile-Inhalte zugreifen. Zusätzlich zum lokalen Dateisystem können Dateien in einer Netzwerkfreigabe oder einem Dateispeicherdienst gespeichert werden, wie beispielsweise Azure Blob Storage.

Ein weiteres Beispiel, das mehrere hochzuladende Dateien in einer Schleife durchläuft und sichere Dateinamen verwendet, finden Sie in der Beispiel-App unter Pages/BufferedMultipleFileUploadPhysical.cshtml.cs.

Warnung

Path.GetTempFileName löst eine IOException aus, wenn mehr als 65.535 Dateien erstellt werden, ohne alte temporäre Dateien zu löschen. Der Grenzwert von 65.535 Dateien gilt pro Server. Weitere Informationen zu diesem Grenzwert für Windows-Betriebssysteme finden Sie in den Hinweisen in den folgenden Themen:

Hochladen kleiner Dateien mit gepufferten Modellbindungen in eine Datenbank

Zum Speichern von Binärdateidaten in einer Datenbank über das Entity Framework definieren Sie für die Entität eine Arrayeigenschaft des Typs Byte:

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

Geben Sie eine Seitenmodelleigenschaft für die Klasse an, die eine IFormFile enthält:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Hinweis

IFormFile kann wie oben dargestellt direkt als Parameter einer Aktionsmethode oder als gebundene Modelleigenschaft verwendet werden. Im vorherigen Beispiel wird eine gebundene Modelleigenschaft verwendet.

FileUpload wird im Razor Pages-Formular verwendet:

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

Wenn das Formular per POST an den Server gesendet wird, kopieren Sie die IFormFile in einen Stream, und speichern Sie ihn als Bytearray in der Datenbank. Im folgenden Beispiel speichert _dbContext den Datenbankkontext der 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();
}

Das vorherige Beispiel ähnelt einem Szenario, das in der Beispiel-App veranschaulicht wird:

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

Warnung

Speichern Sie Binärdaten in relationalen Datenbanken mit Bedacht, da sie Auswirkungen auf die Leistung haben können.

Verlassen Sie sich nicht ohne Validierung auf die FileName-Eigenschaft IFormFile, bzw. vertrauen Sie ihr nicht. Die FileName-Eigenschaft darf nur für Anzeigezwecke und erst nach der HTML-Codierung verwendet werden.

Bei den vorgestellten Beispielen werden keine Sicherheitsaspekte berücksichtigt. Weitere Informationen finden Sie in den folgenden Abschnitten und in der Beispiel-App:

Hochladen von großen Dateien mittels Streaming

Das folgende Beispiel zeigt, wie JavaScript verwendet wird, um eine Datei an eine Controlleraktion zu streamen. Das Fälschungssicherheitstoken einer Datei wird mithilfe eines benutzerdefinierten Filterattributs generiert und an die HTTP-Header des Clients anstelle des Anforderungstexts übergeben. Da die Aktionsmethode die hochgeladenen Daten direkt verarbeitet, wird die Modellbindung des Formulars von einem anderen benutzerdefinierten Filter deaktiviert. Innerhalb der Aktion werden die Inhalte des Formulars über MultipartReader gelesen. Dieses Element liest jede einzelne MultipartSection-Klasse, wodurch die Datei verarbeitet wird oder die Inhalte angemessen gespeichert werden. Nachdem alle mehrteiligen Abschnitte gelesen wurden, führt die Aktion ihre eigene Modellbindung aus.

Die Antwort der Startseite lädt das Formular und speichert das Fälschungssicherheitstoken (über das GenerateAntiforgeryTokenCookieAttribute-Attribut) in einem Cookie (cookie). Das Attribut nutzt die in ASP.NET Core integrierte Unterstützung der Fälschungssicherheit, um ein Cookie (cookie) mit einem Anforderungstoken festzulegen:

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

Das DisableFormValueModelBindingAttribute wird zum Deaktivieren der Modellbindung verwendet:

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

In der Beispiel-App werden GenerateAntiforgeryTokenCookieAttribute und DisableFormValueModelBindingAttribute als Filter auf die Seitenanwendungsmodelle von /StreamedSingleFileUploadDb und /StreamedSingleFileUploadPhysical in Startup.ConfigureServices unter Verwendung der Razor Pages-Konventionen angewendet:

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

Da die Modellbindung das Formular nicht liest, werden Parameter, die über das Formular gebunden werden, nicht gebunden (Abfrage, Route und Header funktionieren weiterhin). Die Aktionsmethode arbeitet direkt mit der Request-Eigenschaft zusammen. Ein MultipartReader wird verwendet, um die verschiedenen Abschnitte zu lesen. Schlüssel-Wert-Daten werden in einem KeyValueAccumulator gespeichert. Nachdem die mehrteiligen Abschnitte gelesen wurden, werden die Inhalte von KeyValueAccumulator verwendet, um die Formulardaten an einen Modelltyp zu binden.

Die vollständige StreamingController.UploadDatabase-Methode für das Streaming an eine Datenbank mit EF Core sieht wie folgt aus:

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

Die vollständige StreamingController.UploadPhysical-Methode für das Streaming an einen physischen Speicherort:

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

In der Beispiel-App werden Validierungsprüfungen von FileHelpers.ProcessStreamedFile übernommen.

Überprüfen

Die FileHelpers-Klasse der Beispiel-App veranschaulicht eine Reihe von Prüfungen für gepufferte IFormFile- und gestreamte Dateiuploads. Informationen zur Verarbeitung von Dateiuploads mit IFormFile-Pufferung in der Beispiel-App finden Sie in der Datei Utilities/FileHelpers.cs in der ProcessFormFile-Methode. Informationen zum Verarbeiten gestreamter Dateien finden Sie in der ProcessStreamedFile-Methode in der gleichen Datei.

Warnung

Die in der Beispiel-App demonstrierten Validierungsverarbeitungsmethoden untersuchen nicht den Inhalt hochgeladener Dateien. In den meisten Produktionsszenarien wird eine API zum Scannen auf Viren/Schadsoftware auf die Datei angewendet, bevor die Datei Benutzern oder anderen Systemen zur Verfügung gestellt wird.

Obwohl das Themenbeispiel ein funktionierendes Beispiel für Validierungstechniken darstellt, implementieren Sie die FileHelpers-Klasse nur unter folgenden Voraussetzungen in einer Produktions-App:

  • Sie verstehen die Implementierung vollständig.
  • Sie ändern die Implementierung entsprechend der Umgebung und den Spezifikationen der App.

Implementieren Sie niemals willkürlich Sicherheitscode in einer App, wenn Sie diese Anforderungen nicht erfüllen.

Validierung von Inhalten

Wenden Sie für hochgeladene Inhalte eine API zum Scannen auf Viren/Schadsoftware von Drittanbietern an.

Das Scannen von Dateien stellt in Szenarien mit hohem Verarbeitungsvolumen hohe Anforderungen an die Serverressourcen. Wenn die Leistung bei der Verarbeitung von Anforderungen durch das Scannen von Dateien beeinträchtigt wird, erwägen Sie, die Scanaufgaben an einen Hintergrunddienst auszulagern, möglicherweise an einen Dienst, der auf einem anderen Server als dem mit der App ausgeführt wird. Üblicherweise werden hochgeladene Dateien in einem Quarantänebereich aufbewahrt, bis der Hintergrundvirenscanner sie prüft. Wenn eine Datei die Prüfung besteht, wird sie an den normalen Speicherort verschoben. Diese Schritte erfolgen in der Regel in Verbindung mit einem Datenbank-Datensatz, der den Scanstatus einer Datei angibt. Bei einem solchen Ansatz bleiben App und App-Server auf die Bearbeitung von Anforderungen ausgerichtet.

Validierung von Dateierweiterungen

Die Erweiterung der hochgeladenen Datei muss mit einer Liste zulässiger Erweiterungen abgeglichen werden. Beispiel:

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
}

Validierung der Dateisignatur

Die Signatur einer Datei wird durch die ersten Bytes am Anfang einer Datei bestimmt. Diese Bytes können verwendet werden, um anzugeben, ob die Erweiterung dem Inhalt der Datei entspricht. Die Beispiel-App überprüft Dateisignaturen auf gängige Dateitypen. Im folgenden Beispiel wird die Dateisignatur eines JPEG-Bilds mit der Datei abgeglichen:

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

Um zusätzliche Dateisignaturen zu erhalten, verwenden Sie eine Datenbank für Dateisignaturen (Google-Suchergebnis) und offizielle Dateispezifikationen. Die Überprüfung der offiziellen Dateispezifikationen kann dazu beitragen, dass die ausgewählten Signaturen gültig sind.

Sicherheit von Dateinamen

Verwenden Sie niemals einen vom Kunden angegebenen Dateinamen zum Speichern einer Datei in physischem Speicher. Erstellen Sie mithilfe von Path.GetRandomFileName oder Path.GetTempFileName einen sicheren Dateinamen für die Datei, um einen vollständigen Pfad (einschließlich des Dateinamens) für die temporäre Speicherung zu erstellen.

Razor versieht anzuzeigende Eigenschaftswerte automatisch mit HTML-Codierung. Der folgende Code kann sicher verwendet werden:

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

Wenden Sie außerhalb von Razor stets HtmlEncode auf Dateinameninhalte aus Benutzeranforderungen an.

Bei vielen Implementierungen muss geprüft werden, ob die Datei existiert. Andernfalls wird die Datei durch eine gleichnamige Datei überschrieben. Stellen Sie zusätzliche Logik bereit, um die Vorgaben Ihrer App zu erfüllen.

Validierung der Größe

Begrenzen Sie die Größe hochgeladener Dateien.

In der Beispiel-App ist die Größe der Datei auf 2 MB begrenzt (angegeben in Bytes). Der Grenzwert wird mittels Konfiguration in der Datei appsettings.json angegeben:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit wird in PageModel-Klassen eingefügt:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Wenn eine Dateigröße den Grenzwert überschreitet, wird die Datei abgelehnt:

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

Vergleichen des Werts des Namensattributs mit dem Parameternamen der POST-Methode

In Razor-fremden Formularen, die Formulardaten per POST übermitteln oder direkt FormData von JavaScript verwenden, muss der im Formularelement oder in FormData angegebene Name dem Namen des Parameters in der Aktion des Controllers entsprechen.

Im folgenden Beispiel:

  • Wenn ein <input>-Element verwendet wird, wird das name-Attribut auf den Wert battlePlans festgelegt:

    <input type="file" name="battlePlans" multiple>
    
  • Bei Verwendung von FormData in JavaScript wird der Name auf den Wert battlePlans festgelegt:

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

Verwenden Sie einen übereinstimmenden Namen für den Parameter der C#-Methode (battlePlans):

  • Für eine Razor Pages-Seitenhandlermethode namens Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Für eine MVC POST-Controlleraktionsmethode:

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

Server- und App-Konfiguration

Grenzwert der Länge von mehrteiligem Text

MultipartBodyLengthLimit legt den Grenzwert der Länge jedes mehrteiligen Texts fest. Formularabschnitte, die diesen Grenzwert überschreiten, lösen beim Analysieren eine InvalidDataException aus. Der Standardwert ist 134.217.728 (128 MB). Passen Sie den Grenzwert mithilfe der Einstellung MultipartBodyLengthLimit in Startup.ConfigureServices an:

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

RequestFormLimitsAttribute dient zum Festlegen des MultipartBodyLengthLimit für eine einzelne Seite oder Aktion.

Wenden Sie in einer Razor Pages-Anwendung den Filter mit einer Konvention in Startup.ConfigureServices an:

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

Wenden Sie in einer Razor Pages- oder MVC-App den Filter auf das Seitenmodell oder auf die Aktionsmethode an:

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

Maximale Größe des Anforderungstexts für Kestrel

Die maximale Größe des Anforderungstexts beträgt für von Kestrel gehostete Apps standardmäßig 30.000.000 Bytes, also ungefähr 28,6 MB. Passen Sie den Grenzwert mithilfe der Kestrel-Serveroption MaxRequestBodySize an:

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 dient zum Festlegen von MaxRequestBodySize für eine einzelne Seite oder Aktion.

Wenden Sie in einer Razor Pages-Anwendung den Filter mit einer Konvention in Startup.ConfigureServices an:

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

Wenden Sie in einer Razor Pages- oder MVC-App den Filter auf die Seitenhandlerklasse oder Aktionsmethode an:

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

RequestSizeLimitAttribute kann auch mithilfe der Razor-Anweisung @attribute angewendet werden:

@attribute [RequestSizeLimitAttribute(52428800)]

Weitere Grenzwerte für Kestrel

Für von Kestrel gehostete Apps können noch andere Kestrel-Grenzwerte gelten:

IIS

Der Standardgrenzwert für Anforderungen (maxAllowedContentLength) beträgt standardmäßig 30.000.000 Bytes, also ungefähr 28,6 MB. Passen Sie den Grenzwert in der Datei web.config an. Im folgenden Beispiel wird der Grenzwert auf 50 MB (52.428.800 Bytes) festgelegt:

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

Die Einstellung maxAllowedContentLength gilt nur für IIS. Weitere Informationen finden Sie unter Request Limits <requestLimits>.

Erhöhen Sie die maximale Anforderungstextgröße für die HTTP-Anforderung durch Festlegen von IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices. Im folgenden Beispiel wird der Grenzwert auf 50 MB (52.428.800 Bytes) festgelegt:

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

Weitere Informationen finden Sie unter Hosten von ASP.NET Core unter Windows mit IIS.

Problembehandlung

Nachfolgend werden einige häufig auftretenden Probleme aufgeführt, die entstehen können, wenn Dateien hochgeladen werden. Außerdem wird erläutert, wie Sie diese Probleme beheben können.

Fehler „Nicht gefunden“ bei Bereitstellung auf einem IIS-Server

Der folgende Fehler gibt an, dass die hochgeladene Datei die konfigurierte Inhaltslänge des Servers überschreitet:

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

Weitere Informationen finden Sie im Abschnitt IIS.

Verbindungsfehler

Ein Verbindungsfehler und eine zurückgesetzte Serververbindung deuten wahrscheinlich darauf hin, dass die hochgeladene Datei die maximale Anforderungstextgröße von Kestrel überschreitet. Weitere Informationen finden Sie im Abschnitt Maximale Größe des Anforderungstexts für Kestrel. Die Verbindungsgrenzwerte für Kestrel-Clients müssen ggf. ebenfalls angepasst werden.

Ausnahme bei möglichem NULL-Verweis mit IFormFile

Wenn der Controller hochgeladene Dateien mit IFormFile akzeptiert, der Wert aber null ist, bestätigen Sie, dass das HTML-Formular den enctype-Wert multipart/form-data angibt. Wenn dieses Attribut für das <form>-Element festgelegt ist, werden keine Dateien hochgeladen, und alle gebundenen IFormFile-Argumente sind null. Bestätigen Sie auch, dass die Uploadbenennung in den Formulardaten mit der Benennung der App übereinstimmt.

Stream war zu lang.

Bei den Beispielen in diesem Thema wird davon ausgegangen, dass MemoryStream den Inhalt der hochgeladenen Datei enthält. Die maximale Größe für einen MemoryStream beträgt int.MaxValue. Wenn das Dateiuploadszenario der App das Speichern von Dateiinhalten mit einer Größe über 50 MB erfordert, verwenden Sie einen alternativen Ansatz, der nicht auf einem einzelnen MemoryStream zum Speichern des Inhalts einer hochgeladenen Datei basiert.

ASP.NET Core unterstützt das Hochladen einer oder mehrerer Dateien über die gepufferte Modellbindung für kleinere Dateien und ungepuffertes Streaming für größere Dateien.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Sicherheitsüberlegungen

Gehen Sie mit Bedacht vor, wenn Sie Benutzern die Möglichkeit geben, Dateien auf einen Server hochzuladen. Angreifer versuchen möglicherweise Folgendes:

  • Ausführen von Denial-of-Service-Angriffen
  • Hochladen von Viren oder Schadsoftware
  • Gefährden von Netzwerken und Servern auf andere Weise

Folgende Schritte können Sie dabei unterstützen, die Wahrscheinlichkeit eines erfolgreichen Angriffs zu verringern:

  • Laden Sie Dateien in einen dedizierten Bereich zum Hochladen von Dateien hoch, vorzugsweise auf ein Nicht-Systemlaufwerk. Ein dedizierter Speicherort erleichtert es, Sicherheitsbeschränkungen für hochgeladene Dateien zu erzwingen. Deaktivieren Sie Ausführungsberechtigungen für den Speicherort für hochgeladene Dateien.†
  • Speichern Sie hochgeladene Dateien nicht persistent in der Verzeichnisstruktur, in der sich auch die App befindet.†
  • Wählen Sie einen sicheren von der App festgelegten Dateinamen. Verwenden Sie keinen benutzerseitig angegebenen Dateinamen oder den nicht vertrauenswürdigen Dateinamen der hochgeladenen Datei.† Codieren Sie den nicht vertrauenswürdigen Dateinamen mit HTML, wenn er angezeigt wird. Beispiele dafür wären etwa die Protokollierung des Dateinamens oder die Anzeige auf der Benutzeroberfläche. (Razor codiert Ausgaben automatisch mit HTML.)
  • Lassen Sie nur genehmigte Dateierweiterungen für die Entwurfsspezifikation der App zu.†
  • Stellen Sie sicher, dass clientseitige Überprüfungen auf dem Server erfolgen.† Clientseitige Überprüfungen sind leicht zu umgehen.
  • Überprüfen Sie die Größe einer hochgeladenen Datei. Legen Sie einen Grenzwert für die maximale Größe fest, um große Uploads zu verhindern.†
  • Wenn Dateien nicht durch eine hochgeladene Datei mit demselben Namen überschrieben werden sollen, vergleichen Sie den Dateinamen mit der Datenbank oder dem physischen Speicher, bevor Sie die Datei hochladen.
  • Wenden Sie auf die hochgeladenen Inhalte einen Scanner auf Viren und Schadsoftware an, ehe die Datei gespeichert wird.

†Die Beispiel-App veranschaulicht einen Ansatz, der die Kriterien erfüllt.

Warnung

Das Hochladen von schädlichem Code auf ein System ist häufig der erste Schritt, um Code mit der folgenden Absicht auszuführen:

  • Erlangen der vollständigen Kontrolle über ein System.
  • Überlasten eines Systems mit dem Ziel eines Systemausfalls.
  • Kompromittieren von Benutzer- oder Systemdaten
  • Anwenden von Graffiti auf eine öffentliche Benutzeroberfläche.

Wie Sie die Angriffsoberfläche beim Akzeptieren von Dateien von Benutzenden reduzieren, erfahren Sie in den folgenden Artikeln:

Weitere Informationen zur Implementierung von Sicherheitsmaßnahmen, einschließlich Beispiele aus der Beispielanwendung, finden Sie im Abschnitt Validierung.

Speicherszenarien

Zu den allgemeinen Speicheroptionen für Dateien gehören u. a.:

  • Datenbank

    • Beim Hochladen kleiner Dateien ist eine Datenbank oft schneller als physische Speicheroptionen (Dateisystem oder Netzwerkfreigabe).
    • Eine Datenbank ist oft praktischer als physische Speicheroptionen, da das Abrufen eines Datenbank-Datensatzes für Benutzerdaten gleichzeitig den Dateiinhalt (z. B. ein Avatarbild) bereitstellen kann.
    • Eine Datenbank ist potenziell kostengünstiger als die Nutzung eines Datenspeicherdiensts.
  • Physischer Speicher (Dateisystem oder Netzwerkfreigabe)

    • Für das Hochladen großer Dateien:
      • Für die Datenbank geltende Grenzwerte können die Größe des Uploads einschränken.
      • Physischer Speicher ist oft teurer als Datenbankspeicher.
    • Physischer Speicher ist potenziell teurer als die Nutzung eines Datenspeicherdiensts.
    • Der Prozess der App muss Lese- und Schreibberechtigungen für den Speicherort haben. Erteilen Sie niemals die Ausführungsberechtigung.
  • Datenspeicherdienst (z. B. Azure Blob Storage)

    • Dienste bieten in der Regel eine bessere Skalierbarkeit und Resilienz gegenüber lokalen Lösungen, die in der Regel Single Points of Failure aufweisen.
    • Dienste sind bei Szenarien mit großen Speicherinfrastrukturen potenziell kostengünstiger.

    Weitere Informationen finden Sie unter Schnellstart: Erstellen eines Blobs im Objektspeicher mithilfe von .NET. Das Thema veranschaulicht UploadFromFileAsync, aber UploadFromStreamAsync kann verwendet werden, um einen FileStream in Blobspeicher zu speichern, wenn ein Stream verwendet wird.

Szenarien für das Hochladen von Dateien

Zwei allgemeine Ansätze für das Hochladen von Dateien sind Pufferung und Streaming.

Pufferung

Die gesamte Datei wird in eine IFormFile eingelesen, die eine C#-Darstellung der Datei ist, die zum Verarbeiten oder Speichern der Datei verwendet wird.

Welche Ressourcen (Datenträger, Arbeitsspeicher) für das Hochladen von Dateien verwendet werden, ist von der Anzahl und Größe gleichzeitig hochgeladener Dateien abhängig. Wenn eine App versucht, zu viele Uploads zu puffern, stürzt die Website ab, sobald der Arbeitsspeicher oder Speicherplatz auf dem Datenträger ausgelastet ist. Wenn die Größe oder Häufigkeit von Dateiuploads die Ressourcen der App auslastet, wählen Sie Streaming.

Hinweis

Jede einzelne gepufferte Datei, die 64 KB überschreitet, wird aus dem Arbeitsspeicher in eine temporäre Datei auf dem Datenträger verschoben.

Die Pufferung kleiner Dateien wird in den folgenden Abschnitten dieses Themas behandelt:

Streaming

Die Datei wird über eine mehrteilige Anforderung empfangen und von der App direkt verarbeitet oder gespeichert. Streaming verbessert die Leistung nicht wesentlich. Streaming reduziert beim Hochladen von Dateien die Anforderungen an den Arbeitsspeicher oder Speicherplatz auf dem Datenträger.

Das Streamen großer Dateien wird im Abschnitt Hochladen großer Dateien mit Streaming beschrieben.

Hochladen kleiner Dateien mit gepufferten Modellbindungen in physischen Speicher

Zum Hochladen kleiner Dateien können Sie ein mehrteiliges Formular verwenden oder über JavaScript eine POST-Anforderung erstellen.

Das folgende Beispiel veranschaulicht die Verwendung eines Razor Pages-Formulars zum Hochladen einer einzelnen Datei (Pages/BufferedSingleFileUploadPhysical.cshtml in der Beispiel-App):

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

Das folgende Beispiel ist analog zum vorherigen Beispiel, mit der Ausnahme, dass:

  • Die (Fetch-API) von JavaScript zum Senden der Formulardaten verwendet wird.
  • Keine Validierung erfolgt.
<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>

Um den POST-Befehl für das Formular in JavaScript für Clients auszuführen, die die Fetch-API nicht unterstützen, wählen Sie einen der folgenden Ansätze:

  • Verwenden Sie Fetch Polyfill (Beispiel: window.fetch polyfill (github/fetch)).

  • Verwenden Sie XMLHttpRequest. Beispiel:

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

HTML-Formulare müssen den Codierungstyp (enctype) multipart/form-data angeben, damit Dateiuploads unterstützt werden.

Für ein Eingabeelement des Typs files, welches das Hochladen mehrerer Dateien unterstützt, geben Sie das Attribut multiple für das Element <input> an:

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

Auf die einzelnen Dateien, die auf den Server geladen werden, kann über eine Modellbindung mittels IFormFilezugegriffen werden. Die Beispiel-App veranschaulicht mehrere gepufferte Dateiuploads für Szenarien mit Datenbank und physischem Speicher.

Warnung

Verwenden Sie die Eigenschaft FileName von IFormFile, ausschließlich für die Anzeige und Protokollierung. Codieren Sie den Dateinamen für die Anzeige und Protokollierung mit HTML. Ein Angreifer kann einen bösartigen Dateinamen bereitstellen, einschließlich vollständiger oder relativer Pfade. Anwendungen sollten folgende Aktionen ausführen:

  • den Pfad aus dem vom Benutzer angegebenen Dateinamen entfernen
  • den mit HTML codierten Dateinamen, aus dem der Pfad entfernt wurde, für die Benutzeroberfläche oder Protokollierung speichern
  • einen neuen zufälligen Dateinamen für die Speicherung generieren

Mit dem folgenden Code wird der Pfad aus dem Dateinamen entfernt:

string untrustedFileName = Path.GetFileName(pathName);

Bei den bisher vorgestellten Beispielen werden keine Sicherheitsaspekte berücksichtigt. Weitere Informationen finden Sie in den folgenden Abschnitten und in der Beispiel-App:

Beim Hochladen von Dateien mit Modellbindung und IFormFile kann die Aktionsmethode Folgendes akzeptieren:

Hinweis

Zur Bindung werden Formulardateien anhand des Namens abgeglichen. So muss beispielsweise der HTML-Wert name in <input type="file" name="formFile"> mit der C#-Parameter-/Eigenschaftsbindung übereinstimmen (FormFile). Weitere Informationen finden Sie im Abschnitt Abgleichen des Werts des Namensattributs mit dem Parameternamen in der POST-Methode.

Im Beispiel unten geschieht Folgendes:

  • Durchläuft mindestens eine hochgeladene Datei.
  • Verwendet Path.GetTempFileName, um einen vollständigen Pfad für eine Datei samt Dateinamen zurückzugeben.
  • Speichert die Dateien im lokalen Dateisystem mit einem von der App generierten Dateinamen.
  • Gibt die Gesamtanzahl und Größe der hochgeladenen Dateien zurück.
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 });
}

Verwenden Sie Path.GetRandomFileName, um einen Dateinamen ohne Pfad zu generieren. Im folgenden Beispiel wird der Pfad aus der Konfiguration abgerufen:

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

Der an FileStream übergebene Pfad muss den Dateinamen enthalten. Ist dies nicht der Fall, wird zur Laufzeit eine UnauthorizedAccessException ausgelöst.

Dateien, die über die IFormFile-Technik hochgeladen werden, werden vor der Verarbeitung im Arbeitsspeicher oder auf einem Datenträger des Servers gepuffert. Innerhalb der Aktionsmethode können Sie über einen Stream auf die IFormFile-Inhalte zugreifen. Zusätzlich zum lokalen Dateisystem können Dateien in einer Netzwerkfreigabe oder einem Dateispeicherdienst gespeichert werden, wie beispielsweise Azure Blob Storage.

Ein weiteres Beispiel, das mehrere hochzuladende Dateien in einer Schleife durchläuft und sichere Dateinamen verwendet, finden Sie in der Beispiel-App unter Pages/BufferedMultipleFileUploadPhysical.cshtml.cs.

Warnung

Path.GetTempFileName löst eine IOException aus, wenn mehr als 65.535 Dateien erstellt werden, ohne alte temporäre Dateien zu löschen. Der Grenzwert von 65.535 Dateien gilt pro Server. Weitere Informationen zu diesem Grenzwert für Windows-Betriebssysteme finden Sie in den Hinweisen in den folgenden Themen:

Hochladen kleiner Dateien mit gepufferten Modellbindungen in eine Datenbank

Zum Speichern von Binärdateidaten in einer Datenbank über das Entity Framework definieren Sie für die Entität eine Arrayeigenschaft des Typs Byte:

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

Geben Sie eine Seitenmodelleigenschaft für die Klasse an, die eine IFormFile enthält:

public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

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

    ...
}

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

Hinweis

IFormFile kann wie oben dargestellt direkt als Parameter einer Aktionsmethode oder als gebundene Modelleigenschaft verwendet werden. Im vorherigen Beispiel wird eine gebundene Modelleigenschaft verwendet.

FileUpload wird im Razor Pages-Formular verwendet:

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

Wenn das Formular per POST an den Server gesendet wird, kopieren Sie die IFormFile in einen Stream, und speichern Sie ihn als Bytearray in der Datenbank. Im folgenden Beispiel speichert _dbContext den Datenbankkontext der 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();
}

Das vorherige Beispiel ähnelt einem Szenario, das in der Beispiel-App veranschaulicht wird:

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

Warnung

Speichern Sie Binärdaten in relationalen Datenbanken mit Bedacht, da sie Auswirkungen auf die Leistung haben können.

Verlassen Sie sich nicht ohne Validierung auf die FileName-Eigenschaft IFormFile, bzw. vertrauen Sie ihr nicht. Die FileName-Eigenschaft darf nur für Anzeigezwecke und erst nach der HTML-Codierung verwendet werden.

Bei den vorgestellten Beispielen werden keine Sicherheitsaspekte berücksichtigt. Weitere Informationen finden Sie in den folgenden Abschnitten und in der Beispiel-App:

Hochladen von großen Dateien mittels Streaming

Das folgende Beispiel zeigt, wie JavaScript verwendet wird, um eine Datei an eine Controlleraktion zu streamen. Das Fälschungssicherheitstoken einer Datei wird mithilfe eines benutzerdefinierten Filterattributs generiert und an die HTTP-Header des Clients anstelle des Anforderungstexts übergeben. Da die Aktionsmethode die hochgeladenen Daten direkt verarbeitet, wird die Modellbindung des Formulars von einem anderen benutzerdefinierten Filter deaktiviert. Innerhalb der Aktion werden die Inhalte des Formulars über MultipartReader gelesen. Dieses Element liest jede einzelne MultipartSection-Klasse, wodurch die Datei verarbeitet wird oder die Inhalte angemessen gespeichert werden. Nachdem alle mehrteiligen Abschnitte gelesen wurden, führt die Aktion ihre eigene Modellbindung aus.

Die Antwort der Startseite lädt das Formular und speichert das Fälschungssicherheitstoken (über das GenerateAntiforgeryTokenCookieAttribute-Attribut) in einem Cookie (cookie). Das Attribut nutzt die in ASP.NET Core integrierte Unterstützung der Fälschungssicherheit, um ein Cookie (cookie) mit einem Anforderungstoken festzulegen:

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

Das DisableFormValueModelBindingAttribute wird zum Deaktivieren der Modellbindung verwendet:

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

In der Beispiel-App werden GenerateAntiforgeryTokenCookieAttribute und DisableFormValueModelBindingAttribute als Filter auf die Seitenanwendungsmodelle von /StreamedSingleFileUploadDb und /StreamedSingleFileUploadPhysical in Startup.ConfigureServices unter Verwendung der Razor Pages-Konventionen angewendet:

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

Da die Modellbindung das Formular nicht liest, werden Parameter, die über das Formular gebunden werden, nicht gebunden (Abfrage, Route und Header funktionieren weiterhin). Die Aktionsmethode arbeitet direkt mit der Request-Eigenschaft zusammen. Ein MultipartReader wird verwendet, um die verschiedenen Abschnitte zu lesen. Schlüssel-Wert-Daten werden in einem KeyValueAccumulator gespeichert. Nachdem die mehrteiligen Abschnitte gelesen wurden, werden die Inhalte von KeyValueAccumulator verwendet, um die Formulardaten an einen Modelltyp zu binden.

Die vollständige StreamingController.UploadDatabase-Methode für das Streaming an eine Datenbank mit EF Core sieht wie folgt aus:

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

Die vollständige StreamingController.UploadPhysical-Methode für das Streaming an einen physischen Speicherort:

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

In der Beispiel-App werden Validierungsprüfungen von FileHelpers.ProcessStreamedFile übernommen.

Überprüfen

Die FileHelpers-Klasse der Beispiel-App veranschaulicht eine Reihe von Prüfungen für gepufferte IFormFile- und gestreamte Dateiuploads. Informationen zur Verarbeitung von Dateiuploads mit IFormFile-Pufferung in der Beispiel-App finden Sie in der Datei Utilities/FileHelpers.cs in der ProcessFormFile-Methode. Informationen zum Verarbeiten gestreamter Dateien finden Sie in der ProcessStreamedFile-Methode in der gleichen Datei.

Warnung

Die in der Beispiel-App demonstrierten Validierungsverarbeitungsmethoden untersuchen nicht den Inhalt hochgeladener Dateien. In den meisten Produktionsszenarien wird eine API zum Scannen auf Viren/Schadsoftware auf die Datei angewendet, bevor die Datei Benutzern oder anderen Systemen zur Verfügung gestellt wird.

Obwohl das Themenbeispiel ein funktionierendes Beispiel für Validierungstechniken darstellt, implementieren Sie die FileHelpers-Klasse nur unter folgenden Voraussetzungen in einer Produktions-App:

  • Sie verstehen die Implementierung vollständig.
  • Sie ändern die Implementierung entsprechend der Umgebung und den Spezifikationen der App.

Implementieren Sie niemals willkürlich Sicherheitscode in einer App, wenn Sie diese Anforderungen nicht erfüllen.

Validierung von Inhalten

Wenden Sie für hochgeladene Inhalte eine API zum Scannen auf Viren/Schadsoftware von Drittanbietern an.

Das Scannen von Dateien stellt in Szenarien mit hohem Verarbeitungsvolumen hohe Anforderungen an die Serverressourcen. Wenn die Leistung bei der Verarbeitung von Anforderungen durch das Scannen von Dateien beeinträchtigt wird, erwägen Sie, die Scanaufgaben an einen Hintergrunddienst auszulagern, möglicherweise an einen Dienst, der auf einem anderen Server als dem mit der App ausgeführt wird. Üblicherweise werden hochgeladene Dateien in einem Quarantänebereich aufbewahrt, bis der Hintergrundvirenscanner sie prüft. Wenn eine Datei die Prüfung besteht, wird sie an den normalen Speicherort verschoben. Diese Schritte erfolgen in der Regel in Verbindung mit einem Datenbank-Datensatz, der den Scanstatus einer Datei angibt. Bei einem solchen Ansatz bleiben App und App-Server auf die Bearbeitung von Anforderungen ausgerichtet.

Validierung von Dateierweiterungen

Die Erweiterung der hochgeladenen Datei muss mit einer Liste zulässiger Erweiterungen abgeglichen werden. Beispiel:

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
}

Validierung der Dateisignatur

Die Signatur einer Datei wird durch die ersten Bytes am Anfang einer Datei bestimmt. Diese Bytes können verwendet werden, um anzugeben, ob die Erweiterung dem Inhalt der Datei entspricht. Die Beispiel-App überprüft Dateisignaturen auf gängige Dateitypen. Im folgenden Beispiel wird die Dateisignatur eines JPEG-Bilds mit der Datei abgeglichen:

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

Um zusätzliche Dateisignaturen zu erhalten, verwenden Sie eine Datenbank für Dateisignaturen (Google-Suchergebnis) und offizielle Dateispezifikationen. Die Überprüfung der offiziellen Dateispezifikationen kann dazu beitragen, dass die ausgewählten Signaturen gültig sind.

Sicherheit von Dateinamen

Verwenden Sie niemals einen vom Kunden angegebenen Dateinamen zum Speichern einer Datei in physischem Speicher. Erstellen Sie mithilfe von Path.GetRandomFileName oder Path.GetTempFileName einen sicheren Dateinamen für die Datei, um einen vollständigen Pfad (einschließlich des Dateinamens) für die temporäre Speicherung zu erstellen.

Razor versieht anzuzeigende Eigenschaftswerte automatisch mit HTML-Codierung. Der folgende Code kann sicher verwendet werden:

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

Wenden Sie außerhalb von Razor stets HtmlEncode auf Dateinameninhalte aus Benutzeranforderungen an.

Bei vielen Implementierungen muss geprüft werden, ob die Datei existiert. Andernfalls wird die Datei durch eine gleichnamige Datei überschrieben. Stellen Sie zusätzliche Logik bereit, um die Vorgaben Ihrer App zu erfüllen.

Validierung der Größe

Begrenzen Sie die Größe hochgeladener Dateien.

In der Beispiel-App ist die Größe der Datei auf 2 MB begrenzt (angegeben in Bytes). Der Grenzwert wird mittels Konfiguration in der Datei appsettings.json angegeben:

{
  "FileSizeLimit": 2097152
}

FileSizeLimit wird in PageModel-Klassen eingefügt:

public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

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

    ...
}

Wenn eine Dateigröße den Grenzwert überschreitet, wird die Datei abgelehnt:

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

Vergleichen des Werts des Namensattributs mit dem Parameternamen der POST-Methode

In Razor-fremden Formularen, die Formulardaten per POST übermitteln oder direkt FormData von JavaScript verwenden, muss der im Formularelement oder in FormData angegebene Name dem Namen des Parameters in der Aktion des Controllers entsprechen.

Im folgenden Beispiel:

  • Wenn ein <input>-Element verwendet wird, wird das name-Attribut auf den Wert battlePlans festgelegt:

    <input type="file" name="battlePlans" multiple>
    
  • Bei Verwendung von FormData in JavaScript wird der Name auf den Wert battlePlans festgelegt:

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

Verwenden Sie einen übereinstimmenden Namen für den Parameter der C#-Methode (battlePlans):

  • Für eine Razor Pages-Seitenhandlermethode namens Upload:

    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
    
  • Für eine MVC POST-Controlleraktionsmethode:

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

Server- und App-Konfiguration

Grenzwert der Länge von mehrteiligem Text

MultipartBodyLengthLimit legt den Grenzwert der Länge jedes mehrteiligen Texts fest. Formularabschnitte, die diesen Grenzwert überschreiten, lösen beim Analysieren eine InvalidDataException aus. Der Standardwert ist 134.217.728 (128 MB). Passen Sie den Grenzwert mithilfe der Einstellung MultipartBodyLengthLimit in Startup.ConfigureServices an:

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

RequestFormLimitsAttribute dient zum Festlegen des MultipartBodyLengthLimit für eine einzelne Seite oder Aktion.

Wenden Sie in einer Razor Pages-Anwendung den Filter mit einer Konvention in Startup.ConfigureServices an:

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

Wenden Sie in einer Razor Pages- oder MVC-App den Filter auf das Seitenmodell oder auf die Aktionsmethode an:

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

Maximale Größe des Anforderungstexts für Kestrel

Die maximale Größe des Anforderungstexts beträgt für von Kestrel gehostete Apps standardmäßig 30.000.000 Bytes, also ungefähr 28,6 MB. Passen Sie den Grenzwert mithilfe der Kestrel-Serveroption MaxRequestBodySize an:

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 dient zum Festlegen von MaxRequestBodySize für eine einzelne Seite oder Aktion.

Wenden Sie in einer Razor Pages-Anwendung den Filter mit einer Konvention in Startup.ConfigureServices an:

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

Wenden Sie in einer Razor Pages- oder MVC-App den Filter auf die Seitenhandlerklasse oder Aktionsmethode an:

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

Weitere Grenzwerte für Kestrel

Für von Kestrel gehostete Apps können noch andere Kestrel-Grenzwerte gelten:

IIS

Der Standardgrenzwert für Anforderungen (maxAllowedContentLength) beträgt standardmäßig 30.000.000 Bytes, also ungefähr 28,6 MB. Passen Sie den Grenzwert in der Datei web.config an. Im folgenden Beispiel wird der Grenzwert auf 50 MB (52.428.800 Bytes) festgelegt:

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

Die Einstellung maxAllowedContentLength gilt nur für IIS. Weitere Informationen finden Sie unter Request Limits <requestLimits>.

Erhöhen Sie die maximale Anforderungstextgröße für die HTTP-Anforderung durch Festlegen von IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices. Im folgenden Beispiel wird der Grenzwert auf 50 MB (52.428.800 Bytes) festgelegt:

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

Weitere Informationen finden Sie unter Hosten von ASP.NET Core unter Windows mit IIS.

Problembehandlung

Nachfolgend werden einige häufig auftretenden Probleme aufgeführt, die entstehen können, wenn Dateien hochgeladen werden. Außerdem wird erläutert, wie Sie diese Probleme beheben können.

Fehler „Nicht gefunden“ bei Bereitstellung auf einem IIS-Server

Der folgende Fehler gibt an, dass die hochgeladene Datei die konfigurierte Inhaltslänge des Servers überschreitet:

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

Weitere Informationen finden Sie im Abschnitt IIS.

Verbindungsfehler

Ein Verbindungsfehler und eine zurückgesetzte Serververbindung deuten wahrscheinlich darauf hin, dass die hochgeladene Datei die maximale Anforderungstextgröße von Kestrel überschreitet. Weitere Informationen finden Sie im Abschnitt Maximale Größe des Anforderungstexts für Kestrel. Die Verbindungsgrenzwerte für Kestrel-Clients müssen ggf. ebenfalls angepasst werden.

Ausnahme bei möglichem NULL-Verweis mit IFormFile

Wenn der Controller hochgeladene Dateien mit IFormFile akzeptiert, der Wert aber null ist, bestätigen Sie, dass das HTML-Formular den enctype-Wert multipart/form-data angibt. Wenn dieses Attribut für das <form>-Element festgelegt ist, werden keine Dateien hochgeladen, und alle gebundenen IFormFile-Argumente sind null. Bestätigen Sie auch, dass die Uploadbenennung in den Formulardaten mit der Benennung der App übereinstimmt.

Stream war zu lang.

Bei den Beispielen in diesem Thema wird davon ausgegangen, dass MemoryStream den Inhalt der hochgeladenen Datei enthält. Die maximale Größe für einen MemoryStream beträgt int.MaxValue. Wenn das Dateiuploadszenario der App das Speichern von Dateiinhalten mit einer Größe über 50 MB erfordert, verwenden Sie einen alternativen Ansatz, der nicht auf einem einzelnen MemoryStream zum Speichern des Inhalts einer hochgeladenen Datei basiert.

Zusätzliche Ressourcen