Nahrání souborů v ASP.NET Core
Autor: Rutger Storm
ASP.NET Core podporuje nahrávání jednoho nebo více souborů pomocí vazby modelu ve vyrovnávací paměti pro menší soubory a neuložení streamovaných souborů u větších souborů.
Zobrazení nebo stažení ukázkového kódu (postup stažení)
Bezpečnostní aspekty
Při poskytování uživatelům, kteří mají možnost nahrávat soubory na server, buďte opatrní. Cyberattackers se může pokusit:
- Spusťte útoky na odepření služeb .
- Nahrajte viry nebo malware.
- Jiné způsoby narušují sítě a servery.
Bezpečnostní kroky, které snižují pravděpodobnost úspěšného útoku:
- Nahrajte soubory do vyhrazené oblasti pro nahrávání souborů, nejlépe na systémovou jednotku. Vyhrazené umístění usnadňuje ukládání bezpečnostních omezení u nahraných souborů. Zakázání oprávnění ke spuštění v umístění pro nahrání souboru.†
- Neuchovávat nahrané soubory ve stejném adresářovém stromu jako app.†
- Použijte bezpečný název souboru určený aplikací. Nepoužívejte název souboru zadaný uživatelem ani nedůvěryhodným názvem souboru nahraného souboru.† HTML kódujte při jeho zobrazení nedůvěryhodný název souboru. Například protokolování názvu souboru nebo zobrazení v uživatelském rozhraní (Razor automaticky kóduje výstup HTML).
- Povolit pouze schválené přípony souborů pro specifikaci návrhu aplikace.†
- Ověřte, zda jsou kontroly na straně klienta provedeny na server.† Kontroly na straně klienta jsou snadné obejít.
- Zkontrolujte velikost nahraného souboru. Nastavení maximálního limitu velikosti, aby se zabránilo velkým nahráváním.†
- Pokud by soubory neměly být přepsány nahraným souborem se stejným názvem, před nahráním souboru zkontrolujte název souboru v databázi nebo fyzickém úložišti.
- Před uložením souboru spusťte skener virů nebo malwaru na nahraný obsah.
†Vzoreková aplikace ukazuje přístup, který splňuje kritéria.
Upozorňující
Nahrání škodlivého kódu do systému je často prvním krokem ke spuštění kódu, který může:
- Zcela získejte kontrolu nad systémem.
- Přetěžte systém výsledkem chybového ukončení systému.
- Ohrožení uživatelských nebo systémových dat
- Použití graffiti na veřejné uživatelské rozhraní
Informace o omezení ohrožení zabezpečení při přijímání souborů od uživatelů najdete v následujících zdrojích informací:
Další informace o implementaci bezpečnostních opatření, včetně příkladů z ukázkové aplikace, najdete v části Ověření .
Scénáře úložiště
Mezi běžné možnosti úložiště pro soubory patří:
Databáze
- U malých nahrávek souborů je databáze často rychlejší než možnosti fyzického úložiště (systému souborů nebo síťové sdílené složky).
- Databáze je často pohodlnější než možnosti fyzického úložiště, protože načtení záznamu databáze pro uživatelská data může současně poskytnout obsah souboru (například obrázek avataru).
- Databáze je potenciálně levnější než použití cloudové služby úložiště dat.
Fyzické úložiště (systém souborů nebo síťová sdílená složka)
- Pro velké nahrávání souborů:
- Limity databáze můžou omezit velikost nahrávání.
- Fyzické úložiště je často méně ekonomické než úložiště v databázi.
- Fyzické úložiště je potenciálně levnější než použití cloudové služby úložiště dat.
- Proces aplikace musí mít oprávnění ke čtení a zápisu do umístění úložiště. Nikdy neudělte oprávnění ke spuštění.
- Pro velké nahrávání souborů:
Cloudová služba úložiště dat, například Azure Blob Storage.
- Služby obvykle nabízejí lepší škálovatelnost a odolnost před místními řešeními, která obvykle podléhají kritickým bodům selhání.
- Služby jsou potenciálně nižší náklady ve scénářích infrastruktury velkých úložišť.
Další informace najdete v tématu Rychlý start: Použití .NET k vytvoření objektu blob v úložišti objektů.
Malé a velké soubory
Definice malých a velkých souborů závisí na dostupných výpočetních prostředcích. Aplikace by měly testovat přístup k úložišti, který se používá k zajištění toho, aby zvládly očekávané velikosti. Proveďte srovnávací testy výkonu paměti, procesoru, disku a databáze.
I když pro vaše nasazení není možné zadat konkrétní hranice, které jsou malé a velké, tady je několik výchozích hodnot FormOptions
souvisejících s ASP.NET Core (dokumentace k rozhraní API):
- Ve výchozím nastavení neukládá celý text požadavku doBufferBody vyrovnávací paměti,
HttpRequest.Form
ale uloží do vyrovnávací paměti všechny zahrnuté soubory formulářů s více částmi. - MultipartBodyLengthLimit je maximální velikost souborů ve formátu vyrovnávací paměti (výchozí hodnota: 128 MB).
- MemoryBufferThreshold označuje prahovou hodnotu ukládání do vyrovnávací paměti před přechodem na soubor vyrovnávací paměti na disku (výchozí hodnota: 64 kB).
MemoryBufferThreshold
funguje jako hranice mezi malými a velkými soubory, která se vyvolává nebo snižuje v závislosti na prostředcích a scénářích aplikací.
Další informace o FormOptions, viz FormOptions
třída v ASP.NET Core referenční zdroj.
Poznámka:
Odkazy na dokumentaci k referenčnímu zdroji .NET obvykle načítají výchozí větev úložiště, která představuje aktuální vývoj pro příští verzi .NET. Pokud chcete vybrat značku pro konkrétní verzi, použijte rozevírací seznam pro přepnutí větví nebo značek. Další informace najdete v tématu Jak vybrat značku verze zdrojového kódu ASP.NET Core (dotnet/AspNetCore.Docs #26205).
Scénáře nahrávání souborů
Dva obecné přístupy pro nahrávání souborů jsou ukládání do vyrovnávací paměti a streamování.
Vyrovnávání
Celý soubor se načte do souboru IFormFile. IFormFile
je reprezentace souboru jazyka C#, který se používá ke zpracování nebo uložení souboru.
Disk a paměť používané nahráváním souborů závisí na počtu a velikosti souběžných nahrávání souborů. Pokud se aplikace pokusí uložit příliš mnoho nahrávek do vyrovnávací paměti, dojde k chybovému ukončení webu při nedostatku paměti nebo místa na disku. Pokud velikost nebo frekvence nahrávání souborů vyčerpává prostředky aplikace, použijte streamování.
Všechny soubory s jednou vyrovnávací pamětí větší než 64 kB se přesunou z paměti do dočasného souboru na disku.
Dočasné soubory pro větší požadavky se zapíšou do umístění pojmenovaného ASPNETCORE_TEMP
v proměnné prostředí. Pokud ASPNETCORE_TEMP
není definován, soubory se zapíšou do dočasné složky aktuálního uživatele.
Ukládání malých souborů do vyrovnávací paměti je popsáno v následujících částech tohoto tématu:
Streamování
Soubor se přijímá z žádosti o více částí a aplikace ho přímo zpracuje nebo uloží. Streamování výrazně nezlepší výkon. Streamování snižuje požadavky na paměť nebo místo na disku při nahrávání souborů.
Streamování velkých souborů je popsáno v části Nahrání velkých souborů pomocí oddílu streamování .
Nahrání malých souborů s vazbou modelu do vyrovnávací paměti do fyzického úložiště
Pokud chcete nahrát malé soubory, použijte formulář s více částmi nebo vytvořte požadavek POST pomocí JavaScriptu.
Následující příklad ukazuje použití Razor formuláře Pages k nahrání jednoho souboru (Pages/BufferedSingleFileUploadPhysical.cshtml
v ukázkové aplikaci):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file" />
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
Následující příklad je podobný předchozímu příkladu s tím rozdílem, že:
- JavaScript (Fetch API) slouží k odeslání dat formuláře.
- Neexistuje žádné ověření.
<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>
<input class="btn" type="submit" value="Upload" />
<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>
<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.href = '/';
}
resultElement.value = 'Result: ' + response.status + ' ' +
response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>
K provedení formuláře POST v JavaScriptu pro klienty, kteří nepodporují rozhraní Fetch API, použijte jeden z následujících přístupů:
Použijte funkci Fetch Polyfill (například window.fetch polyfill (github/fetch)).
Použijte
XMLHttpRequest
. Příklad:<script> "use strict"; function AJAXSubmit (oFormElement) { var oReq = new XMLHttpRequest(); oReq.onload = function(e) { oFormElement.elements.namedItem("result").value = 'Result: ' + this.status + ' ' + this.statusText; }; oReq.open("post", oFormElement.action); oReq.send(new FormData(oFormElement)); } </script>
Aby bylo možné podporovat nahrávání souborů, musí formuláře HTML určovat typ kódování (enctype
) .multipart/form-data
files
Vstupní prvek pro podporu nahrávání více souborů poskytuje multiple
atribut elementu<input>
:
<input asp-for="FileUpload.FormFiles" type="file" multiple />
Jednotlivé soubory nahrané na server lze získat přístup prostřednictvím vazby modelu pomocí IFormFile. Ukázková aplikace ukazuje několik nahrávání souborů do vyrovnávací paměti pro scénáře databáze a fyzického úložiště.
Upozorňující
FileName
Nepoužívejte vlastnost jiné než IFormFile pro zobrazení a protokolování. Při zobrazení nebo protokolování kóduje kód HTML název souboru. Cyberattacker může poskytnout škodlivý název souboru, včetně úplných cest nebo relativních cest. Aplikace by měly:
- Odeberte cestu ze souboru zadaného uživatelem.
- Uložte název souboru s kódováním HTML nebo odebranou cestou pro uživatelské rozhraní nebo protokolování.
- Vygenerujte nový náhodný název souboru pro úložiště.
Následující kód odebere cestu z názvu souboru:
string untrustedFileName = Path.GetFileName(pathName);
Zatím uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:
Při nahrávání souborů pomocí vazby modelu a IFormFilemetoda akce může přijmout:
- Jeden .IFormFile
- Některé z následujících kolekcí, které představují několik souborů:
Poznámka:
Vazba odpovídá souborům formuláře podle názvu. Například hodnota HTML name
musí <input type="file" name="formFile">
odpovídat vázanémuFormFile
parametru nebo vlastnosti jazyka C#. Další informace naleznete v části Match name attribute value to parameter name name of POST method section.
Následující příklad:
- Smyčky procházejí jedním nebo více nahranými soubory.
- Pomocí Path.GetTempFileName vrátí úplnou cestu k souboru, včetně názvu souboru.
- Uloží soubory do místního systému souborů pomocí názvu souboru vygenerovaného aplikací.
- Vrátí celkový počet a velikost nahraných souborů.
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size });
}
Slouží Path.GetRandomFileName
k vygenerování názvu souboru bez cesty. V následujícím příkladu se cesta získá z konfigurace:
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
Cesta předaná do souboru FileStream musí obsahovat název souboru. Pokud není zadaný název souboru, UnauthorizedAccessException vyvolá se za běhu.
Soubory nahrané pomocí IFormFile techniky se před zpracováním ukládají do vyrovnávací paměti nebo na disku na serveru. Uvnitř metody IFormFile akce je obsah přístupný jako Stream. Kromě místního systému souborů je možné soubory ukládat do sdílené síťové složky nebo do služby úložiště souborů, jako je azure Blob Storage.
Další příklad, který pro nahrání smyčuje více souborů a používá bezpečné názvy souborů, najdete Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
v ukázkové aplikaci.
Upozorňující
Path.GetTempFileName vyvolá chybu IOException , pokud se vytvoří více než 65 535 souborů bez odstranění předchozích dočasných souborů. Limit 65 535 souborů je limit pro jednotlivé servery. Další informace o tomto limitu operačního systému Windows najdete v poznámkách v následujících tématech:
Nahrání malých souborů s vazbou modelu ve vyrovnávací paměti do databáze
Pokud chcete ukládat data binárního souboru do databáze pomocí Entity Frameworku, definujte Byte vlastnost pole u entity:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
Zadejte vlastnost modelu stránky pro třídu, která obsahuje :IFormFile
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
Poznámka:
IFormFile lze použít přímo jako parametr metody akce nebo jako vlastnost vázaného modelu. Předchozí příklad používá vlastnost vázaného modelu.
Používá se FileUpload
ve formuláři Razor Stránky:
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
Když je formulář poSTed na server, zkopírujte IFormFile ho do datového proudu a uložte ho jako pole bajtů v databázi. V následujícím příkladu _dbContext
uloží kontext databáze aplikace:
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);
// Upload the file if less than 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}
Předchozí příklad je podobný scénáři popsanému v ukázkové aplikaci:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
Upozorňující
Při ukládání binárních dat v relačních databázích buďte opatrní, protože to může nepříznivě ovlivnit výkon.
Nespoléhejte na vlastnost IFormFile bez ověření ani ji nedůvěřujteFileName
. Vlastnost FileName
by měla být použita pouze pro účely zobrazení a pouze po kódování HTML.
Uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:
Nahrávání velkých souborů se streamováním
Příklad 3.1 ukazuje, jak pomocí JavaScriptu streamovat soubor do akce kontroleru. Antiforgery token souboru se vygeneruje pomocí atributu vlastního filtru a předá se hlavičkám HTTP klienta místo v textu požadavku. Vzhledem k tomu, že metoda akce zpracovává nahraná data přímo, je vazba modelu formuláře zakázána jiným vlastním filtrem. Obsah formuláře se v rámci akce čte pomocí MultipartReader
souboru, který čte jednotlivé soubory MultipartSection
nebo ukládá obsah podle potřeby. Po přečtení oddílů s více částmi provede akce vlastní vazbu modelu.
Počáteční odpověď stránky načte formulář a uloží antiforgery token do objektu cookie (prostřednictvím atributu GenerateAntiforgeryTokenCookieAttribute
). Tento atribut používá k nastavení cookie tokenu požadavku integrovanou podporu antiforgery ASP.NET Core:
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
// Send the request token as a JavaScript-readable cookie
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
public override void OnResultExecuted(ResultExecutedContext context)
{
}
}
Slouží DisableFormValueModelBindingAttribute
k zakázání vazby modelu:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
V ukázkové aplikaci GenerateAntiforgeryTokenCookieAttribute
a DisableFormValueModelBindingAttribute
použijí se jako filtry pro modely /StreamedSingleFileUploadDb
stránkovací aplikace a /StreamedSingleFileUploadPhysical
při Startup.ConfigureServices
používání Razor konvencí Pages:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
});
Vzhledem k tomu, že vazba modelu nečte formulář, parametry vázané z formuláře se neváže (dotaz, trasa a hlavička budou nadále fungovat). Metoda akce funguje přímo s Request
vlastností. A MultipartReader
slouží ke čtení jednotlivých oddílů. Data klíče/hodnoty jsou uložena KeyValueAccumulator
v souboru . Po přečtení oddílů s více částmi se obsah formuláře KeyValueAccumulator
použije k vytvoření vazby dat formuláře na typ modelu.
Úplná StreamingController.UploadDatabase
metoda streamování do databáze pomocí EF Core:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
// Accumulate the form data key-value pairs in the request (formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage = contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
streamedFileContent =
await FileHelpers.ProcessStreamedFile(section, contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);
if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}
formAccumulator.Append(key, value);
if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 3).");
// Log error
return BadRequest(ModelState);
}
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
// Bind form data to the model
var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);
if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error
return BadRequest(ModelState);
}
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.
var file = new AppFile()
{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};
_context.File.Add(file);
await _context.SaveChangesAsync();
return Created(nameof(StreamingController), null);
}
MultipartRequestHelper
(Utilities/MultipartRequestHelper.cs
):
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace SampleApp.Utilities
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}
Úplná StreamingController.UploadPhysical
metoda streamování do fyzického umístění:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
V ukázkové aplikaci se kontroly ověření zpracovávají FileHelpers.ProcessStreamedFile
.
Ověřování
Třída ukázkové aplikace FileHelpers
ukazuje několik kontrol ukládání do IFormFile vyrovnávací paměti a nahrání streamovaných souborů. Informace o zpracování IFormFile nahrávání souborů ve vyrovnávací paměti v ukázkové aplikaci najdete v ProcessFormFile
metodě v Utilities/FileHelpers.cs
souboru. Pokud chcete zpracovávat streamované soubory, podívejte se na metodu ProcessStreamedFile
ve stejném souboru.
Upozorňující
Metody zpracování ověřování, které jsou ukázány v ukázkové aplikaci, nekontroluje obsah nahraných souborů. Ve většině produkčních scénářů se v souboru před zpřístupněním souboru uživatelům nebo jiným systémům používá rozhraní API pro kontrolu virů a malwaru.
I když ukázka tématu poskytuje funkční příklad technik ověřování, neimplementujte FileHelpers
třídu v produkční aplikaci, pokud:
- Plně porozumíte implementaci.
- Upravte implementaci podle potřeby pro prostředí a specifikace aplikace.
Nikdy nerozlišují implementaci bezpečnostního kódu v aplikaci bez řešení těchto požadavků.
Ověření obsahu
K nahrání obsahu použijte rozhraní API pro kontrolu virů nebo malwaru třetí strany.
Skenování souborů je náročné na serverové prostředky ve scénářích s velkým objemem. Pokud se sníží výkon zpracování požadavků z důvodu prohledávání souborů, zvažte snížení zátěže skenovací práce do služby na pozadí, případně služby spuštěné na serveru, který se liší od serveru aplikace. Nahrané soubory se obvykle uchovávají v karanténě, dokud je antivirový skener na pozadí nekontroluje. Když soubor projde, soubor se přesune do normálního umístění úložiště souborů. Tyto kroky se obvykle provádějí ve spojení se záznamem databáze, který označuje stav kontroly souboru. Díky takovému přístupu zůstává aplikace a aplikační server zaměřeny na reakci na požadavky.
Ověření přípony souboru
Přípona nahraného souboru by se měla kontrolovat v seznamu povolených přípon. Příklad:
private string[] permittedExtensions = { ".txt", ".pdf" };
var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}
Ověření podpisu souboru
Podpis souboru je určen několika prvními bajty na začátku souboru. Tyto bajty lze použít k označení, jestli přípona odpovídá obsahu souboru. Ukázková aplikace kontroluje podpisy souborů u několika běžných typů souborů. V následujícím příkladu je podpis souboru pro obrázek JPEG zkontrolován proti souboru:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
Chcete-li získat další podpisy souborů, použijte databázi podpisů souborů (výsledek vyhledávání Google) a oficiální specifikace souborů. Specifikace oficiálního souboru konzultace mohou zajistit, aby vybrané podpisy byly platné.
Zabezpečení názvu souboru
Nikdy nepoužívejte název souboru zadaného klientem k uložení souboru do fyzického úložiště. Vytvořte pro soubor bezpečný název souboru pomocí Path.GetRandomFileName nebo Path.GetTempFileName a vytvořte úplnou cestu (včetně názvu souboru) pro dočasné úložiště.
Razor automaticky kóduje hodnoty vlastností pro zobrazení. Následující kód je bezpečný pro použití:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
RazorMimo text , vždy HtmlEncode název souboru obsahu z žádosti uživatele.
Mnoho implementací musí obsahovat kontrolu, že soubor existuje; v opačném případě se soubor přepíše souborem se stejným názvem. Zadejte další logiku, která vyhovuje specifikacím vaší aplikace.
Ověření velikosti
Omezte velikost nahraných souborů.
V ukázkové aplikaci je velikost souboru omezená na 2 MB (uvedené v bajtech). Limit se poskytuje prostřednictvím konfigurace ze appsettings.json
souboru:
{
"FileSizeLimit": 2097152
}
Vloží se FileSizeLimit
do PageModel
tříd:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
Pokud velikost souboru překročí limit, soubor se odmítne:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
Shoda s hodnotou atributu name s názvem parametru metody POST
V jinýchRazor formách, než jsou data formuláře POST nebo přímo používají JavaScript FormData
, název zadaný v elementu formuláře nebo FormData
se musí shodovat s názvem parametru v akci kontroleru.
V následujícím příkladu:
Při použití elementu
<input>
name
je atribut nastaven na hodnotubattlePlans
:<input type="file" name="battlePlans" multiple>
Při použití
FormData
v JavaScriptu je název nastaven na hodnotubattlePlans
:var formData = new FormData(); for (var file in files) { formData.append("battlePlans", file, file.name); }
Pro parametr metody jazyka C# použijte odpovídající název (battlePlans
):
Pro metodu obslužné Razor rutiny stránky Pages s názvem
Upload
:public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
Metoda akce kontroleru MVC POST:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
Konfigurace serveru a aplikace
Omezení délky těla s více částmi
MultipartBodyLengthLimit nastaví limit pro délku každého vícedílného těla. Oddíly formuláře, které tento limit překračují, při analýze vyvolá výjimku InvalidDataException . Výchozí hodnota je 134 217 728 (128 MB). Přizpůsobení limitu pomocí nastavení vStartup.ConfigureServices
:MultipartBodyLengthLimit
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute slouží k nastavení MultipartBodyLengthLimit jedné stránky nebo akce.
Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
});
Razor V aplikaci Pages nebo aplikaci MVC použijte filtr na model stránky nebo metodu akce:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Kestrel maximální velikost textu požadavku
U aplikací hostovaných ve Kestrelvýchozím nastavení je maximální velikost textu požadavku 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit pomocí možnosti Serveru MaxRequestBodySizeKestrel :
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel((context, options) =>
{
// Handle requests up to 50 MB
options.Limits.MaxRequestBodySize = 52428800;
})
.UseStartup<Startup>();
});
RequestSizeLimitAttribute slouží k nastavení MaxRequestBodySize pro jednu stránku nebo akci.
Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});
Razor V aplikaci stránky nebo aplikaci MVC použijte filtr na třídu obslužné rutiny stránky nebo metodu akce:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Lze RequestSizeLimitAttribute
také použít direktivu @attribute
Razor :
@attribute [RequestSizeLimitAttribute(52428800)]
Další Kestrel limity
Jiné Kestrel limity se můžou vztahovat na aplikace hostované:Kestrel
IIS
Výchozí limit požadavků (maxAllowedContentLength
) je 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit v web.config
souboru. V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
Nastavení maxAllowedContentLength
platí jenom pro službu IIS. Další informace naleznete v tématu Omezení <requestLimits>
požadavků .
Odstraňování potíží
Níže jsou uvedeny některé běžné problémy, ke kterým dochází při práci s nahráváním souborů a jejich možných řešení.
Chyba Nenalezena při nasazení na server služby IIS
Následující chyba značí, že nahraný soubor překračuje nakonfigurovanou délku obsahu serveru:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
Další informace najdete v části IIS .
Chyba připojení
Chyba připojení a připojení k resetování serveru pravděpodobně značí, že nahraný soubor překračuje Kestrelmaximální velikost textu požadavku. Další informace najdete v Kestrel části Maximální velikost textu požadavku. Kestrel Limity připojení klienta mohou vyžadovat také úpravu.
Výjimka odkazu null se souborem IFormFile
Pokud kontroler přijímá nahrané soubory pomocí IFormFile , ale hodnota je null
, potvrďte, že formulář HTML určuje enctype
hodnotu multipart/form-data
. Pokud tento atribut není nastaven na <form>
elementu, nahraje se soubor a žádné vázané IFormFile argumenty jsou null
. Ověřte také, že pojmenování nahrávání v datech formuláře odpovídá pojmenování aplikace.
Stream byl příliš dlouhý.
Příklady v tomto tématu se spoléhají na MemoryStream uložení obsahu nahraného souboru. Limit velikosti je MemoryStream
int.MaxValue
. Pokud scénář nahrávání souborů aplikace vyžaduje uchovávání obsahu souborů větší než 50 MB, použijte alternativní přístup, který nespoléhá na jediný MemoryStream
obsah nahraného souboru.
ASP.NET Core podporuje nahrávání jednoho nebo více souborů pomocí vazby modelu ve vyrovnávací paměti pro menší soubory a neuložení streamovaných souborů u větších souborů.
Zobrazení nebo stažení ukázkového kódu (postup stažení)
Bezpečnostní aspekty
Při poskytování uživatelům, kteří mají možnost nahrávat soubory na server, buďte opatrní. Cyberattackers se může pokusit:
- Spusťte útoky na odepření služeb .
- Nahrajte viry nebo malware.
- Jiné způsoby narušují sítě a servery.
Bezpečnostní kroky, které snižují pravděpodobnost úspěšného útoku:
- Nahrajte soubory do vyhrazené oblasti pro nahrávání souborů, nejlépe na systémovou jednotku. Vyhrazené umístění usnadňuje ukládání bezpečnostních omezení u nahraných souborů. Zakázání oprávnění ke spuštění v umístění pro nahrání souboru.†
- Neuchovávat nahrané soubory ve stejném adresářovém stromu jako app.†
- Použijte bezpečný název souboru určený aplikací. Nepoužívejte název souboru zadaný uživatelem ani nedůvěryhodným názvem souboru nahraného souboru.† HTML kódujte při jeho zobrazení nedůvěryhodný název souboru. Například protokolování názvu souboru nebo zobrazení v uživatelském rozhraní (Razor automaticky kóduje výstup HTML).
- Povolit pouze schválené přípony souborů pro specifikaci návrhu aplikace.†
- Ověřte, zda jsou kontroly na straně klienta provedeny na server.† Kontroly na straně klienta jsou snadné obejít.
- Zkontrolujte velikost nahraného souboru. Nastavení maximálního limitu velikosti, aby se zabránilo velkým nahráváním.†
- Pokud by soubory neměly být přepsány nahraným souborem se stejným názvem, před nahráním souboru zkontrolujte název souboru v databázi nebo fyzickém úložišti.
- Před uložením souboru spusťte skener virů nebo malwaru na nahraný obsah.
†Vzoreková aplikace ukazuje přístup, který splňuje kritéria.
Upozorňující
Nahrání škodlivého kódu do systému je často prvním krokem ke spuštění kódu, který může:
- Zcela získejte kontrolu nad systémem.
- Přetěžte systém výsledkem chybového ukončení systému.
- Ohrožení uživatelských nebo systémových dat
- Použití graffiti na veřejné uživatelské rozhraní
Informace o omezení ohrožení zabezpečení při přijímání souborů od uživatelů najdete v následujících zdrojích informací:
Další informace o implementaci bezpečnostních opatření, včetně příkladů z ukázkové aplikace, najdete v části Ověření .
Scénáře úložiště
Mezi běžné možnosti úložiště pro soubory patří:
Databáze
- U malých nahrávek souborů je databáze často rychlejší než možnosti fyzického úložiště (systému souborů nebo síťové sdílené složky).
- Databáze je často pohodlnější než možnosti fyzického úložiště, protože načtení záznamu databáze pro uživatelská data může současně poskytnout obsah souboru (například obrázek avataru).
- Databáze je potenciálně levnější než použití služby úložiště dat.
Fyzické úložiště (systém souborů nebo síťová sdílená složka)
- Pro velké nahrávání souborů:
- Limity databáze můžou omezit velikost nahrávání.
- Fyzické úložiště je často méně ekonomické než úložiště v databázi.
- Fyzické úložiště je potenciálně levnější než použití služby úložiště dat.
- Proces aplikace musí mít oprávnění ke čtení a zápisu do umístění úložiště. Nikdy neudělte oprávnění ke spuštění.
- Pro velké nahrávání souborů:
Služba úložiště dat (například Azure Blob Storage)
- Služby obvykle nabízejí lepší škálovatelnost a odolnost před místními řešeními, která obvykle podléhají kritickým bodům selhání.
- Služby jsou potenciálně nižší náklady ve scénářích infrastruktury velkých úložišť.
Další informace najdete v tématu Rychlý start: Použití .NET k vytvoření objektu blob v úložišti objektů.
Scénáře nahrávání souborů
Dva obecné přístupy pro nahrávání souborů jsou ukládání do vyrovnávací paměti a streamování.
Vyrovnávání
Celý soubor se načte do IFormFilesouboru, což je reprezentace souboru jazyka C#, který se používá ke zpracování nebo uložení souboru.
Prostředky (disk, paměť) používané nahráváním souborů závisí na počtu a velikosti souběžných nahrávání souborů. Pokud se aplikace pokusí uložit příliš mnoho nahrávek do vyrovnávací paměti, dojde k chybovému ukončení webu při nedostatku paměti nebo místa na disku. Pokud velikost nebo frekvence nahrávání souborů vyčerpává prostředky aplikace, použijte streamování.
Poznámka:
Všechny soubory s jednou vyrovnávací pamětí větší než 64 kB se přesunou z paměti do dočasného souboru na disku.
Ukládání malých souborů do vyrovnávací paměti je popsáno v následujících částech tohoto tématu:
Streamování
Soubor se přijímá z žádosti o více částí a aplikace ho přímo zpracuje nebo uloží. Streamování výrazně nezlepší výkon. Streamování snižuje požadavky na paměť nebo místo na disku při nahrávání souborů.
Streamování velkých souborů je popsáno v části Nahrání velkých souborů pomocí oddílu streamování .
Nahrání malých souborů s vazbou modelu do vyrovnávací paměti do fyzického úložiště
Pokud chcete nahrát malé soubory, použijte formulář s více částmi nebo vytvořte požadavek POST pomocí JavaScriptu.
Následující příklad ukazuje použití Razor formuláře Pages k nahrání jednoho souboru (Pages/BufferedSingleFileUploadPhysical.cshtml
v ukázkové aplikaci):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
Následující příklad je podobný předchozímu příkladu s tím rozdílem, že:
- JavaScript (Fetch API) slouží k odeslání dat formuláře.
- Neexistuje žádné ověření.
<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>
<input class="btn" type="submit" value="Upload" />
<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>
<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.href = '/';
}
resultElement.value = 'Result: ' + response.status + ' ' +
response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>
K provedení formuláře POST v JavaScriptu pro klienty, kteří nepodporují rozhraní Fetch API, použijte jeden z následujících přístupů:
Použijte funkci Fetch Polyfill (například window.fetch polyfill (github/fetch)).
Použijte
XMLHttpRequest
. Příklad:<script> "use strict"; function AJAXSubmit (oFormElement) { var oReq = new XMLHttpRequest(); oReq.onload = function(e) { oFormElement.elements.namedItem("result").value = 'Result: ' + this.status + ' ' + this.statusText; }; oReq.open("post", oFormElement.action); oReq.send(new FormData(oFormElement)); } </script>
Aby bylo možné podporovat nahrávání souborů, musí formuláře HTML určovat typ kódování (enctype
) .multipart/form-data
files
Vstupní prvek pro podporu nahrávání více souborů poskytuje multiple
atribut elementu<input>
:
<input asp-for="FileUpload.FormFiles" type="file" multiple>
Jednotlivé soubory nahrané na server lze získat přístup prostřednictvím vazby modelu pomocí IFormFile. Ukázková aplikace ukazuje několik nahrávání souborů do vyrovnávací paměti pro scénáře databáze a fyzického úložiště.
Upozorňující
FileName
Nepoužívejte vlastnost jiné než IFormFile pro zobrazení a protokolování. Při zobrazení nebo protokolování kóduje kód HTML název souboru. Cyberattacker může poskytnout škodlivý název souboru, včetně úplných cest nebo relativních cest. Aplikace by měly:
- Odeberte cestu ze souboru zadaného uživatelem.
- Uložte název souboru s kódováním HTML nebo odebranou cestou pro uživatelské rozhraní nebo protokolování.
- Vygenerujte nový náhodný název souboru pro úložiště.
Následující kód odebere cestu z názvu souboru:
string untrustedFileName = Path.GetFileName(pathName);
Zatím uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:
Při nahrávání souborů pomocí vazby modelu a IFormFilemetoda akce může přijmout:
- Jeden .IFormFile
- Některé z následujících kolekcí, které představují několik souborů:
Poznámka:
Vazba odpovídá souborům formuláře podle názvu. Například hodnota HTML name
musí <input type="file" name="formFile">
odpovídat vázanémuFormFile
parametru nebo vlastnosti jazyka C#. Další informace naleznete v části Match name attribute value to parameter name name of POST method section.
Následující příklad:
- Smyčky procházejí jedním nebo více nahranými soubory.
- Pomocí Path.GetTempFileName vrátí úplnou cestu k souboru, včetně názvu souboru.
- Uloží soubory do místního systému souborů pomocí názvu souboru vygenerovaného aplikací.
- Vrátí celkový počet a velikost nahraných souborů.
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size });
}
Slouží Path.GetRandomFileName
k vygenerování názvu souboru bez cesty. V následujícím příkladu se cesta získá z konfigurace:
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
Cesta předaná do souboru FileStream musí obsahovat název souboru. Pokud není zadaný název souboru, UnauthorizedAccessException vyvolá se za běhu.
Soubory nahrané pomocí IFormFile techniky se před zpracováním ukládají do vyrovnávací paměti nebo na disku na serveru. Uvnitř metody IFormFile akce je obsah přístupný jako Stream. Kromě místního systému souborů je možné soubory ukládat do sdílené síťové složky nebo do služby úložiště souborů, jako je azure Blob Storage.
Další příklad, který pro nahrání smyčuje více souborů a používá bezpečné názvy souborů, najdete Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
v ukázkové aplikaci.
Upozorňující
Path.GetTempFileName vyvolá chybu IOException , pokud se vytvoří více než 65 535 souborů bez odstranění předchozích dočasných souborů. Limit 65 535 souborů je limit pro jednotlivé servery. Další informace o tomto limitu operačního systému Windows najdete v poznámkách v následujících tématech:
Nahrání malých souborů s vazbou modelu ve vyrovnávací paměti do databáze
Pokud chcete ukládat data binárního souboru do databáze pomocí Entity Frameworku, definujte Byte vlastnost pole u entity:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
Zadejte vlastnost modelu stránky pro třídu, která obsahuje :IFormFile
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
Poznámka:
IFormFile lze použít přímo jako parametr metody akce nebo jako vlastnost vázaného modelu. Předchozí příklad používá vlastnost vázaného modelu.
Používá se FileUpload
ve formuláři Razor Stránky:
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
Když je formulář poSTed na server, zkopírujte IFormFile ho do datového proudu a uložte ho jako pole bajtů v databázi. V následujícím příkladu _dbContext
uloží kontext databáze aplikace:
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);
// Upload the file if less than 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}
Předchozí příklad je podobný scénáři popsanému v ukázkové aplikaci:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
Upozorňující
Při ukládání binárních dat v relačních databázích buďte opatrní, protože to může nepříznivě ovlivnit výkon.
Nespoléhejte na vlastnost IFormFile bez ověření ani ji nedůvěřujteFileName
. Vlastnost FileName
by měla být použita pouze pro účely zobrazení a pouze po kódování HTML.
Uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:
Nahrávání velkých souborů se streamováním
Následující příklad ukazuje, jak pomocí JavaScriptu streamovat soubor do akce kontroleru. Antiforgery token souboru se vygeneruje pomocí atributu vlastního filtru a předá se hlavičkám HTTP klienta místo v textu požadavku. Vzhledem k tomu, že metoda akce zpracovává nahraná data přímo, je vazba modelu formuláře zakázána jiným vlastním filtrem. Obsah formuláře se v rámci akce čte pomocí MultipartReader
souboru, který čte jednotlivé soubory MultipartSection
nebo ukládá obsah podle potřeby. Po přečtení oddílů s více částmi provede akce vlastní vazbu modelu.
Počáteční odpověď stránky načte formulář a uloží antiforgery token do objektu cookie (prostřednictvím atributu GenerateAntiforgeryTokenCookieAttribute
). Tento atribut používá k nastavení cookie tokenu požadavku integrovanou podporu antiforgery ASP.NET Core:
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
// Send the request token as a JavaScript-readable cookie
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
public override void OnResultExecuted(ResultExecutedContext context)
{
}
}
Slouží DisableFormValueModelBindingAttribute
k zakázání vazby modelu:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
V ukázkové aplikaci GenerateAntiforgeryTokenCookieAttribute
a DisableFormValueModelBindingAttribute
použijí se jako filtry pro modely /StreamedSingleFileUploadDb
stránkovací aplikace a /StreamedSingleFileUploadPhysical
při Startup.ConfigureServices
používání Razor konvencí Pages:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
});
Vzhledem k tomu, že vazba modelu nečte formulář, parametry vázané z formuláře se neváže (dotaz, trasa a hlavička budou nadále fungovat). Metoda akce funguje přímo s Request
vlastností. A MultipartReader
slouží ke čtení jednotlivých oddílů. Data klíče/hodnoty jsou uložena KeyValueAccumulator
v souboru . Po přečtení oddílů s více částmi se obsah formuláře KeyValueAccumulator
použije k vytvoření vazby dat formuláře na typ modelu.
Úplná StreamingController.UploadDatabase
metoda streamování do databáze pomocí EF Core:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
// Accumulate the form data key-value pairs in the request (formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage = contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
streamedFileContent =
await FileHelpers.ProcessStreamedFile(section, contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);
if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}
formAccumulator.Append(key, value);
if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 3).");
// Log error
return BadRequest(ModelState);
}
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
// Bind form data to the model
var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);
if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error
return BadRequest(ModelState);
}
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.
var file = new AppFile()
{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};
_context.File.Add(file);
await _context.SaveChangesAsync();
return Created(nameof(StreamingController), null);
}
MultipartRequestHelper
(Utilities/MultipartRequestHelper.cs
):
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace SampleApp.Utilities
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}
Úplná StreamingController.UploadPhysical
metoda streamování do fyzického umístění:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
V ukázkové aplikaci se kontroly ověření zpracovávají FileHelpers.ProcessStreamedFile
.
Ověřování
Třída ukázkové aplikace FileHelpers
ukazuje několik kontrol ukládání do vyrovnávací paměti IFormFile a nahrání streamovaných souborů. Informace o zpracování IFormFile nahrávání souborů ve vyrovnávací paměti v ukázkové aplikaci najdete v ProcessFormFile
metodě v Utilities/FileHelpers.cs
souboru. Pokud chcete zpracovávat streamované soubory, podívejte se na metodu ProcessStreamedFile
ve stejném souboru.
Upozorňující
Metody zpracování ověřování, které jsou ukázány v ukázkové aplikaci, nekontroluje obsah nahraných souborů. Ve většině produkčních scénářů se v souboru před zpřístupněním souboru uživatelům nebo jiným systémům používá rozhraní API pro kontrolu virů a malwaru.
I když ukázka tématu poskytuje funkční příklad technik ověřování, neimplementujte FileHelpers
třídu v produkční aplikaci, pokud:
- Plně porozumíte implementaci.
- Upravte implementaci podle potřeby pro prostředí a specifikace aplikace.
Nikdy nerozlišují implementaci bezpečnostního kódu v aplikaci bez řešení těchto požadavků.
Ověření obsahu
K nahrání obsahu použijte rozhraní API pro kontrolu virů nebo malwaru třetí strany.
Skenování souborů je náročné na serverové prostředky ve scénářích s velkým objemem. Pokud se sníží výkon zpracování požadavků z důvodu prohledávání souborů, zvažte snížení zátěže skenovací práce do služby na pozadí, případně služby spuštěné na serveru, který se liší od serveru aplikace. Nahrané soubory se obvykle uchovávají v karanténě, dokud je antivirový skener na pozadí nekontroluje. Když soubor projde, soubor se přesune do normálního umístění úložiště souborů. Tyto kroky se obvykle provádějí ve spojení se záznamem databáze, který označuje stav kontroly souboru. Díky takovému přístupu zůstává aplikace a aplikační server zaměřeny na reakci na požadavky.
Ověření přípony souboru
Přípona nahraného souboru by se měla kontrolovat v seznamu povolených přípon. Příklad:
private string[] permittedExtensions = { ".txt", ".pdf" };
var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}
Ověření podpisu souboru
Podpis souboru je určen několika prvními bajty na začátku souboru. Tyto bajty lze použít k označení, jestli přípona odpovídá obsahu souboru. Ukázková aplikace kontroluje podpisy souborů u několika běžných typů souborů. V následujícím příkladu je podpis souboru pro obrázek JPEG zkontrolován proti souboru:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
Chcete-li získat další podpisy souborů, použijte databázi podpisů souborů (výsledek vyhledávání Google) a oficiální specifikace souborů. Specifikace oficiálního souboru konzultace mohou zajistit, aby vybrané podpisy byly platné.
Zabezpečení názvu souboru
Nikdy nepoužívejte název souboru zadaného klientem k uložení souboru do fyzického úložiště. Vytvořte pro soubor bezpečný název souboru pomocí Path.GetRandomFileName nebo Path.GetTempFileName a vytvořte úplnou cestu (včetně názvu souboru) pro dočasné úložiště.
Razor automaticky kóduje hodnoty vlastností pro zobrazení. Následující kód je bezpečný pro použití:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
RazorMimo text , vždy HtmlEncode název souboru obsahu z žádosti uživatele.
Mnoho implementací musí obsahovat kontrolu, že soubor existuje; v opačném případě se soubor přepíše souborem se stejným názvem. Zadejte další logiku, která vyhovuje specifikacím vaší aplikace.
Ověření velikosti
Omezte velikost nahraných souborů.
V ukázkové aplikaci je velikost souboru omezená na 2 MB (uvedené v bajtech). Limit se poskytuje prostřednictvím konfigurace ze appsettings.json
souboru:
{
"FileSizeLimit": 2097152
}
Vloží se FileSizeLimit
do PageModel
tříd:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
Pokud velikost souboru překročí limit, soubor se odmítne:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
Shoda s hodnotou atributu name s názvem parametru metody POST
V jinýchRazor formách, než jsou data formuláře POST nebo přímo používají JavaScript FormData
, název zadaný v elementu formuláře nebo FormData
se musí shodovat s názvem parametru v akci kontroleru.
V následujícím příkladu:
Při použití elementu
<input>
name
je atribut nastaven na hodnotubattlePlans
:<input type="file" name="battlePlans" multiple>
Při použití
FormData
v JavaScriptu je název nastaven na hodnotubattlePlans
:var formData = new FormData(); for (var file in files) { formData.append("battlePlans", file, file.name); }
Pro parametr metody jazyka C# použijte odpovídající název (battlePlans
):
Pro metodu obslužné Razor rutiny stránky Pages s názvem
Upload
:public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
Metoda akce kontroleru MVC POST:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
Konfigurace serveru a aplikace
Omezení délky těla s více částmi
MultipartBodyLengthLimit nastaví limit pro délku každého vícedílného těla. Oddíly formuláře, které tento limit překračují, při analýze vyvolá výjimku InvalidDataException . Výchozí hodnota je 134 217 728 (128 MB). Přizpůsobení limitu pomocí nastavení vStartup.ConfigureServices
:MultipartBodyLengthLimit
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute slouží k nastavení MultipartBodyLengthLimit jedné stránky nebo akce.
Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
});
Razor V aplikaci Pages nebo aplikaci MVC použijte filtr na model stránky nebo metodu akce:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Kestrel maximální velikost textu požadavku
U aplikací hostovaných ve Kestrelvýchozím nastavení je maximální velikost textu požadavku 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit pomocí možnosti Serveru MaxRequestBodySizeKestrel :
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel((context, options) =>
{
// Handle requests up to 50 MB
options.Limits.MaxRequestBodySize = 52428800;
})
.UseStartup<Startup>();
});
RequestSizeLimitAttribute slouží k nastavení MaxRequestBodySize pro jednu stránku nebo akci.
Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});
Razor V aplikaci stránky nebo aplikaci MVC použijte filtr na třídu obslužné rutiny stránky nebo metodu akce:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Lze RequestSizeLimitAttribute
také použít direktivu @attribute
Razor :
@attribute [RequestSizeLimitAttribute(52428800)]
Další Kestrel limity
Jiné Kestrel limity se můžou vztahovat na aplikace hostované:Kestrel
IIS
Výchozí limit požadavků (maxAllowedContentLength
) je 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit v web.config
souboru. V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
Nastavení maxAllowedContentLength
platí jenom pro službu IIS. Další informace naleznete v tématu Omezení <requestLimits>
požadavků .
Zvětšete maximální velikost textu požadavku HTTP nastavením v Startup.ConfigureServices
.IISServerOptions.MaxRequestBodySize V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):
services.Configure<IISServerOptions>(options =>
{
options.MaxRequestBodySize = 52428800;
});
Další informace naleznete v tématu Hostitel ASP.NET Core ve Windows se službou IIS.
Odstraňování potíží
Níže jsou uvedeny některé běžné problémy, ke kterým dochází při práci s nahráváním souborů a jejich možných řešení.
Chyba Nenalezena při nasazení na server služby IIS
Následující chyba značí, že nahraný soubor překračuje nakonfigurovanou délku obsahu serveru:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
Další informace najdete v části IIS .
Chyba připojení
Chyba připojení a připojení k resetování serveru pravděpodobně značí, že nahraný soubor překračuje Kestrelmaximální velikost textu požadavku. Další informace najdete v Kestrel části Maximální velikost textu požadavku. Kestrel Limity připojení klienta mohou vyžadovat také úpravu.
Výjimka odkazu null se souborem IFormFile
Pokud kontroler přijímá nahrané soubory pomocí IFormFile , ale hodnota je null
, potvrďte, že formulář HTML určuje enctype
hodnotu multipart/form-data
. Pokud tento atribut není nastaven na <form>
elementu, nahraje se soubor a žádné vázané IFormFile argumenty jsou null
. Ověřte také, že pojmenování nahrávání v datech formuláře odpovídá pojmenování aplikace.
Stream byl příliš dlouhý.
Příklady v tomto tématu se spoléhají na MemoryStream uložení obsahu nahraného souboru. Limit velikosti je MemoryStream
int.MaxValue
. Pokud scénář nahrávání souborů aplikace vyžaduje uchovávání obsahu souborů větší než 50 MB, použijte alternativní přístup, který nespoléhá na jediný MemoryStream
obsah nahraného souboru.
ASP.NET Core podporuje nahrávání jednoho nebo více souborů pomocí vazby modelu ve vyrovnávací paměti pro menší soubory a neuložení streamovaných souborů u větších souborů.
Zobrazení nebo stažení ukázkového kódu (postup stažení)
Bezpečnostní aspekty
Při poskytování uživatelům, kteří mají možnost nahrávat soubory na server, buďte opatrní. Cyberattackers se může pokusit:
- Spusťte útoky na odepření služeb .
- Nahrajte viry nebo malware.
- Jiné způsoby narušují sítě a servery.
Bezpečnostní kroky, které snižují pravděpodobnost úspěšného útoku:
- Nahrajte soubory do vyhrazené oblasti pro nahrávání souborů, nejlépe na systémovou jednotku. Vyhrazené umístění usnadňuje ukládání bezpečnostních omezení u nahraných souborů. Zakázání oprávnění ke spuštění v umístění pro nahrání souboru.†
- Neuchovávat nahrané soubory ve stejném adresářovém stromu jako app.†
- Použijte bezpečný název souboru určený aplikací. Nepoužívejte název souboru zadaný uživatelem ani nedůvěryhodným názvem souboru nahraného souboru.† HTML kódujte při jeho zobrazení nedůvěryhodný název souboru. Například protokolování názvu souboru nebo zobrazení v uživatelském rozhraní (Razor automaticky kóduje výstup HTML).
- Povolit pouze schválené přípony souborů pro specifikaci návrhu aplikace.†
- Ověřte, zda jsou kontroly na straně klienta provedeny na server.† Kontroly na straně klienta jsou snadné obejít.
- Zkontrolujte velikost nahraného souboru. Nastavení maximálního limitu velikosti, aby se zabránilo velkým nahráváním.†
- Pokud by soubory neměly být přepsány nahraným souborem se stejným názvem, před nahráním souboru zkontrolujte název souboru v databázi nebo fyzickém úložišti.
- Před uložením souboru spusťte skener virů nebo malwaru na nahraný obsah.
†Vzoreková aplikace ukazuje přístup, který splňuje kritéria.
Upozorňující
Nahrání škodlivého kódu do systému je často prvním krokem ke spuštění kódu, který může:
- Zcela získejte kontrolu nad systémem.
- Přetěžte systém výsledkem chybového ukončení systému.
- Ohrožení uživatelských nebo systémových dat
- Použití graffiti na veřejné uživatelské rozhraní
Informace o omezení ohrožení zabezpečení při přijímání souborů od uživatelů najdete v následujících zdrojích informací:
Další informace o implementaci bezpečnostních opatření, včetně příkladů z ukázkové aplikace, najdete v části Ověření .
Scénáře úložiště
Mezi běžné možnosti úložiště pro soubory patří:
Databáze
- U malých nahrávek souborů je databáze často rychlejší než možnosti fyzického úložiště (systému souborů nebo síťové sdílené složky).
- Databáze je často pohodlnější než možnosti fyzického úložiště, protože načtení záznamu databáze pro uživatelská data může současně poskytnout obsah souboru (například obrázek avataru).
- Databáze je potenciálně levnější než použití služby úložiště dat.
Fyzické úložiště (systém souborů nebo síťová sdílená složka)
- Pro velké nahrávání souborů:
- Limity databáze můžou omezit velikost nahrávání.
- Fyzické úložiště je často méně ekonomické než úložiště v databázi.
- Fyzické úložiště je potenciálně levnější než použití služby úložiště dat.
- Proces aplikace musí mít oprávnění ke čtení a zápisu do umístění úložiště. Nikdy neudělte oprávnění ke spuštění.
- Pro velké nahrávání souborů:
Služba úložiště dat (například Azure Blob Storage)
- Služby obvykle nabízejí lepší škálovatelnost a odolnost před místními řešeními, která obvykle podléhají kritickým bodům selhání.
- Služby jsou potenciálně nižší náklady ve scénářích infrastruktury velkých úložišť.
Další informace najdete v tématu Rychlý start: Použití .NET k vytvoření objektu blob v úložišti objektů. Téma ukazuje UploadFromFileAsync, ale UploadFromStreamAsync lze použít k uložení FileStream do úložiště objektů blob při práci s Stream.
Scénáře nahrávání souborů
Dva obecné přístupy pro nahrávání souborů jsou ukládání do vyrovnávací paměti a streamování.
Vyrovnávání
Celý soubor se načte do IFormFilesouboru, což je reprezentace souboru jazyka C#, který se používá ke zpracování nebo uložení souboru.
Prostředky (disk, paměť) používané nahráváním souborů závisí na počtu a velikosti souběžných nahrávání souborů. Pokud se aplikace pokusí uložit příliš mnoho nahrávek do vyrovnávací paměti, dojde k chybovému ukončení webu při nedostatku paměti nebo místa na disku. Pokud velikost nebo frekvence nahrávání souborů vyčerpává prostředky aplikace, použijte streamování.
Poznámka:
Všechny soubory s jednou vyrovnávací pamětí větší než 64 kB se přesunou z paměti do dočasného souboru na disku.
Ukládání malých souborů do vyrovnávací paměti je popsáno v následujících částech tohoto tématu:
Streamování
Soubor se přijímá z žádosti o více částí a aplikace ho přímo zpracuje nebo uloží. Streamování výrazně nezlepší výkon. Streamování snižuje požadavky na paměť nebo místo na disku při nahrávání souborů.
Streamování velkých souborů je popsáno v části Nahrání velkých souborů pomocí oddílu streamování .
Nahrání malých souborů s vazbou modelu do vyrovnávací paměti do fyzického úložiště
Pokud chcete nahrát malé soubory, použijte formulář s více částmi nebo vytvořte požadavek POST pomocí JavaScriptu.
Následující příklad ukazuje použití Razor formuláře Pages k nahrání jednoho souboru (Pages/BufferedSingleFileUploadPhysical.cshtml
v ukázkové aplikaci):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
Následující příklad je podobný předchozímu příkladu s tím rozdílem, že:
- JavaScript (Fetch API) slouží k odeslání dat formuláře.
- Neexistuje žádné ověření.
<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>
<input class="btn" type="submit" value="Upload" />
<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>
<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.href = '/';
}
resultElement.value = 'Result: ' + response.status + ' ' +
response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>
K provedení formuláře POST v JavaScriptu pro klienty, kteří nepodporují rozhraní Fetch API, použijte jeden z následujících přístupů:
Použijte funkci Fetch Polyfill (například window.fetch polyfill (github/fetch)).
Použijte
XMLHttpRequest
. Příklad:<script> "use strict"; function AJAXSubmit (oFormElement) { var oReq = new XMLHttpRequest(); oReq.onload = function(e) { oFormElement.elements.namedItem("result").value = 'Result: ' + this.status + ' ' + this.statusText; }; oReq.open("post", oFormElement.action); oReq.send(new FormData(oFormElement)); } </script>
Aby bylo možné podporovat nahrávání souborů, musí formuláře HTML určovat typ kódování (enctype
) .multipart/form-data
files
Vstupní prvek pro podporu nahrávání více souborů poskytuje multiple
atribut elementu<input>
:
<input asp-for="FileUpload.FormFiles" type="file" multiple>
Jednotlivé soubory nahrané na server lze získat přístup prostřednictvím vazby modelu pomocí IFormFile. Ukázková aplikace ukazuje několik nahrávání souborů do vyrovnávací paměti pro scénáře databáze a fyzického úložiště.
Upozorňující
FileName
Nepoužívejte vlastnost jiné než IFormFile pro zobrazení a protokolování. Při zobrazení nebo protokolování kóduje kód HTML název souboru. Cyberattacker může poskytnout škodlivý název souboru, včetně úplných cest nebo relativních cest. Aplikace by měly:
- Odeberte cestu ze souboru zadaného uživatelem.
- Uložte název souboru s kódováním HTML nebo odebranou cestou pro uživatelské rozhraní nebo protokolování.
- Vygenerujte nový náhodný název souboru pro úložiště.
Následující kód odebere cestu z názvu souboru:
string untrustedFileName = Path.GetFileName(pathName);
Zatím uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:
Při nahrávání souborů pomocí vazby modelu a IFormFilemetoda akce může přijmout:
- Jeden .IFormFile
- Některé z následujících kolekcí, které představují několik souborů:
Poznámka:
Vazba odpovídá souborům formuláře podle názvu. Například hodnota HTML name
musí <input type="file" name="formFile">
odpovídat vázanémuFormFile
parametru nebo vlastnosti jazyka C#. Další informace naleznete v části Match name attribute value to parameter name name of POST method section.
Následující příklad:
- Smyčky procházejí jedním nebo více nahranými soubory.
- Pomocí Path.GetTempFileName vrátí úplnou cestu k souboru, včetně názvu souboru.
- Uloží soubory do místního systému souborů pomocí názvu souboru vygenerovaného aplikací.
- Vrátí celkový počet a velikost nahraných souborů.
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size });
}
Slouží Path.GetRandomFileName
k vygenerování názvu souboru bez cesty. V následujícím příkladu se cesta získá z konfigurace:
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
Cesta předaná do souboru FileStream musí obsahovat název souboru. Pokud není zadaný název souboru, UnauthorizedAccessException vyvolá se za běhu.
Soubory nahrané pomocí IFormFile techniky se před zpracováním ukládají do vyrovnávací paměti nebo na disku na serveru. Uvnitř metody IFormFile akce je obsah přístupný jako Stream. Kromě místního systému souborů je možné soubory ukládat do sdílené síťové složky nebo do služby úložiště souborů, jako je azure Blob Storage.
Další příklad, který pro nahrání smyčuje více souborů a používá bezpečné názvy souborů, najdete Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
v ukázkové aplikaci.
Upozorňující
Path.GetTempFileName vyvolá chybu IOException , pokud se vytvoří více než 65 535 souborů bez odstranění předchozích dočasných souborů. Limit 65 535 souborů je limit pro jednotlivé servery. Další informace o tomto limitu operačního systému Windows najdete v poznámkách v následujících tématech:
Nahrání malých souborů s vazbou modelu ve vyrovnávací paměti do databáze
Pokud chcete ukládat data binárního souboru do databáze pomocí Entity Frameworku, definujte Byte vlastnost pole u entity:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
Zadejte vlastnost modelu stránky pro třídu, která obsahuje :IFormFile
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
Poznámka:
IFormFile lze použít přímo jako parametr metody akce nebo jako vlastnost vázaného modelu. Předchozí příklad používá vlastnost vázaného modelu.
Používá se FileUpload
ve formuláři Razor Stránky:
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
Když je formulář poSTed na server, zkopírujte IFormFile ho do datového proudu a uložte ho jako pole bajtů v databázi. V následujícím příkladu _dbContext
uloží kontext databáze aplikace:
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);
// Upload the file if less than 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}
Předchozí příklad je podobný scénáři popsanému v ukázkové aplikaci:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
Upozorňující
Při ukládání binárních dat v relačních databázích buďte opatrní, protože to může nepříznivě ovlivnit výkon.
Nespoléhejte na vlastnost IFormFile bez ověření ani ji nedůvěřujteFileName
. Vlastnost FileName
by měla být použita pouze pro účely zobrazení a pouze po kódování HTML.
Uvedené příklady nebere v úvahu aspekty zabezpečení. Další informace najdete v následujících částech a ukázkové aplikaci:
Nahrávání velkých souborů se streamováním
Následující příklad ukazuje, jak pomocí JavaScriptu streamovat soubor do akce kontroleru. Antiforgery token souboru se vygeneruje pomocí atributu vlastního filtru a předá se hlavičkám HTTP klienta místo v textu požadavku. Vzhledem k tomu, že metoda akce zpracovává nahraná data přímo, je vazba modelu formuláře zakázána jiným vlastním filtrem. Obsah formuláře se v rámci akce čte pomocí MultipartReader
souboru, který čte jednotlivé soubory MultipartSection
nebo ukládá obsah podle potřeby. Po přečtení oddílů s více částmi provede akce vlastní vazbu modelu.
Počáteční odpověď stránky načte formulář a uloží antiforgery token do objektu cookie (prostřednictvím atributu GenerateAntiforgeryTokenCookieAttribute
). Tento atribut používá k nastavení cookie tokenu požadavku integrovanou podporu antiforgery ASP.NET Core:
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
// Send the request token as a JavaScript-readable cookie
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
public override void OnResultExecuted(ResultExecutedContext context)
{
}
}
Slouží DisableFormValueModelBindingAttribute
k zakázání vazby modelu:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
V ukázkové aplikaci GenerateAntiforgeryTokenCookieAttribute
a DisableFormValueModelBindingAttribute
použijí se jako filtry pro modely /StreamedSingleFileUploadDb
stránkovací aplikace a /StreamedSingleFileUploadPhysical
při Startup.ConfigureServices
používání Razor konvencí Pages:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Vzhledem k tomu, že vazba modelu nečte formulář, parametry vázané z formuláře se neváže (dotaz, trasa a hlavička budou nadále fungovat). Metoda akce funguje přímo s Request
vlastností. A MultipartReader
slouží ke čtení jednotlivých oddílů. Data klíče/hodnoty jsou uložena KeyValueAccumulator
v souboru . Po přečtení oddílů s více částmi se obsah formuláře KeyValueAccumulator
použije k vytvoření vazby dat formuláře na typ modelu.
Úplná StreamingController.UploadDatabase
metoda streamování do databáze pomocí EF Core:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
// Accumulate the form data key-value pairs in the request (formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage = contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
streamedFileContent =
await FileHelpers.ProcessStreamedFile(section, contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);
if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}
formAccumulator.Append(key, value);
if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 3).");
// Log error
return BadRequest(ModelState);
}
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
// Bind form data to the model
var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);
if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error
return BadRequest(ModelState);
}
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.
var file = new AppFile()
{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};
_context.File.Add(file);
await _context.SaveChangesAsync();
return Created(nameof(StreamingController), null);
}
MultipartRequestHelper
(Utilities/MultipartRequestHelper.cs
):
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace SampleApp.Utilities
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}
Úplná StreamingController.UploadPhysical
metoda streamování do fyzického umístění:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
V ukázkové aplikaci se kontroly ověření zpracovávají FileHelpers.ProcessStreamedFile
.
Ověřování
Třída ukázkové aplikace FileHelpers
ukazuje několik kontrol ukládání do vyrovnávací paměti IFormFile a nahrání streamovaných souborů. Informace o zpracování IFormFile nahrávání souborů ve vyrovnávací paměti v ukázkové aplikaci najdete v ProcessFormFile
metodě v Utilities/FileHelpers.cs
souboru. Pokud chcete zpracovávat streamované soubory, podívejte se na metodu ProcessStreamedFile
ve stejném souboru.
Upozorňující
Metody zpracování ověřování, které jsou ukázány v ukázkové aplikaci, nekontroluje obsah nahraných souborů. Ve většině produkčních scénářů se v souboru před zpřístupněním souboru uživatelům nebo jiným systémům používá rozhraní API pro kontrolu virů a malwaru.
I když ukázka tématu poskytuje funkční příklad technik ověřování, neimplementujte FileHelpers
třídu v produkční aplikaci, pokud:
- Plně porozumíte implementaci.
- Upravte implementaci podle potřeby pro prostředí a specifikace aplikace.
Nikdy nerozlišují implementaci bezpečnostního kódu v aplikaci bez řešení těchto požadavků.
Ověření obsahu
K nahrání obsahu použijte rozhraní API pro kontrolu virů nebo malwaru třetí strany.
Skenování souborů je náročné na serverové prostředky ve scénářích s velkým objemem. Pokud se sníží výkon zpracování požadavků z důvodu prohledávání souborů, zvažte snížení zátěže skenovací práce do služby na pozadí, případně služby spuštěné na serveru, který se liší od serveru aplikace. Nahrané soubory se obvykle uchovávají v karanténě, dokud je antivirový skener na pozadí nekontroluje. Když soubor projde, soubor se přesune do normálního umístění úložiště souborů. Tyto kroky se obvykle provádějí ve spojení se záznamem databáze, který označuje stav kontroly souboru. Díky takovému přístupu zůstává aplikace a aplikační server zaměřeny na reakci na požadavky.
Ověření přípony souboru
Přípona nahraného souboru by se měla kontrolovat v seznamu povolených přípon. Příklad:
private string[] permittedExtensions = { ".txt", ".pdf" };
var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}
Ověření podpisu souboru
Podpis souboru je určen několika prvními bajty na začátku souboru. Tyto bajty lze použít k označení, jestli přípona odpovídá obsahu souboru. Ukázková aplikace kontroluje podpisy souborů u několika běžných typů souborů. V následujícím příkladu je podpis souboru pro obrázek JPEG zkontrolován proti souboru:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
Chcete-li získat další podpisy souborů, použijte databázi podpisů souborů (výsledek vyhledávání Google) a oficiální specifikace souborů. Specifikace oficiálního souboru konzultace mohou zajistit, aby vybrané podpisy byly platné.
Zabezpečení názvu souboru
Nikdy nepoužívejte název souboru zadaného klientem k uložení souboru do fyzického úložiště. Vytvořte pro soubor bezpečný název souboru pomocí Path.GetRandomFileName nebo Path.GetTempFileName a vytvořte úplnou cestu (včetně názvu souboru) pro dočasné úložiště.
Razor automaticky kóduje hodnoty vlastností pro zobrazení. Následující kód je bezpečný pro použití:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
RazorMimo text , vždy HtmlEncode název souboru obsahu z žádosti uživatele.
Mnoho implementací musí obsahovat kontrolu, že soubor existuje; v opačném případě se soubor přepíše souborem se stejným názvem. Zadejte další logiku, která vyhovuje specifikacím vaší aplikace.
Ověření velikosti
Omezte velikost nahraných souborů.
V ukázkové aplikaci je velikost souboru omezená na 2 MB (uvedené v bajtech). Limit se poskytuje prostřednictvím konfigurace ze appsettings.json
souboru:
{
"FileSizeLimit": 2097152
}
Vloží se FileSizeLimit
do PageModel
tříd:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
Pokud velikost souboru překročí limit, soubor se odmítne:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
Shoda s hodnotou atributu name s názvem parametru metody POST
V jinýchRazor formách, než jsou data formuláře POST nebo přímo používají JavaScript FormData
, název zadaný v elementu formuláře nebo FormData
se musí shodovat s názvem parametru v akci kontroleru.
V následujícím příkladu:
Při použití elementu
<input>
name
je atribut nastaven na hodnotubattlePlans
:<input type="file" name="battlePlans" multiple>
Při použití
FormData
v JavaScriptu je název nastaven na hodnotubattlePlans
:var formData = new FormData(); for (var file in files) { formData.append("battlePlans", file, file.name); }
Pro parametr metody jazyka C# použijte odpovídající název (battlePlans
):
Pro metodu obslužné Razor rutiny stránky Pages s názvem
Upload
:public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
Metoda akce kontroleru MVC POST:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
Konfigurace serveru a aplikace
Omezení délky těla s více částmi
MultipartBodyLengthLimit nastaví limit pro délku každého vícedílného těla. Oddíly formuláře, které tento limit překračují, při analýze vyvolá výjimku InvalidDataException . Výchozí hodnota je 134 217 728 (128 MB). Přizpůsobení limitu pomocí nastavení vStartup.ConfigureServices
:MultipartBodyLengthLimit
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute slouží k nastavení MultipartBodyLengthLimit jedné stránky nebo akce.
Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices
:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Razor V aplikaci Pages nebo aplikaci MVC použijte filtr na model stránky nebo metodu akce:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Kestrel maximální velikost textu požadavku
U aplikací hostovaných ve Kestrelvýchozím nastavení je maximální velikost textu požadavku 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit pomocí možnosti Serveru MaxRequestBodySizeKestrel :
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
// Handle requests up to 50 MB
options.Limits.MaxRequestBodySize = 52428800;
});
RequestSizeLimitAttribute slouží k nastavení MaxRequestBodySize pro jednu stránku nebo akci.
Razor V aplikaci Pages použijte filtr s konvencí vStartup.ConfigureServices
:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Razor V aplikaci stránky nebo aplikaci MVC použijte filtr na třídu obslužné rutiny stránky nebo metodu akce:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Další Kestrel limity
Jiné Kestrel limity se můžou vztahovat na aplikace hostované:Kestrel
IIS
Výchozí limit požadavků (maxAllowedContentLength
) je 30 000 000 bajtů, což je přibližně 28,6 MB. Přizpůsobte limit v web.config
souboru. V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
Nastavení maxAllowedContentLength
platí jenom pro službu IIS. Další informace naleznete v tématu Omezení <requestLimits>
požadavků .
Zvětšete maximální velikost textu požadavku HTTP nastavením v Startup.ConfigureServices
.IISServerOptions.MaxRequestBodySize V následujícím příkladu je limit nastavený na 50 MB (52 428 800 bajtů):
services.Configure<IISServerOptions>(options =>
{
options.MaxRequestBodySize = 52428800;
});
Další informace naleznete v tématu Hostitel ASP.NET Core ve Windows se službou IIS.
Odstraňování potíží
Níže jsou uvedeny některé běžné problémy, ke kterým dochází při práci s nahráváním souborů a jejich možných řešení.
Chyba Nenalezena při nasazení na server služby IIS
Následující chyba značí, že nahraný soubor překračuje nakonfigurovanou délku obsahu serveru:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
Další informace najdete v části IIS .
Chyba připojení
Chyba připojení a připojení k resetování serveru pravděpodobně značí, že nahraný soubor překračuje Kestrelmaximální velikost textu požadavku. Další informace najdete v Kestrel části Maximální velikost textu požadavku. Kestrel Limity připojení klienta mohou vyžadovat také úpravu.
Výjimka odkazu null se souborem IFormFile
Pokud kontroler přijímá nahrané soubory pomocí IFormFile , ale hodnota je null
, potvrďte, že formulář HTML určuje enctype
hodnotu multipart/form-data
. Pokud tento atribut není nastaven na <form>
elementu, nahraje se soubor a žádné vázané IFormFile argumenty jsou null
. Ověřte také, že pojmenování nahrávání v datech formuláře odpovídá pojmenování aplikace.
Stream byl příliš dlouhý.
Příklady v tomto tématu se spoléhají na MemoryStream uložení obsahu nahraného souboru. Limit velikosti je MemoryStream
int.MaxValue
. Pokud scénář nahrávání souborů aplikace vyžaduje uchovávání obsahu souborů větší než 50 MB, použijte alternativní přístup, který nespoléhá na jediný MemoryStream
obsah nahraného souboru.