MVC way to check the File Extension

Following on from my article about checking for the file signature to ensure that the file is who it says it is

I decide to take it further and produce a Data Validation attribute, I found that the Microsoft.Web.Mvc.dll library already has one ,but it only checks for the file names, which is generally okay.

So I thought I’d enhance the Microsoft FileExtension attribute, but it was sealed……!  Grrrrrrrr  But Microsoft now have most of its code as open source, yaaaaa  🙂

So I downloaded the source and created my own variation of the FileExtensionsAttribute

First we need to generate DataTypeAttribute, and IClientValidatable to inherit.

IClientValidatable

using System.Collections.Generic;
using System.Web.Mvc;

public interface IClientValidatable
{
    IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context);
}

ModelClientValidationRule

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
public class ModelClientValidationRule
{
    private readonly Dictionary<string, object> _validationParameters = new Dictionary<string, object>();
    private string _validationType;

    public string ErrorMessage { get; set; }

    public IDictionary<string, object> ValidationParameters
    {
        get { return _validationParameters; }
    }

    public string ValidationType
    {
        get { return _validationType jQuery15201478678032162548_1380881227039 String.Empty; }
        set { _validationType = value; }
    }
}

Now the actual code for FileExtensionsAttribute

FileExtensionsAttribute

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using DataValidation.Properties;

/// <summary>
/// The file extensions, default is png,jpg,jpeg,gif.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class FileExtensionsAttribute : DataTypeAttribute, IClientValidatable
{
    private string _extensions;
    private string _extensionsSignatures;

    public FileExtensionsAttribute()
        : base("upload")
    {
        ErrorMessage = DataValidationResource.FileExtensionsAttribute_Invalid;
    }

    /// <summary>
    /// The file extensions, default is png,jpg,jpeg,gif.
    /// </summary>
    /// <value>
    /// The extensions.
    /// </value>
    public string Extensions
    {
        get { return String.IsNullOrWhiteSpace(_extensions) ? "png,jpg,jpeg,gif" : _extensions; }
        set { _extensions = value; }
    }

    /// <summary>
    /// The extensions signatures, this is the fileclass signature within the actual file.
    /// </summary>
    /// <value>
    /// The extensions signatures.
    /// </value>
    public string ExtensionsSignatures
    {
        get { return String.IsNullOrWhiteSpace(_extensionsSignatures) ? "" : _extensionsSignatures; }
        set { _extensionsSignatures = value; }
    }

    private string ExtensionsFormatted
    {
        get { return ExtensionsParsed.Aggregate((left, right) => left + ", " + right); }
    }

    [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "These strings are normalized to lowercase because they are presented to the user in lowercase format")]
    private string ExtensionsNormalized
    {
        get { return Extensions.Replace(" ", String.Empty).Replace(".", String.Empty).ToLowerInvariant(); }
    }

    [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "These strings are normalized to lowercase because they are presented to the user in lowercase format")]
    private string ExtensionsSignaturesNormalized
    {
        get { return ExtensionsSignatures.Replace(" ", String.Empty).Replace(".", String.Empty).ToLowerInvariant(); }
    }

    private IEnumerable<string> ExtensionsParsed
    {
        get { return ExtensionsNormalized.Split(',').Select(e => "." + e); }
    }

    private IEnumerable<string> ExtensionsSignaturesParsed
    {
        get { return ExtensionsSignaturesNormalized.Split(','); }
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, ExtensionsFormatted);
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "extension",
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
        };
        rule.ValidationParameters["extension"] = ExtensionsNormalized;
        yield return rule;
    }

    public override bool IsValid(object value)
    {
        if (value == null)
        {
            return true;
        }

        var valueAsFileBase = value as HttpPostedFileBase;
        if (valueAsFileBase != null)
        {
            return ValidateExtension(valueAsFileBase.FileName) && ValidateFileContent(valueAsFileBase);
        }

        var valueAsString = value as string;
        return valueAsString != null && ValidateExtension(valueAsString);
    }

    [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "These strings are normalized to lowercase because they are presented to the user in lowercase format")]
    private bool ValidateExtension(string fileName)
    {
        try
        {
            return ExtensionsParsed.Contains(Path.GetExtension(fileName).ToLowerInvariant());
        }
        catch (ArgumentException)
        {
            return false;
        }
    }

    private bool ValidateFileContent(HttpPostedFileBase file)
    {
        var validExtensions = GetValidExtenstions();
            
        var br = new BinaryReader(file.InputStream);
        string fileclass;
        byte buffer;

        try
        {
            buffer = br.ReadByte();
            fileclass = buffer.ToString();
            buffer = br.ReadByte();
            fileclass += buffer.ToString();
        }
        catch
        {
            return false;
        }

        return string.IsNullOrWhiteSpace(ExtensionsSignatures)
            ? ExtensionsParsed.Where(validExtensions.ContainsKey).Any(extension => validExtensions[extension].ToString() == fileclass) 
            : ExtensionsSignaturesParsed.Any(extensionsSignatures => extensionsSignatures == fileclass);
    }

    private Dictionary<string, int> GetValidExtenstions()
    {
        var validExtenstions = new Dictionary<string, int>
                                    {
                                        {".gif", 7173},
                                        {".jpg", 255216},
                                        {".png", 13780},
                                        {".bmp", 6677},
                                        {".txt", 239187},
                                        {"/", 239187},
                                        {".asp", 239187},
                                        {".sql", 239187},
                                        {".xls", 208207},
                                        {".doc", 208207},
                                        {".docx", 208207},
                                        {".dot", 208207},
                                        {".dotx", 208207},
                                        {".ppt", 208207},
                                        {".xml", 6063},
                                        {".html", 6033},
                                        {".js", 4742},
                                        {".xlsx", 8075},
                                        {".zip", 8075},
                                        {".ptx", 8075},
                                        {".mmap", 8075},
                                        {".rar", 8297},
                                        {".accdb", 01},
                                        {".mdb", 01},
                                        {".exe", 7790},
                                        {".dll", 7790},
                                        {".bat", 64101},
                                        {".tiff", 7373},
                                        {".tif", 7373}
                                    };
        return validExtenstions;
    }
}

You’ll notice the ValidateFileContent method which is the additional feature I’ve added to check for the file signature.

That’s it job done

A sample project can be found here

FileUploader.zip (9.24 mb)