Events
Nov 19, 11 PM - Nov 21, 11 PM
Join online sessions at Microsoft Ignite created to expand your skills and help you tackle today's complex issues.
Register nowThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
By Rutger Storm
ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.
View or download sample code (how to download)
Use caution when providing users with the ability to upload files to a server. Cyberattackers may attempt to:
Security steps that reduce the likelihood of a successful attack are:
†The sample app demonstrates an approach that meets the criteria.
Warning
Uploading malicious code to a system is frequently the first step to executing code that can:
For information on reducing vulnerabilities when accepting files from users, see the following resources:
For more information on implementing security measures, including examples from the sample app, see the Validation section.
Common storage options for files include:
Database
Physical storage (file system or network share)
Cloud data storage service, for example, Azure Blob Storage.
For more information, see Quickstart: Use .NET to create a blob in object storage.
The definition of small and large files depend on the computing resources available. Apps should benchmark the storage approach used to ensure it can handle the expected sizes. Benchmark memory, CPU, disk, and database performance.
While specific boundaries can't be provided on what is small versus large for your deployment, here are some of ASP.NET Core's related defaults for FormOptions
(API documentation):
HttpRequest.Form
doesn't buffer the entire request body (BufferBody), but it does buffer any multipart form files included.MemoryBufferThreshold
acts as a boundary between small and large files, which is raised or lowered depending on the apps resources and scenarios.For more information on FormOptions, see the FormOptions
class in the ASP.NET Core reference source.
Note
Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).
Two general approaches for uploading files are buffering and streaming.
Buffering
The entire file is read into an IFormFile. IFormFile
is a C# representation of the file used to process or save the file.
The disk and memory used by file uploads depend on the number and size of concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. If the size or frequency of file uploads is exhausting app resources, use streaming.
Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.
Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP
environment variable. If ASPNETCORE_TEMP
is not defined, the files are written to the current user's temporary folder.
Buffering small files is covered in the following sections of this topic:
Streaming
The file is received from a multipart request and directly processed or saved by the app. Streaming doesn't improve performance significantly. Streaming reduces the demands for memory or disk space when uploading files.
Streaming large files is covered in the Upload large files with streaming section.
To upload small files, use a multipart form or construct a POST request using JavaScript.
The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml
in the sample app):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file" />
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
The following example is analogous to the prior example except that:
<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>
To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:
Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).
Use XMLHttpRequest
. For example:
<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>
In order to support file uploads, HTML forms must specify an encoding type (enctype
) of multipart/form-data
.
For a files
input element to support uploading multiple files provide the multiple
attribute on the <input>
element:
<input asp-for="FileUpload.FormFiles" type="file" multiple />
The individual files uploaded to the server can be accessed through Model Binding using IFormFile. The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.
Warning
Do not use the FileName
property of IFormFile other than for display and logging. When displaying or logging, HTML encode the file name. A cyberattacker can provide a malicious filename, including full paths or relative paths. Applications should:
The following code removes the path from the file name:
string untrustedFileName = Path.GetFileName(pathName);
The examples provided thus far don't take into account security considerations. Additional information is provided by the following sections and the sample app:
When uploading files using model binding and IFormFile, the action method can accept:
Note
Binding matches form files by name. For example, the HTML name
value in <input type="file" name="formFile">
must match the C# parameter/property bound (FormFile
). For more information, see the Match name attribute value to parameter name of POST method section.
The following example:
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 });
}
Use Path.GetRandomFileName
to generate a file name without a path. In the following example, the path is obtained from configuration:
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);
}
}
}
The path passed to the FileStream must include the file name. If the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.
Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. Inside the action method, the IFormFile contents are accessible as a Stream. In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.
For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
in the sample app.
Warning
Path.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. The limit of 65,535 files is a per-server limit. For more information on this limit on Windows OS, see the remarks in the following topics:
To store binary file data in a database using Entity Framework, define a Byte array property on the entity:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
Specify a page model property for the class that includes an IFormFile:
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
Note
IFormFile can be used directly as an action method parameter or as a bound model property. The prior example uses a bound model property.
The FileUpload
is used in the Razor Pages form:
<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>
When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. In the following example, _dbContext
stores the app's database context:
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();
}
The preceding example is similar to a scenario demonstrated in the sample app:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
Warning
Use caution when storing binary data in relational databases, as it can adversely impact performance.
Don't rely on or trust the FileName
property of IFormFile without validation. The FileName
property should only be used for display purposes and only after HTML encoding.
The examples provided don't take into account security considerations. Additional information is provided by the following sections and the sample app:
The 3.1 example demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a MultipartReader
, which reads each individual MultipartSection
, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding.
The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute
attribute). The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:
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)
{
}
}
The DisableFormValueModelBindingAttribute
is used to disable model binding:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
In the sample app, GenerateAntiforgeryTokenCookieAttribute
and DisableFormValueModelBindingAttribute
are applied as filters to the page application models of /StreamedSingleFileUploadDb
and /StreamedSingleFileUploadPhysical
in Startup.ConfigureServices
using Razor Pages conventions:
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());
});
});
Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). The action method works directly with the Request
property. A MultipartReader
is used to read each section. Key/value data is stored in a KeyValueAccumulator
. After the multipart sections are read, the contents of the KeyValueAccumulator
are used to bind the form data to a model type.
The complete StreamingController.UploadDatabase
method for streaming to a database with 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));
}
}
}
The complete StreamingController.UploadPhysical
method for streaming to a physical location:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile
.
The sample app's FileHelpers
class demonstrates several checks for buffered IFormFile and streamed file uploads. For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile
method in the Utilities/FileHelpers.cs
file. For processing streamed files, see the ProcessStreamedFile
method in the same file.
Warning
The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.
Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers
class in a production app unless you:
Never indiscriminately implement security code in an app without addressing these requirements.
Use a third party virus/malware scanning API on uploaded content.
Scanning files is demanding on server resources in high volume scenarios. If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. When a file passes, the file is moved to the normal file storage location. These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. By using such an approach, the app and app server remain focused on responding to requests.
The uploaded file's extension should be checked against a list of permitted extensions. For example:
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
}
A file's signature is determined by the first few bytes at the start of a file. These bytes can be used to indicate if the extension matches the content of the file. The sample app checks file signatures for a few common file types. In the following example, the file signature for a JPEG image is checked against the file:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
To obtain additional file signatures, use a file signatures database (Google search result) and official file specifications. Consulting official file specifications may ensure that the selected signatures are valid.
Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.
Razor automatically HTML encodes property values for display. The following code is safe to use:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
Outside of Razor, always HtmlEncode file name content from a user's request.
Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. Supply additional logic to meet your app's specifications.
Limit the size of uploaded files.
In the sample app, the size of the file is limited to 2 MB (indicated in bytes). The limit is supplied via Configuration from the appsettings.json
file:
{
"FileSizeLimit": 2097152
}
The FileSizeLimit
is injected into PageModel
classes:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
When a file size exceeds the limit, the file is rejected:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
In non-Razor forms that POST form data or use JavaScript's FormData
directly, the name specified in the form's element or FormData
must match the name of the parameter in the controller's action.
In the following example:
When using an <input>
element, the name
attribute is set to the value battlePlans
:
<input type="file" name="battlePlans" multiple>
When using FormData
in JavaScript, the name is set to the value battlePlans
:
var formData = new FormData();
for (var file in files) {
formData.append("battlePlans", file, file.name);
}
Use a matching name for the parameter of the C# method (battlePlans
):
For a Razor Pages page handler method named Upload
:
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
For an MVC POST controller action method:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
MultipartBodyLengthLimit sets the limit for the length of each multipart body. Form sections that exceed this limit throw an InvalidDataException when parsed. The default is 134,217,728 (128 MB). Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.
In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
});
In a Razor Pages app or an MVC app, apply the filter to the page model or action method:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit using the MaxRequestBodySize Kestrel server option:
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 is used to set the MaxRequestBodySize for a single page or action.
In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});
In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
The RequestSizeLimitAttribute
can also be applied using the @attribute
Razor directive:
@attribute [RequestSizeLimitAttribute(52428800)]
Other Kestrel limits may apply for apps hosted by Kestrel:
The default request limit (maxAllowedContentLength
) is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit in the web.config
file. In the following example, the limit is set to 50 MB (52,428,800 bytes):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
The maxAllowedContentLength
setting only applies to IIS. For more information, see Request Limits <requestLimits>
.
Below are some common problems encountered when working with uploading files and their possible solutions.
The following error indicates that the uploaded file exceeds the server's configured content length:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
For more information, see the IIS section.
A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. For more information, see the Kestrel maximum request body size section. Kestrel client connection limits may also require adjustment.
If the controller is accepting uploaded files using IFormFile but the value is null
, confirm that the HTML form is specifying an enctype
value of multipart/form-data
. If this attribute isn't set on the <form>
element, the file upload doesn't occur and any bound IFormFile arguments are null
. Also confirm that the upload naming in form data matches the app's naming.
The examples in this topic rely upon MemoryStream to hold the uploaded file's content. The size limit of a MemoryStream
is int.MaxValue
. If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream
for holding an uploaded file's content.
ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.
View or download sample code (how to download)
Use caution when providing users with the ability to upload files to a server. Cyberattackers may attempt to:
Security steps that reduce the likelihood of a successful attack are:
†The sample app demonstrates an approach that meets the criteria.
Warning
Uploading malicious code to a system is frequently the first step to executing code that can:
For information on reducing vulnerabilities when accepting files from users, see the following resources:
For more information on implementing security measures, including examples from the sample app, see the Validation section.
Common storage options for files include:
Database
Physical storage (file system or network share)
Data storage service (for example, Azure Blob Storage)
For more information, see Quickstart: Use .NET to create a blob in object storage.
Two general approaches for uploading files are buffering and streaming.
Buffering
The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.
The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. If the size or frequency of file uploads is exhausting app resources, use streaming.
Note
Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.
Buffering small files is covered in the following sections of this topic:
Streaming
The file is received from a multipart request and directly processed or saved by the app. Streaming doesn't improve performance significantly. Streaming reduces the demands for memory or disk space when uploading files.
Streaming large files is covered in the Upload large files with streaming section.
To upload small files, use a multipart form or construct a POST request using JavaScript.
The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml
in the sample app):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
The following example is analogous to the prior example except that:
<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>
To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:
Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).
Use XMLHttpRequest
. For example:
<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>
In order to support file uploads, HTML forms must specify an encoding type (enctype
) of multipart/form-data
.
For a files
input element to support uploading multiple files provide the multiple
attribute on the <input>
element:
<input asp-for="FileUpload.FormFiles" type="file" multiple>
The individual files uploaded to the server can be accessed through Model Binding using IFormFile. The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.
Warning
Do not use the FileName
property of IFormFile other than for display and logging. When displaying or logging, HTML encode the file name. A cyberattacker can provide a malicious filename, including full paths or relative paths. Applications should:
The following code removes the path from the file name:
string untrustedFileName = Path.GetFileName(pathName);
The examples provided thus far don't take into account security considerations. Additional information is provided by the following sections and the sample app:
When uploading files using model binding and IFormFile, the action method can accept:
Note
Binding matches form files by name. For example, the HTML name
value in <input type="file" name="formFile">
must match the C# parameter/property bound (FormFile
). For more information, see the Match name attribute value to parameter name of POST method section.
The following example:
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 });
}
Use Path.GetRandomFileName
to generate a file name without a path. In the following example, the path is obtained from configuration:
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);
}
}
}
The path passed to the FileStream must include the file name. If the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.
Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. Inside the action method, the IFormFile contents are accessible as a Stream. In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.
For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
in the sample app.
Warning
Path.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. The limit of 65,535 files is a per-server limit. For more information on this limit on Windows OS, see the remarks in the following topics:
To store binary file data in a database using Entity Framework, define a Byte array property on the entity:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
Specify a page model property for the class that includes an IFormFile:
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
Note
IFormFile can be used directly as an action method parameter or as a bound model property. The prior example uses a bound model property.
The FileUpload
is used in the Razor Pages form:
<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>
When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. In the following example, _dbContext
stores the app's database context:
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();
}
The preceding example is similar to a scenario demonstrated in the sample app:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
Warning
Use caution when storing binary data in relational databases, as it can adversely impact performance.
Don't rely on or trust the FileName
property of IFormFile without validation. The FileName
property should only be used for display purposes and only after HTML encoding.
The examples provided don't take into account security considerations. Additional information is provided by the following sections and the sample app:
The following example demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a MultipartReader
, which reads each individual MultipartSection
, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding.
The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute
attribute). The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:
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)
{
}
}
The DisableFormValueModelBindingAttribute
is used to disable model binding:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
In the sample app, GenerateAntiforgeryTokenCookieAttribute
and DisableFormValueModelBindingAttribute
are applied as filters to the page application models of /StreamedSingleFileUploadDb
and /StreamedSingleFileUploadPhysical
in Startup.ConfigureServices
using Razor Pages conventions:
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());
});
});
Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). The action method works directly with the Request
property. A MultipartReader
is used to read each section. Key/value data is stored in a KeyValueAccumulator
. After the multipart sections are read, the contents of the KeyValueAccumulator
are used to bind the form data to a model type.
The complete StreamingController.UploadDatabase
method for streaming to a database with 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));
}
}
}
The complete StreamingController.UploadPhysical
method for streaming to a physical location:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile
.
The sample app's FileHelpers
class demonstrates a several checks for buffered IFormFile and streamed file uploads. For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile
method in the Utilities/FileHelpers.cs
file. For processing streamed files, see the ProcessStreamedFile
method in the same file.
Warning
The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.
Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers
class in a production app unless you:
Never indiscriminately implement security code in an app without addressing these requirements.
Use a third party virus/malware scanning API on uploaded content.
Scanning files is demanding on server resources in high volume scenarios. If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. When a file passes, the file is moved to the normal file storage location. These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. By using such an approach, the app and app server remain focused on responding to requests.
The uploaded file's extension should be checked against a list of permitted extensions. For example:
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
}
A file's signature is determined by the first few bytes at the start of a file. These bytes can be used to indicate if the extension matches the content of the file. The sample app checks file signatures for a few common file types. In the following example, the file signature for a JPEG image is checked against the file:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
To obtain additional file signatures, use a file signatures database (Google search result) and official file specifications. Consulting official file specifications may ensure that the selected signatures are valid.
Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.
Razor automatically HTML encodes property values for display. The following code is safe to use:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
Outside of Razor, always HtmlEncode file name content from a user's request.
Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. Supply additional logic to meet your app's specifications.
Limit the size of uploaded files.
In the sample app, the size of the file is limited to 2 MB (indicated in bytes). The limit is supplied via Configuration from the appsettings.json
file:
{
"FileSizeLimit": 2097152
}
The FileSizeLimit
is injected into PageModel
classes:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
When a file size exceeds the limit, the file is rejected:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
In non-Razor forms that POST form data or use JavaScript's FormData
directly, the name specified in the form's element or FormData
must match the name of the parameter in the controller's action.
In the following example:
When using an <input>
element, the name
attribute is set to the value battlePlans
:
<input type="file" name="battlePlans" multiple>
When using FormData
in JavaScript, the name is set to the value battlePlans
:
var formData = new FormData();
for (var file in files) {
formData.append("battlePlans", file, file.name);
}
Use a matching name for the parameter of the C# method (battlePlans
):
For a Razor Pages page handler method named Upload
:
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
For an MVC POST controller action method:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
MultipartBodyLengthLimit sets the limit for the length of each multipart body. Form sections that exceed this limit throw an InvalidDataException when parsed. The default is 134,217,728 (128 MB). Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.
In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
});
In a Razor Pages app or an MVC app, apply the filter to the page model or action method:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit using the MaxRequestBodySize Kestrel server option:
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 is used to set the MaxRequestBodySize for a single page or action.
In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices
:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});
In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
The RequestSizeLimitAttribute
can also be applied using the @attribute
Razor directive:
@attribute [RequestSizeLimitAttribute(52428800)]
Other Kestrel limits may apply for apps hosted by Kestrel:
The default request limit (maxAllowedContentLength
) is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit in the web.config
file. In the following example, the limit is set to 50 MB (52,428,800 bytes):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
The maxAllowedContentLength
setting only applies to IIS. For more information, see Request Limits <requestLimits>
.
Increase the maximum request body size for the HTTP request by setting IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices
. In the following example, the limit is set to 50 MB (52,428,800 bytes):
services.Configure<IISServerOptions>(options =>
{
options.MaxRequestBodySize = 52428800;
});
For more information, see Host ASP.NET Core on Windows with IIS.
Below are some common problems encountered when working with uploading files and their possible solutions.
The following error indicates that the uploaded file exceeds the server's configured content length:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
For more information, see the IIS section.
A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. For more information, see the Kestrel maximum request body size section. Kestrel client connection limits may also require adjustment.
If the controller is accepting uploaded files using IFormFile but the value is null
, confirm that the HTML form is specifying an enctype
value of multipart/form-data
. If this attribute isn't set on the <form>
element, the file upload doesn't occur and any bound IFormFile arguments are null
. Also confirm that the upload naming in form data matches the app's naming.
The examples in this topic rely upon MemoryStream to hold the uploaded file's content. The size limit of a MemoryStream
is int.MaxValue
. If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream
for holding an uploaded file's content.
ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.
View or download sample code (how to download)
Use caution when providing users with the ability to upload files to a server. Cyberattackers may attempt to:
Security steps that reduce the likelihood of a successful attack are:
†The sample app demonstrates an approach that meets the criteria.
Warning
Uploading malicious code to a system is frequently the first step to executing code that can:
For information on reducing vulnerabilities when accepting files from users, see the following resources:
For more information on implementing security measures, including examples from the sample app, see the Validation section.
Common storage options for files include:
Database
Physical storage (file system or network share)
Data storage service (for example, Azure Blob Storage)
For more information, see Quickstart: Use .NET to create a blob in object storage. The topic demonstrates UploadFromFileAsync, but UploadFromStreamAsync can be used to save a FileStream to blob storage when working with a Stream.
Two general approaches for uploading files are buffering and streaming.
Buffering
The entire file is read into an IFormFile, which is a C# representation of the file used to process or save the file.
The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. If the size or frequency of file uploads is exhausting app resources, use streaming.
Note
Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.
Buffering small files is covered in the following sections of this topic:
Streaming
The file is received from a multipart request and directly processed or saved by the app. Streaming doesn't improve performance significantly. Streaming reduces the demands for memory or disk space when uploading files.
Streaming large files is covered in the Upload large files with streaming section.
To upload small files, use a multipart form or construct a POST request using JavaScript.
The following example demonstrates the use of a Razor Pages form to upload a single file (Pages/BufferedSingleFileUploadPhysical.cshtml
in the sample app):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
The following example is analogous to the prior example except that:
<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>
To perform the form POST in JavaScript for clients that don't support the Fetch API, use one of the following approaches:
Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch)).
Use XMLHttpRequest
. For example:
<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>
In order to support file uploads, HTML forms must specify an encoding type (enctype
) of multipart/form-data
.
For a files
input element to support uploading multiple files provide the multiple
attribute on the <input>
element:
<input asp-for="FileUpload.FormFiles" type="file" multiple>
The individual files uploaded to the server can be accessed through Model Binding using IFormFile. The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.
Warning
Do not use the FileName
property of IFormFile other than for display and logging. When displaying or logging, HTML encode the file name. A cyberattacker can provide a malicious filename, including full paths or relative paths. Applications should:
The following code removes the path from the file name:
string untrustedFileName = Path.GetFileName(pathName);
The examples provided thus far don't take into account security considerations. Additional information is provided by the following sections and the sample app:
When uploading files using model binding and IFormFile, the action method can accept:
Note
Binding matches form files by name. For example, the HTML name
value in <input type="file" name="formFile">
must match the C# parameter/property bound (FormFile
). For more information, see the Match name attribute value to parameter name of POST method section.
The following example:
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 });
}
Use Path.GetRandomFileName
to generate a file name without a path. In the following example, the path is obtained from configuration:
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);
}
}
}
The path passed to the FileStream must include the file name. If the file name isn't provided, an UnauthorizedAccessException is thrown at runtime.
Files uploaded using the IFormFile technique are buffered in memory or on disk on the server before processing. Inside the action method, the IFormFile contents are accessible as a Stream. In addition to the local file system, files can be saved to a network share or to a file storage service, such as Azure Blob storage.
For another example that loops over multiple files for upload and uses safe file names, see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
in the sample app.
Warning
Path.GetTempFileName throws an IOException if more than 65,535 files are created without deleting previous temporary files. The limit of 65,535 files is a per-server limit. For more information on this limit on Windows OS, see the remarks in the following topics:
To store binary file data in a database using Entity Framework, define a Byte array property on the entity:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
Specify a page model property for the class that includes an IFormFile:
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
Note
IFormFile can be used directly as an action method parameter or as a bound model property. The prior example uses a bound model property.
The FileUpload
is used in the Razor Pages form:
<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>
When the form is POSTed to the server, copy the IFormFile to a stream and save it as a byte array in the database. In the following example, _dbContext
stores the app's database context:
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();
}
The preceding example is similar to a scenario demonstrated in the sample app:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
Warning
Use caution when storing binary data in relational databases, as it can adversely impact performance.
Don't rely on or trust the FileName
property of IFormFile without validation. The FileName
property should only be used for display purposes and only after HTML encoding.
The examples provided don't take into account security considerations. Additional information is provided by the following sections and the sample app:
The following example demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a MultipartReader
, which reads each individual MultipartSection
, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding.
The initial page response loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieAttribute
attribute). The attribute uses ASP.NET Core's built-in antiforgery support to set a cookie with a request token:
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)
{
}
}
The DisableFormValueModelBindingAttribute
is used to disable model binding:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
In the sample app, GenerateAntiforgeryTokenCookieAttribute
and DisableFormValueModelBindingAttribute
are applied as filters to the page application models of /StreamedSingleFileUploadDb
and /StreamedSingleFileUploadPhysical
in Startup.ConfigureServices
using Razor Pages conventions:
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);
Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). The action method works directly with the Request
property. A MultipartReader
is used to read each section. Key/value data is stored in a KeyValueAccumulator
. After the multipart sections are read, the contents of the KeyValueAccumulator
are used to bind the form data to a model type.
The complete StreamingController.UploadDatabase
method for streaming to a database with 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));
}
}
}
The complete StreamingController.UploadPhysical
method for streaming to a physical location:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile
.
The sample app's FileHelpers
class demonstrates a several checks for buffered IFormFile and streamed file uploads. For processing IFormFile buffered file uploads in the sample app, see the ProcessFormFile
method in the Utilities/FileHelpers.cs
file. For processing streamed files, see the ProcessStreamedFile
method in the same file.
Warning
The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.
Although the topic sample provides a working example of validation techniques, don't implement the FileHelpers
class in a production app unless you:
Never indiscriminately implement security code in an app without addressing these requirements.
Use a third party virus/malware scanning API on uploaded content.
Scanning files is demanding on server resources in high volume scenarios. If request processing performance is diminished due to file scanning, consider offloading the scanning work to a background service, possibly a service running on a server different from the app's server. Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. When a file passes, the file is moved to the normal file storage location. These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. By using such an approach, the app and app server remain focused on responding to requests.
The uploaded file's extension should be checked against a list of permitted extensions. For example:
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
}
A file's signature is determined by the first few bytes at the start of a file. These bytes can be used to indicate if the extension matches the content of the file. The sample app checks file signatures for a few common file types. In the following example, the file signature for a JPEG image is checked against the file:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
To obtain additional file signatures, use a file signatures database (Google search result) and official file specifications. Consulting official file specifications may ensure that the selected signatures are valid.
Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using Path.GetRandomFileName or Path.GetTempFileName to create a full path (including the file name) for temporary storage.
Razor automatically HTML encodes property values for display. The following code is safe to use:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
Outside of Razor, always HtmlEncode file name content from a user's request.
Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. Supply additional logic to meet your app's specifications.
Limit the size of uploaded files.
In the sample app, the size of the file is limited to 2 MB (indicated in bytes). The limit is supplied via Configuration from the appsettings.json
file:
{
"FileSizeLimit": 2097152
}
The FileSizeLimit
is injected into PageModel
classes:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
When a file size exceeds the limit, the file is rejected:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
In non-Razor forms that POST form data or use JavaScript's FormData
directly, the name specified in the form's element or FormData
must match the name of the parameter in the controller's action.
In the following example:
When using an <input>
element, the name
attribute is set to the value battlePlans
:
<input type="file" name="battlePlans" multiple>
When using FormData
in JavaScript, the name is set to the value battlePlans
:
var formData = new FormData();
for (var file in files) {
formData.append("battlePlans", file, file.name);
}
Use a matching name for the parameter of the C# method (battlePlans
):
For a Razor Pages page handler method named Upload
:
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
For an MVC POST controller action method:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
MultipartBodyLengthLimit sets the limit for the length of each multipart body. Form sections that exceed this limit throw an InvalidDataException when parsed. The default is 134,217,728 (128 MB). Customize the limit using the MultipartBodyLengthLimit setting in Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single page or action.
In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices
:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
In a Razor Pages app or an MVC app, apply the filter to the page model or action method:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit using the MaxRequestBodySize Kestrel server option:
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 is used to set the MaxRequestBodySize for a single page or action.
In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices
:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Other Kestrel limits may apply for apps hosted by Kestrel:
The default request limit (maxAllowedContentLength
) is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit in the web.config
file. In the following example, the limit is set to 50 MB (52,428,800 bytes):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
The maxAllowedContentLength
setting only applies to IIS. For more information, see Request Limits <requestLimits>
.
Increase the maximum request body size for the HTTP request by setting IISServerOptions.MaxRequestBodySize in Startup.ConfigureServices
. In the following example, the limit is set to 50 MB (52,428,800 bytes):
services.Configure<IISServerOptions>(options =>
{
options.MaxRequestBodySize = 52428800;
});
For more information, see Host ASP.NET Core on Windows with IIS.
Below are some common problems encountered when working with uploading files and their possible solutions.
The following error indicates that the uploaded file exceeds the server's configured content length:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
For more information, see the IIS section.
A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. For more information, see the Kestrel maximum request body size section. Kestrel client connection limits may also require adjustment.
If the controller is accepting uploaded files using IFormFile but the value is null
, confirm that the HTML form is specifying an enctype
value of multipart/form-data
. If this attribute isn't set on the <form>
element, the file upload doesn't occur and any bound IFormFile arguments are null
. Also confirm that the upload naming in form data matches the app's naming.
The examples in this topic rely upon MemoryStream to hold the uploaded file's content. The size limit of a MemoryStream
is int.MaxValue
. If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single MemoryStream
for holding an uploaded file's content.
ASP.NET Core feedback
ASP.NET Core is an open source project. Select a link to provide feedback:
Events
Nov 19, 11 PM - Nov 21, 11 PM
Join online sessions at Microsoft Ignite created to expand your skills and help you tackle today's complex issues.
Register nowTraining
Module
Download and upload files in an ASP.NET Core app with Microsoft Graph - Training
Learn how to list, download, and upload a user's files in an ASP.NET Core application by using Microsoft Graph.
Documentation
Sending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME - ASP.NET 4.x
This tutorial shows how to upload files to a web API. It also describes how to process multipart MIME data.
Sending HTML Form Data in ASP.NET Web API: Form-urlencoded Data - ASP.NET 4.x
This article shows how to post form-urlencoded data to a Web API controller with ASP.NET 4.x