Sitecore Webforms for Marketers – Custom Field Validator MIME Type Sniffer


Update: I have since released this as a Sitecore Marketplace Module for download. Improvements I have made and the full source code are available in this recent blog post


MIME Type Custom Form Validator

I was recently working on methods to validate files uploaded by End Users and the usual recommendation of checking the file extension didn’t sit right with me, even more so when I found out that the .NET API ultimately just checks the File extension as well.

So the product a mornings work I’ve written a decent method of checking those upload files are what they say they are. In this case I created a Custom Sitecore WFFM Field Validator but its simple enough to port the code to custom web forms. Hopefully another dev will find this suitable as an alternative method.

FormCustomValidator

As the FileUpload control is a Sitecore Webforms for Marketers Form I need to create a WFFM custom Validator (jump to the section below to skip the Sitecore set-up).

The custom validator is created by a class that implements FormCustomValidator and overriding the EvaludateIsValid method. Within that method we want to retrieve FileUpload control that holds the file we want to validate, confirm a file is present and then pass it to a function to complete the various stages of validation.


namespace ISlayTitans.Cms.WebForms.Validators
{
public class CustomValidator : FormCustomValidator
{
protected override bool EvaluateIsValid()
{
bool isValid = false;
if (!String.IsNullOrEmpty(base.ControlToValidate))
{
Control control = this.FindControl(base.ControlToValidate);
var fileUpload = control as FileUpload;
if (fileUpload != null && fileUpload.HasFile)
{
isValid = ValidateFile(fileUpload.PostedFile);
}
}
return isValid;
}
// Custom function which calls the various stages of validation
private bool ValidateFile(HttpPostedFile fileUploaded)
{
bool validMime = ValidateMimeType(fileUploaded);
if (!validMime) return false;
bool validSize = ValidateFileSize(fileUploaded);
if (!validSize) return false;
bool validUpload = ValidateRepeatUpload(fileUploaded);
if (!validUpload) return false;
return true;
}
}
}

Configuring a new validator in Sitecore requires creating a new BaseValidator Item under the default path of /sitecore/system/Modules/Web Forms for Marketers/Settings/Validation. You will want to enter your the assembly and class into the fields of the same name as well as a error message for when the validator returns false.

Finally to add the validator to validate the appropriate field, navigate to the field, under the default path of /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types, find the Field Type Item, in the Validator field select your new validator.

Mime Type Detection

Now that we have the file from the FileUpload field we need to validate we can go ahead and make a Mime Detection utilities class.

The class contains byte array properties relating to each type of file; pdf, docx, gif etc. The main function accepts a byte array of the file and then compares the arrangement against the properties, returning the mime type when a match is made. File extension does have some limited use, but only to improve accuracy of similar file types.


namespace ISlayTitans.Business.IO
{
public class MimeTypeUtil
{
private readonly byte[] BMP = { 66, 77 };
private readonly byte[] DOC = { 208, 207, 17, 224, 161, 177, 26, 225 };
private readonly byte[] EXE_DLL = { 77, 90 };
private readonly byte[] GIF = { 71, 73, 70, 56 };
private readonly byte[] ICO = { 0, 0, 1, 0 };
private readonly byte[] JPG = { 255, 216, 255 };
private readonly byte[] MP3 = { 255, 251, 48 };
private readonly byte[] OGG = { 79, 103, 103, 83, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0 };
private readonly byte[] PDF = { 37, 80, 68, 70, 45, 49, 46 };
private readonly byte[] PNG = { 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82 };
private readonly byte[] RAR = { 82, 97, 114, 33, 26, 7, 0 };
private readonly byte[] SWF = { 70, 87, 83 };
private readonly byte[] TIFF = { 73, 73, 42, 0 };
private readonly byte[] TORRENT = { 100, 56, 58, 97, 110, 110, 111, 117, 110, 99, 101 };
private readonly byte[] TTF = { 0, 1, 0, 0, 0 };
private readonly byte[] WAV_AVI = { 82, 73, 70, 70 };
private readonly byte[] WMV_WMA = { 48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108 };
private readonly byte[] ZIP_DOCX = { 80, 75, 3, 4 };
public string GetMimeType(byte[] file, string fileName)
{
Assert.IsNotNull(file, "the file can not be null");
Assert.IsNotNullOrEmpty(fileName, "the fileName can not be null");
string mime = MimeType.DefaultUnknown;
string extension = Path.GetExtension(fileName) != null
? Path.GetExtension(fileName).ToUpper()
: string.Empty;
if (file.Take(2).SequenceEqual(BMP))
{
mime = MimeType.ImageBmp;
}
else if (file.Take(8).SequenceEqual(DOC))
{
mime = MimeType.ApplicationMsWord;
}
else if (file.Take(2).SequenceEqual(EXE_DLL))
{
mime = MimeType.ApplicationXMsDownload;
}
else if (file.Take(4).SequenceEqual(GIF))
{
mime = MimeType.ImageGif;
}
else if (file.Take(4).SequenceEqual(ICO))
{
mime = MimeType.ImageXIcon;
}
else if (file.Take(3).SequenceEqual(JPG))
{
mime = MimeType.ImageJpeg;
}
else if (file.Take(3).SequenceEqual(MP3))
{
mime = MimeType.AudioMPeg;
}
else if (file.Take(14).SequenceEqual(OGG))
{
if (extension == ".OGX")
{
mime = MimeType.ApplicationOgg;
}
else if (extension == ".OGA")
{
mime = MimeType.AudioOgg;
}
else
{
mime = MimeType.VideoOgg;
}
}
else if (file.Take(7).SequenceEqual(PDF))
{
mime = MimeType.ApplicationPdf;
}
else if (file.Take(16).SequenceEqual(PNG))
{
mime = MimeType.ImagePng;
}
else if (file.Take(7).SequenceEqual(RAR))
{
mime = MimeType.ApplicationXRarCompressed;
}
else if (file.Take(3).SequenceEqual(SWF))
{
mime = MimeType.ApplicationXShockwaveFlash;
}
else if (file.Take(4).SequenceEqual(TIFF))
{
mime = MimeType.ImageTiff;
}
else if (file.Take(11).SequenceEqual(TORRENT))
{
mime = MimeType.ApplicationXBittorrent;
}
else if (file.Take(5).SequenceEqual(TTF))
{
mime = MimeType.ApplicationXFontTtf;
}
else if (file.Take(4).SequenceEqual(WAV_AVI))
{
mime = extension == ".AVI" ? MimeType.VideoXMsVideo : MimeType.AudioXWav;
}
else if (file.Take(16).SequenceEqual(WMV_WMA))
{
mime = extension == ".WMA" ? MimeType.AudioXMsWma : MimeType.VideoxMsWmv;
}
else if (file.Take(4).SequenceEqual(ZIP_DOCX))
{
mime = extension == ".DOCX" ? MimeType.ApplicationDocx : MimeType.ApplicationXZipCompressed;
}
return mime;
}
}
}

