I'm using ASP.NET Core 2.2 and I'm using model binding for uploading file.
This is my UserViewModel
public class UserViewModel
{
[Required(ErrorMessage = "Please select a file.")]
[DataType(DataType.Upload)]
public IFormFile Photo { get; set; }
}
This is MyView
@model UserViewModel
<form method="post"
asp-action="UploadPhoto"
asp-controller="TestFileUpload"
enctype="multipart/form-data">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input asp-for="Photo" />
<span asp-validation-for="Photo" class="text-danger"></span>
<input type="submit" value="Upload"/>
</form>
And finally this is MyController
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel)
{
if (ModelState.IsValid)
{
var formFile = userViewModel.Photo;
if (formFile == null || formFile.Length == 0)
{
ModelState.AddModelError("", "Uploaded file is empty or null.");
return View(viewName: "Index");
}
var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsRootFolder))
{
Directory.CreateDirectory(uploadsRootFolder);
}
var filePath = Path.Combine(uploadsRootFolder, formFile.FileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(fileStream).ConfigureAwait(false);
}
RedirectToAction("Index");
}
return View(viewName: "Index");
}
How can I limit uploaded files to lower than 5MB with specific extensions like.jpeg and.png? I think both of these validations are done in the ViewModel. But I don't know how to do that.
You could custom validation attribute MaxFileSizeAttribute
like below
MaxFileSizeAttribute
public class MaxFileSizeAttribute : ValidationAttribute
{
private readonly int _maxFileSize;
public MaxFileSizeAttribute(int maxFileSize)
{
_maxFileSize = maxFileSize;
}
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
var file = value as IFormFile;
if (file != null)
{
if (file.Length > _maxFileSize)
{
return new ValidationResult(GetErrorMessage());
}
}
return ValidationResult.Success;
}
public string GetErrorMessage()
{
return $"Maximum allowed file size is { _maxFileSize} bytes.";
}
}
AllowedExtensionsAttribute
public class AllowedExtensionsAttribute : ValidationAttribute
{
private readonly string[] _extensions;
public AllowedExtensionsAttribute(string[] extensions)
{
_extensions = extensions;
}
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
var file = value as IFormFile;
if (file != null)
{
var extension = Path.GetExtension(file.FileName);
if (!_extensions.Contains(extension.ToLower()))
{
return new ValidationResult(GetErrorMessage());
}
}
return ValidationResult.Success;
}
public string GetErrorMessage()
{
return $"This photo extension is not allowed!";
}
}
Add MaxFileSize
attribute and AllowedExtensions
attribute to Photo
property
public class UserViewModel
{
[Required(ErrorMessage = "Please select a file.")]
[DataType(DataType.Upload)]
[MaxFileSize(5* 1024 * 1024)]
[AllowedExtensions(new string[] { ".jpg", ".png" })]
public IFormFile Photo { get; set; }
}
This is how I use the custom validation attributes, almost the same as @xueli-chen 's answer but production-ready.
FileExtensionsAttribute
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Http;
using NewsPassWebApi.Properties;
namespace NewsPassWebApi.Models.DataAnnotaions
{
/// <summary>
/// Validation attribute to assert an <see cref="IFormFile">IFormFile</see> property, field or parameter has a specific extention.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class FileExtensionsAttribute : ValidationAttribute
{
private string _extensions;
/// <summary>
/// Gets or sets the acceptable extensions of the file.
/// </summary>
public string Extensions
{
get
{
// Default file extensions match those from jquery validate.
return string.IsNullOrEmpty(_extensions) ? "png,jpg,jpeg,gif" : _extensions;
}
set
{
_extensions = value;
}
}
private string ExtensionsNormalized
{
get
{
return Extensions.Replace(" ", "", StringComparison.Ordinal).ToUpperInvariant();
}
}
/// <summary>
/// Parameterless constructor.
/// </summary>
public FileExtensionsAttribute() : base(() => Resources.FileExtensionsAttribute_ValidationError)
{ }
/// <summary>
/// Override of <see cref="ValidationAttribute.IsValid(object)"/>
/// </summary>
/// <remarks>
/// This method returns <c>true</c> if the <paramref name="value"/> is null.
/// It is assumed the <see cref="RequiredAttribute"/> is used if the value may not be null.
/// </remarks>
/// <param name="value">The value to test.</param>
/// <returns><c>true</c> if the value is null or it's extension is not invluded in the set extensionss</returns>
public override bool IsValid(object value)
{
// Automatically pass if value is null. RequiredAttribute should be used to assert a value is not null.
if (value == null)
{
return true;
}
// We expect a cast exception if the passed value was not an IFormFile.
return ExtensionsNormalized.Split(",").Contains(Path.GetExtension(((IFormFile)value).FileName).ToUpperInvariant());
}
/// <summary>
/// Override of <see cref="ValidationAttribute.FormatErrorMessage"/>
/// </summary>
/// <param name="name">The name to include in the formatted string</param>
/// <returns>A localized string to describe the acceptable extensions</returns>
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, Extensions);
}
}
}
FileSizeAttribute
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using NewsPassWebApi.Properties;
namespace NewsPassWebApi.Models.DataAnnotaions
{
/// <summary>
/// Validation attribute to assert an <see cref="IFormFile">IFormFile</see> property, field or parameter does not exceed a maximum size.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class FileSizeAttribute : ValidationAttribute
{
/// <summary>
/// Gets the maximum acceptable size of the file.
/// </summary>
public long MaximumSize { get; private set; }
/// <summary>
/// Gets or sets the minimum acceptable size of the file.
/// </summary>
public int MinimumSize { get; set; }
/// <summary>
/// Constructor that accepts the maximum size of the file.
/// </summary>
/// <param name="maximumSize">The maximum size, inclusive. It may not be negative.</param>
public FileSizeAttribute(int maximumSize) : base(() => Resources.FileSizeAttribute_ValidationError)
{
MaximumSize = maximumSize;
}
/// <summary>
/// Override of <see cref="ValidationAttribute.IsValid(object)"/>
/// </summary>
/// <remarks>
/// This method returns <c>true</c> if the <paramref name="value"/> is null.
/// It is assumed the <see cref="RequiredAttribute"/> is used if the value may not be null.
/// </remarks>
/// <param name="value">The value to test.</param>
/// <returns><c>true</c> if the value is null or it's size is less than or equal to the set maximum size</returns>
/// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
public override bool IsValid(object value)
{
// Check the lengths for legality
EnsureLegalSizes();
// Automatically pass if value is null. RequiredAttribute should be used to assert a value is not null.
// We expect a cast exception if the passed value was not an IFormFile.
var length = value == null ? 0 : ((IFormFile)value).Length;
return value == null || (length >= MinimumSize && length <= MaximumSize);
}
/// <summary>
/// Override of <see cref="ValidationAttribute.FormatErrorMessage"/>
/// </summary>
/// <param name="name">The name to include in the formatted string</param>
/// <returns>A localized string to describe the maximum acceptable size</returns>
/// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
public override string FormatErrorMessage(string name)
{
EnsureLegalSizes();
string errorMessage = MinimumSize != 0 ? Resources.FileSizeAttribute_ValidationErrorIncludingMinimum : ErrorMessageString;
// it's ok to pass in the minLength even for the error message without a {2} param since String.Format will just ignore extra arguments
return string.Format(CultureInfo.CurrentCulture, errorMessage, name, MaximumSize, MinimumSize);
}
/// <summary>
/// Checks that MinimumSize and MaximumSize have legal values. Throws InvalidOperationException if not.
/// </summary>
private void EnsureLegalSizes()
{
if (MaximumSize < 0)
{
throw new InvalidOperationException(Resources.FileSizeAttribute_InvalidMaxSize);
}
if (MaximumSize < MinimumSize)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.RangeAttribute_MinGreaterThanMax, MaximumSize, MinimumSize));
}
}
}
}
now you can use those like any built-in validation attribute including custom/localized error messages, max-min file sizes, and file extensions.
You can implement IValidatableObject
to validate your model.
public class UserViewModel : IValidatableObject
{
[Required(ErrorMessage = "Please select a file.")]
[DataType(DataType.Upload)]
public IFormFile Photo { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var photo = ((UserViewModel)validationContext.ObjectInstance).Photo;
var extension = Path.GetExtension(photo.FileName);
var size = photo.Length;
if (!extension.ToLower().Equals(".jpg"))
yield return new ValidationResult("File extension is not valid.");
if(size > (5 * 1024 * 1024))
yield return new ValidationResult("File size is bigger than 5MB.");
}
}
Following previous comment , you can add this class:
public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.IsValid)
{
return;
}
var validationErrors = context.ModelState
.Keys
.SelectMany(k => context.ModelState[k].Errors)
.Select(e => e.ErrorMessage)
.ToArray();
var json = new JsonErrorResponse
{
Messages = validationErrors
};
context.Result = new BadRequestObjectResult(json);
}
}
Finally, add the filter to the controllers in Startup class:
services.AddControllers(options =>
{
options.Filters.Add(typeof(ValidateModelStateFilter));
})
Tested on .Net 5. How about more elastic attribute with custom object
(you can use own ActionResult class)
[AttributeUsage(AttributeTargets.Method)]
public class MaxFileSizeAttribute : Attribute, IAsyncActionFilter
{
public MaxFileSizeAttribute(int sizeInByte) { this._maxFileSize = sizeInByte; }
private int _maxFileSize;
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (
context
.HttpContext
.Request
.Form != null
&& context.HttpContext.Request.Form.Files.Count > 0
)
{
if (
context
.HttpContext
.Request
.Form
.Files
.Any(x => x.Length >= _maxFileSize)
)
{
context
.Result = new ObjectResult($"Max file size is {_maxFileSize} bytes"
/*put here whatever object you want*/)
{
StatusCode = 413 //Payload Too Large
};
} else { await next(); }
} else { await next(); }
}
}
And use it in controller:
[MaxFileSize(1024)]
public async Task<ActionResult> SendImages([FromForm] SendAttachment request)
{
return Ok();
}
You can do like below code
var formFile = userViewModel.Photo;
var extension = Path.GetExtension(formFile.FileName);
var allowedExtensions = new[] { ".jpeg", ".png" };
if (formFile == null || formFile.Length == 0)
{
ModelState.AddModelError("", "Uploaded file is empty or null.");
return View(viewName: "Index");
}
if (formFile != null && formFile.ContentLength > 0 &&
allowedExtensions.Contains(extension.ToLower())
&& formFile.ContentLength <= (5 * 1024 * 1024))
{
//do some logic here because the data is smaller than 5mb
}
else
{
// error
return View("Error");
}
Above solutions are validating file by its extension (which is just the filename really). That's not secure, as it allows loading executables (pretending to be image or so).
You should be comparing signature of the file with a pre-defined list of allowed signatures. See the File signature validation section in MS docs.
I then call the IsFileValid()
method from controllers.
public static bool IsFileValid(IFormFile file)
{
using (var reader = new BinaryReader(file.OpenReadStream()))
{
var signatures = _fileSignatures.Values.SelectMany(x => x).ToList(); // flatten all signatures to single list
var headerBytes = reader.ReadBytes(_fileSignatures.Max(m => m.Value.Max(n => n.Length)));
bool result = signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature));
return result;
}
}
private static readonly Dictionary<string, List<byte[]>> _fileSignatures = new()
{
{ ".gif", new List<byte[]> { new byte[] { 0x47, 0x49, 0x46, 0x38 } } },
{ ".png", new List<byte[]> { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xEE },
new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
}
},
{ ".jpeg2000", new List<byte[]> { new byte[] { 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A } } },
{ ".jpg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xEE },
new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
}
},
{ ".zip", new List<byte[]> //also docx, xlsx, pptx, ...
{
new byte[] { 0x50, 0x4B, 0x03, 0x04 },
new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
new byte[] { 0x50, 0x4B, 0x05, 0x06 },
new byte[] { 0x50, 0x4B, 0x07, 0x08 },
new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
}
},
{ ".pdf", new List<byte[]> { new byte[] { 0x25, 0x50, 0x44, 0x46 } } },
{ ".z", new List<byte[]>
{
new byte[] { 0x1F, 0x9D },
new byte[] { 0x1F, 0xA0 }
}
},
{ ".tar", new List<byte[]>
{
new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30 , 0x30 },
new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20 , 0x00 },
}
},
{ ".tar.z", new List<byte[]>
{
new byte[] { 0x1F, 0x9D },
new byte[] { 0x1F, 0xA0 }
}
},
{ ".tif", new List<byte[]>
{
new byte[] { 0x49, 0x49, 0x2A, 0x00 },
new byte[] { 0x4D, 0x4D, 0x00, 0x2A }
}
},
{ ".tiff", new List<byte[]>
{
new byte[] { 0x49, 0x49, 0x2A, 0x00 },
new byte[] { 0x4D, 0x4D, 0x00, 0x2A }
}
},
{ ".rar", new List<byte[]>
{
new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07 , 0x00 },
new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07 , 0x01, 0x00 },
}
},
{ ".7z", new List<byte[]>
{
new byte[] { 0x37, 0x7A, 0xBC, 0xAF, 0x27 , 0x1C },
}
},
{ ".txt", new List<byte[]>
{
new byte[] { 0xEF, 0xBB , 0xBF },
new byte[] { 0xFF, 0xFE},
new byte[] { 0xFE, 0xFF },
new byte[] { 0x00, 0x00, 0xFE, 0xFF },
}
},
{ ".mp3", new List<byte[]>
{
new byte[] { 0xFF, 0xFB },
new byte[] { 0xFF, 0xF3},
new byte[] { 0xFF, 0xF2},
new byte[] { 0x49, 0x44, 0x43},
}
},
};
More signatures can be found on Wikipedia .
Secure Extension Validation
With the help of this solution and this solution I could come up with a combined solution which works as a DataAnnotation also prevents files like exe from being injected maliciously. As explained well by FFFffff , checking extensions strings only
allows loading executables (pretending to be image or so)
So Here it is:
public class AllowedFileExtensions : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var file = value as IFormFile;
if (file != null)
{
if (!IsFileValid(file))
{
// get the allowed extensions to show in ValidationResult
string? keys = null;
foreach (var key in _fileSignatures.Keys)
{
keys += key + ", ";
}
return new ValidationResult($"File type is not allowed. These extensions are allowed only: {keys}");
}
}
return ValidationResult.Success;
}
public static bool IsFileValid(IFormFile file)
{
using (var reader = new BinaryReader(file.OpenReadStream()))
{
var signatures = _fileSignatures.Values.SelectMany(x => x).ToList(); // flatten all signatures to single list
var headerBytes = reader.ReadBytes(_fileSignatures.Max(m => m.Value.Max(n => n.Length)));
bool result = signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature));
return result;
}
}
public static Dictionary<string, List<byte[]>> _fileSignatures = new()
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xEE },
new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
}
},
{ ".jpg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xEE },
new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
}
},
{ ".jpeg2000", new List<byte[]> { new byte[] { 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A } } },
{ ".png", new List<byte[]> { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
// { ".gif", new List<byte[]> { new byte[] { 0x47, 0x49, 0x46, 0x38 } } },
// { ".zip", new List<byte[]> //also docx, xlsx, pptx, ...
// {
// new byte[] { 0x50, 0x4B, 0x03, 0x04 },
// new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
// new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
// new byte[] { 0x50, 0x4B, 0x05, 0x06 },
// new byte[] { 0x50, 0x4B, 0x07, 0x08 },
// new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
// }
// },
// { ".pdf", new List<byte[]> { new byte[] { 0x25, 0x50, 0x44, 0x46 } } },
// { ".z", new List<byte[]>
// {
// new byte[] { 0x1F, 0x9D },
// new byte[] { 0x1F, 0xA0 }
// }
// },
// { ".tar", new List<byte[]>
// {
// new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30 , 0x30 },
// new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20 , 0x00 },
// }
// },
// { ".tar.z", new List<byte[]>
// {
// new byte[] { 0x1F, 0x9D },
// new byte[] { 0x1F, 0xA0 }
// }
// },
// { ".tif", new List<byte[]>
// {
// new byte[] { 0x49, 0x49, 0x2A, 0x00 },
// new byte[] { 0x4D, 0x4D, 0x00, 0x2A }
// }
// },
// { ".tiff", new List<byte[]>
// {
// new byte[] { 0x49, 0x49, 0x2A, 0x00 },
// new byte[] { 0x4D, 0x4D, 0x00, 0x2A }
// }
// },
// { ".rar", new List<byte[]>
// {
// new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07 , 0x00 },
// new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07 , 0x01, 0x00 },
// }
// },
// { ".7z", new List<byte[]>
// {
// new byte[] { 0x37, 0x7A, 0xBC, 0xAF, 0x27 , 0x1C },
// }
// },
// { ".txt", new List<byte[]>
// {
// new byte[] { 0xEF, 0xBB , 0xBF },
// new byte[] { 0xFF, 0xFE},
// new byte[] { 0xFE, 0xFF },
// new byte[] { 0x00, 0x00, 0xFE, 0xFF },
// }
// },
// { ".mp3", new List<byte[]>
// {
// new byte[] { 0xFF, 0xFB },
// new byte[] { 0xFF, 0xF3},
// new byte[] { 0xFF, 0xF2},
// new byte[] { 0x49, 0x44, 0x43},
// }
// },
};
}
I purposely commented some extensions from the _fileSignatures Dictionary to block them.
Now just add [AllowedFileExtensions] to your Controller's method signature directly or Model:
public async Task<IActionResult> AddPhoto([AllowedFileExtensions] IFormFile file) { }
Here is the output if a non-allowed file is selected:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-94aa03b188cdf7c402ff22d809ad1bd4-4c1a49080c27213d-00",
"errors": {
"files": [
"File type is not allowed. These extensions are allowed only: .jpeg, .jpg, .jpeg2000, .png, "
]
}
}
Just a quick one if anyone is looking for a PDF file validator:
{ ".pdf", new List<byte[]> { new byte[] { 0x25, 0x50, 0x44, 0x46 } } },
See the brilliant site:
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.