Validating file uploads in Sitecore WFFM

Our Sitecore installation was in dire need of a way to lock down file uploads on forms built with the Web Forms for Marketers (WFFM) module; out of the box, it doesn’t do any checking at all, which can lead to some risky situations. I tacked on a simple whitelist attribute to the UploadFile control, and our security engineer can breathe easy.

Using reflection, I pulled the source for the UploadFile control and added a new VisualProperty, “Allowed extensions”:

using Sitecore.Form.Core.Attributes;
using Sitecore.Form.Core.Controls.Data;
using Sitecore.Form.Core.Media;
using Sitecore.Form.Core.Visual;
using Sitecore.Form.UI.Adapters;
using Sitecore.Form.Web.UI.Controls;
using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace example.sitecore.wffm.fileuploadvalidator
{
    [Adapter(typeof(FileUploadAdapter))]
    public class UploadFile : ValidateControl, IHasTitle
    {
        private static readonly string baseCssClassName = "scfFileUploadBorder";
        protected Panel generalPanel = new Panel();
        protected System.Web.UI.WebControls.Label title = new System.Web.UI.WebControls.Label();
        protected FileUpload upload = new FileUpload();
        private string uploadDir = "/sitecore/media library";
        private string allowedExtensions = "";

        public override string ID
        {
            get
            {
                return this.upload.ID;
            }
            set
            {
                this.title.ID = value + "text";
                this.upload.ID = value;
                base.ID = value + "scope";
            }
        }

        [VisualProperty("Upload To:", 0)]
        [DefaultValue("/sitecore/media library")]
        [VisualFieldType(typeof(SelectDirectoryField))]
        [VisualCategory("Upload")]
        public string UploadTo
        {
            get
            {
                return this.uploadDir;
            }
            set
            {
                this.uploadDir = value;
            }
        }

        [VisualProperty("Allowed extensions:", 1000)]
        [DefaultValue("")]
        [VisualCategory("Validation")]
        public string AllowedExtensions
        {
            get
            {
                return this.classAtributes["allowedExtensions"];
            }

            set
            {
                this.classAtributes["allowedExtensions"] = value;
            }
        }

        public override ControlResult Result
        {
            get
            {
                if (this.upload.HasFile)
                    return new ControlResult(this.ControlName, (object)new PostedFile(this.upload.FileBytes, this.upload.FileName, this.UploadTo), "medialink");
                else
                    return new ControlResult(this.ControlName, (object)null, string.Empty);
            }
        }

        public string Title
        {
            get
            {
                return this.title.Text;
            }
            set
            {
                this.title.Text = value;
            }
        }

        [DefaultValue("scfFileUploadBorder")]
        [VisualFieldType(typeof(CssClassField))]
        [VisualProperty("Css Class:", 600)]
        public new string CssClass
        {
            get
            {
                return base.CssClass;
            }
            set
            {
                base.CssClass = value;
            }
        }

        protected override Control ValidatorContainer
        {
            get
            {
                return (Control)this;
            }
        }

        protected override Control InnerValidatorContainer
        {
            get
            {
                return (Control)this.generalPanel;
            }
        }

        public UploadFile(HtmlTextWriterTag tag)
            : base(tag)
        {
            this.CssClass = UploadFile.baseCssClassName;
        }

        public UploadFile()
            : this(HtmlTextWriterTag.Div)
        {
        }

        public override void RenderControl(HtmlTextWriter writer)
        {
            this.DoRender(writer);
        }

        protected virtual void DoRender(HtmlTextWriter writer)
        {
            base.RenderControl(writer);
        }

        protected override void OnInit(EventArgs e)
        {
            this.upload.CssClass = "scfFileUpload";
            this.help.CssClass = "scfFileUploadUsefulInfo";
            this.title.CssClass = "scfFileUploadLabel";
            this.title.AssociatedControlID = this.upload.ID;
            this.generalPanel.CssClass = "scfFileUploadGeneralPanel";
            this.Controls.AddAt(0, (Control)this.generalPanel);
            this.Controls.AddAt(0, (Control)this.title);
            this.generalPanel.Controls.AddAt(0, (Control)this.help);
            this.generalPanel.Controls.AddAt(0, (Control)this.upload);
        }
    }
}

This was then coupled with a FormCustomValidator which took the value entered into the VisualProperty and split it into an array of allowed extensions (by using the comma as a delineating token):

using Sitecore.Form.Core.Validators;
using System;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.UI.WebControls;

namespace example.sitecore.wffm.fileuploadvalidator
{
    public class FileUploadValidator : FormCustomValidator
    {
        [Browsable(true)]
        public string AllowedExtensions
        {
            get
            {
                return this.classAttributes["allowedExtensions"] ?? string.Empty;
            }

            set
            {
                this.classAttributes["allowedExtensions"] = value;
            }
        }

        public FileUploadValidator()
        {
            this.AllowedExtensions = "";
        }

        protected override void OnLoad(EventArgs e)
        {
            this.ErrorMessage = string.Format(this.ErrorMessage,  "{0}", this.AllowedExtensions.Replace(",", ", "));
            this.Text = this.ErrorMessage;
            base.OnLoad(e);
        }

        protected override bool OnServerValidate(string value)
        {
            if(AllowedExtensions.Trim().Length == 0)
            {
                return true;
            }

            Regex rgxExtension = new Regex(".+\\.([^\\.]+)$");
            string filename = ((FileUpload)base.FindControl(this.ControlToValidate)).FileName;
            Match matchExtension = rgxExtension.Match(filename);
            string extension = matchExtension.Groups[1].Value.ToLower();
            string[] allowedExtensions = AllowedExtensions.Split(',');

            if (!allowedExtensions.Contains(extension))
            {
                return false;
            }

            return true;
        }
    }
}

We now have the ability to lock down a file upload field for a curriculum vitae/resume to, for instance, “docx,doc,pdf,rtf,txt”. Someone can’t send along a ZIP file or an EXE instead of a document. Additionally, we still have the freedom to avoid using the whitelist altogether by leaving the “Allowed extensions” value blank (though I wouldn’t recommend it).

I hope this helps somebody out there!