view raw

MimeTypeUtil.cs

hosted with ❤ by GitHub

Having completed the complex part we need to wire up the ValidateMimeType method to use our new MimeUtilities – reading the byte array in the stream before passing it to get the Mime type.

With the return value from the GetMimeType method it’s now time to determine if the file should be uploaded or not. This is easily achieved via checking the identified Mime type is present in a collection of permitted file types. In this example, the field should only allow images therefore I created a list property holding png, gif, jpeg etc. If the file is present the method returns true.


private bool ValidateMimeType(HttpPostedFile postedFile)
{
bool valid = false;
try
{
Stream stream = postedFile.InputStream;
var mimeTypeUtil = new MimeTypeUtil();
string mime = mimeTypeUtil.GetMimeType(ReadFully(stream), postedFile.FileName);
valid = PermittedMimeTypes.Contains(mime);
}
catch (Exception ex)
{
Log.Error("Error occurred when determining mime type of file", ex, this);
throw;
}
return valid;
}
private static byte[] ReadFully(Stream input)
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
private IEnumerable<string> _permittedMimeTypes;
private IEnumerable<string> PermittedMimeTypes
{
get
{
if (_permittedMimeTypes == null)
{
_permittedMimeTypes = new List<string>()
{
MimeType.ImageBmp,
MimeType.ImageGif,
MimeType.ImageJpeg,
MimeType.ImagePng,
};
}
return _permittedMimeTypes;
}
}

This could easily be extended to allow a Content Editor to define via Sitecore which File types are allowed per field.

That’s pretty much all you need to implement a more robust way of sniffing out what files your users are really uploading to your site and putting a stop to it! There are other logical steps you may want to implement – identification and limiting repeat uploads or implementing a file size limitation. An example of the latter is shown below.

File size check


private const int DefaultFileSizeLimit = 3000000;
private int? _fileSizeLimitInBytes;
private int FileSizeLimitInBytes
{
get
{
if (_fileSizeLimitInBytes == null || _fileSizeLimitInBytes == DefaultFileSizeLimit)
{
_fileSizeLimitInBytes = DefaultFileSizeLimit;
Item fileUploadConfig = ItemNodes.SiteConfig.Children.FirstOrDefault(x => x.TemplateID == Enumerators.SitecoreConfig.Guids.Templates.FileUploadConfigId);
if (fileUploadConfig != null && fileUploadConfig.Fields[Enumerators.SitecoreConfig.Fields.Global.ImageFileSizeLimit] != null
&& !string.IsNullOrEmpty(fileUploadConfig[Enumerators.SitecoreConfig.Fields.Global.ImageFileSizeLimit]))
{
int sizeInMegaBytes;
bool success =
int.TryParse(fileUploadConfig[Enumerators.SitecoreConfig.Fields.Global.ImageFileSizeLimit],
out sizeInMegaBytes);
if (success)
_fileSizeLimitInBytes = sizeInMegaBytes * 1000000;
}
}
return _fileSizeLimitInBytes.Value;
}
}
private bool ValidateFileSize(HttpPostedFile postedFile)
{
var sizeInBytes = postedFile.ContentLength;
return (sizeInBytes <= FileSizeLimitInBytes);
}

And that’s it!

Pretty nice way of implementing security of uploaded files; sniffing out those MIME types and throwing away files we don’t want. There are a few good posts about using Byte Array Sequencing around the net if you wanna read more.

Leave a comment