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":

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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!

Comments !