Skip to main content

Validating file uploads in Sitecore WFFM

Tags:
Reading time: 4 minutes Suggest a change

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!