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.
- Für das Hochladen großer Dateien:
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:
- Eine einzelne IFormFile.
- Eine der folgenden Sammlungen, die mehrere Dateien darstellen:
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 dasname
-Attribut auf den WertbattlePlans
festgelegt:<input type="file" name="battlePlans" multiple>
Bei Verwendung von
FormData
in JavaScript wird der Name auf den WertbattlePlans
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.
- Für das Hochladen großer Dateien:
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:
- Eine einzelne IFormFile.
- Eine der folgenden Sammlungen, die mehrere Dateien darstellen:
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 dasname
-Attribut auf den WertbattlePlans
festgelegt:<input type="file" name="battlePlans" multiple>
Bei Verwendung von
FormData
in JavaScript wird der Name auf den WertbattlePlans
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.
- Für das Hochladen großer Dateien:
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:
- Eine einzelne IFormFile.
- Eine der folgenden Sammlungen, die mehrere Dateien darstellen:
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 dasname
-Attribut auf den WertbattlePlans
festgelegt:<input type="file" name="battlePlans" multiple>
Bei Verwendung von
FormData
in JavaScript wird der Name auf den WertbattlePlans
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
- Unrestricted File Upload (Uneingeschränkter Dateiupload)
- Sicherheitsrahmen: Eingabeüberprüfung | Risikominderung (Azure-Sicherheit)
- Muster „Valet-Schlüssel“ (Azure-Cloudentwurfsmuster)