Restrict IP Access of Directories and Files in Your Sitecore Web Application Using a httpRequestBegin Pipeline Processor
Last week my friend and colleague Greg Coffman had asked me if I knew of a way to restrict IP access to directories within the Sitecore web application, and I recalled reading a post by Alex Shyba quite some time ago.
Although Alex’s solution is probably good enough in most circumstances, I decided to explore other solutions, and came up with the following <httpRequestBegin> pipeline processor as another way to accomplish this:
using System; using System.Collections.Generic; using System.Linq; using System.Web.Hosting; using Sitecore.Configuration; using Sitecore.Diagnostics; using Sitecore.Pipelines.HttpRequest; using Sitecore.Web; namespace Sitecore.Sandbox.Pipelines.HttpRequest { public class FilePathRestrictor : HttpRequestProcessor { public override void Process(HttpRequestArgs args) { Assert.ArgumentNotNull(args, "args"); if (!ShouldRedirect(args)) { return; } RedirectToNoAccessUrl(); } private bool ShouldRedirect(HttpRequestArgs args) { return CanProcess(args, GetFilePath(args)) && !CanAccess(args.Context.Request.UserHostAddress); } protected virtual string GetFilePath(HttpRequestArgs args) { if (string.IsNullOrWhiteSpace(Context.Page.FilePath)) { return args.Url.FilePath; } return Context.Page.FilePath; } protected virtual bool CanProcess(HttpRequestArgs args, string filePath) { return !string.IsNullOrWhiteSpace(filePath) && !string.IsNullOrWhiteSpace(RootFilePath) && AllowedIPs != null && AllowedIPs.Any() && (HostingEnvironment.VirtualPathProvider.DirectoryExists(filePath) || HostingEnvironment.VirtualPathProvider.FileExists(filePath)) && args.Url.FilePath.StartsWith(RootFilePath, StringComparison.CurrentCultureIgnoreCase) && !string.IsNullOrWhiteSpace(args.Context.Request.UserHostAddress) && !string.Equals(filePath, Settings.NoAccessUrl, StringComparison.CurrentCultureIgnoreCase); } protected virtual bool CanAccess(string ip) { Assert.ArgumentNotNullOrEmpty(ip, "ip"); return AllowedIPs.Contains(ip); } protected virtual void RedirectToNoAccessUrl() { WebUtil.Redirect(Settings.NoAccessUrl); } protected virtual void AddAllowedIP(string ip) { if (string.IsNullOrWhiteSpace(ip) || AllowedIPs.Contains(ip)) { return; } AllowedIPs.Add(ip); } private string RootFilePath { get; set; } private IList<string> _AllowedIPs; private IList<string> AllowedIPs { get { if (_AllowedIPs == null) { _AllowedIPs = new List<string>(); } return _AllowedIPs; } } } }
The pipeline processor above determines whether the IP making the request has access to the directory or file on the file system — a list of IP addresses that should have access are passed to the pipeline processor via a configuration file, and the code does check to see if the requested URL is a directory or a file on the file system — by matching the beginning of the URL with a configuration defined root path.
If the user does not have access to the requested path, s/he is redirected to the “No Access Url” which is specified in the Sitecore instance’s configuration.
The list of IP addresses that should have access to the directory — including everything within it — and the root path are handed to the pipeline processor via the following patch configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <httpRequestBegin> <processor patch:before=" processor[@type='Sitecore.Pipelines.HttpRequest.FileResolver, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.HttpRequest.FilePathRestrictor, Sitecore.Sandbox"> <RootFilePath>/sitecore</RootFilePath> <AllowedIPs hint="list:AddAllowedIP"> <IP>127.0.0.2</IP> </AllowedIPs> </processor> </httpRequestBegin> </pipelines> </sitecore> </configuration>
Since my IP is 127.0.0.1, I decided to only allow 127.0.0.2 access to my Sitecore directory — this also includes everything within it — in the above configuration file for testing.
After navigating to /sitecore of my local sandbox instance, I was redirected to the “No Access Url” page defined in my Web.config:
If you have any thoughts on this, or know of other solutions, please share in a comment.
Add the HTML5 Range Input Control into Web Forms for Marketers in Sitecore
A couple of weeks ago, I was researching what new input controls exist in HTML5 — I am quite a dinosaur when it comes to front-end code, and felt it was a good idea to see what is currently available or possible — and discovered the range HTML control:
I immediately wanted to add this HTML5 input control into Web Forms for Marketers in Sitecore, and built the following control class as a proof of concept:
using System; using System.Web.UI; using System.Web.UI.WebControls; using Sitecore.Diagnostics; using Sitecore.Form.Web.UI.Controls; namespace Sitecore.Sandbox.Form.Web.UI.Controls { public class Range : InputControl { public Range() : this(HtmlTextWriterTag.Div) { } public Range(HtmlTextWriterTag tag) : base(tag) { } protected override void OnInit(EventArgs e) { base.textbox.CssClass = "scfSingleLineTextBox"; base.help.CssClass = "scfSingleLineTextUsefulInfo"; base.generalPanel.CssClass = "scfSingleLineGeneralPanel"; base.title.CssClass = "scfSingleLineTextLabel"; this.Controls.AddAt(0, base.generalPanel); this.Controls.AddAt(0, base.title); base.generalPanel.Controls.AddAt(0, base.help); base.generalPanel.Controls.AddAt(0, textbox); } protected override void DoRender(HtmlTextWriter writer) { textbox.Attributes["type"] = "range"; TrySetIntegerAttribute("min", MinimumValue); TrySetIntegerAttribute("max", MaximumValue); TrySetIntegerAttribute("step", StepInterval); EnsureDefaultValue(); textbox.MaxLength = 0; base.DoRender(writer); } protected virtual void TrySetIntegerAttribute(string attributeName, string value) { int integerValue; if (int.TryParse(value, out integerValue)) { SetAttribute(attributeName, integerValue.ToString()); } } protected virtual void SetAttribute(string attributeName, string value) { Assert.ArgumentNotNull(textbox, "textbox"); Assert.ArgumentNotNullOrEmpty(attributeName, "attributeName"); textbox.Attributes[attributeName] = value; } protected virtual void EnsureDefaultValue() { int value; if (!int.TryParse(Text, out value)) { textbox.Text = string.Empty; } } public string MinimumValue { get; set; } public string MaximumValue { get; set; } public string StepInterval { get; set; } } }
Most of the magic behind how this works occurs in the DoRender() method above. In that method we are changing the “type” attribute on the TextBox instance defined in the parent InputControl class to be “range” instead of “text”: this is how the browser will know that it is to render a range control instead of a textbox.
The DoRender() method also delegates to other helper methods: one to set the default value for the control, and another to add additional attributes to our control — the “step”, “min”, and “max” attributes in particular (you can learn more about these attributes by reading this specification for the range control) — and these are only set when values are passed to our code via XML defined in Sitecore for the control:
Let’s test this out!
I whipped up a test form, and added a range field to it:
This is what the form looked like on the page before I clicked the submit button (trust me, that’s 75 😉 ):
After clicking submit, I was given a confirmation message:
As you can see in the Form Reports for our test form, the value selected on the range control was captured:
I will admit that I had a lot of fun adding this range input control into Web Forms for Marketers but do question whether anyone would use this control.
Why?
I found no way to add label markers for the different values on the control (if you are aware of a way to do this, please leave a comment).
Also, it should be noted that this control will not work in Internet Explorer 9 or earlier versions.
If you can think of ways around making this better, or have ideas for other HTML5 controls that could/should be added to Web Forms for Marketers, please share in a comment.
Restrict Certain Files from Being Attached to Web Forms for Marketers Forms in Sitecore
Last week I was given a task to research how to prevent certain files from being attached to Web Forms for Marketers (WFFM) forms: basically files that have certain extensions, or files that exceed a specified size.
I have not seen this done before in WFFM, so I did what comes naturally to me: I Googled! 🙂
After a few unsuccessful searches on the internet — if you have some examples on how others have accomplished this in WFFM, please share in a comment — I decided to dig into the WFFM assemblies to see what would be needed to accomplish this, and felt using custom WFFM field validators would be the way to go.
I thought having a custom validator to check the attached file’s MIME type would be a better solution over one that checks the file’s extension — thanks to Sitecore MVP Yogesh Patel for giving me the idea from his post on restricting certain files from being uploading into Sitecore by checking their MIME types — since a malefactor could attach a restricted file with a different extension to bypass the extension validation step.
That thought lead me to build the following custom WFFM field validator:
using System; using System.Collections.Generic; using System.Linq; using System.Web.UI.WebControls; using Sitecore.Form.Core.Validators; using Sitecore.Form.Web.UI.Controls; namespace Sitecore.Sandbox.Form.Core.Validators { public class RestrictedFileTypes : FormCustomValidator { public string MimeTypesNotAllowed { get { if (string.IsNullOrWhiteSpace(base.classAttributes["mimeTypesNotAllowed"])) { return string.Empty; } return base.classAttributes["mimeTypesNotAllowed"]; } set { base.classAttributes["mimeTypesNotAllowed"] = value; } } public RestrictedFileTypes() { } protected override bool OnServerValidate(string value) { IEnumerable<string> mimeTypesNotAllowed = GetMimeTypesNotAllowed(); FileUpload fileUpload = FindControl(ControlToValidate) as FileUpload; bool canProcess = mimeTypesNotAllowed.Any() && fileUpload != null && fileUpload.HasFile; if (!canProcess) { return true; } foreach(string mimeType in mimeTypesNotAllowed) { if (AreEqualIgnoreCase(mimeType, fileUpload.PostedFile.ContentType)) { return false; } } return true; } private IEnumerable<string> GetMimeTypesNotAllowed() { if (string.IsNullOrWhiteSpace(MimeTypesNotAllowed)) { return new List<string>(); } return MimeTypesNotAllowed.Split(new []{',', ';'}, StringSplitOptions.RemoveEmptyEntries); } private static bool AreEqualIgnoreCase(string stringOne, string stringTwo) { return string.Equals(stringOne, stringTwo, StringComparison.CurrentCultureIgnoreCase); } } }
Restricted MIME types are passed to the custom validator through a parameter named MimeTypesNotAllowed, and these are injected into a property of the same name.
The MIME types can be separated by commas or semicolons, and the code above splits the string along these delimiters into a collection, checks to see if the uploaded file — we get the uploaded file via the FileUpload control on the form for the field we are validating — has a restricted MIME type by iterating over the collection of restricted MIME types, and comparing each entry to its MIME type. If there is a match, we return “false”: basically the field is invalid.
If no MIME types were set for the validator, or no file was uploaded, we return “true”: the field is valid. We do this for the case where the field is not required, or there is a required field validator set for it, and we don’t want to interfere with its validation check.
I then mapped the above validator in Sitecore:
After saving the above validator Item in Sitecore, I built the following validator class to check to see if a file exceeds a certain size:
using System; using System.Collections.Generic; using System.Linq; using System.Web.UI.WebControls; using Sitecore.Form.Core.Validators; using Sitecore.Form.Web.UI.Controls; namespace Sitecore.Sandbox.Form.Core.Validators { public class RestrictedFileSize : FormCustomValidator { public int MaxFileSize { get { int maxSize; if (int.TryParse(base.classAttributes["mimeTypesNotAllowed"], out maxSize)) { return maxSize; } return 0; } set { base.classAttributes["mimeTypesNotAllowed"] = value.ToString(); } } public RestrictedFileSize() { } protected override bool OnServerValidate(string value) { FileUpload fileUpload = FindControl(ControlToValidate) as FileUpload; if (!fileUpload.HasFile) { return true; } return fileUpload.PostedFile.ContentLength <= MaxFileSize; } } }
Just as we had done in the other validator, we grab the FileUpload from the form, and see if there is a file attached. If there is no file attached, we return “true”: we don’t want to say the field is invalid when the field is not required.
We then return whether the uploaded file is less than or equal to the specified maximum size in bytes — this is set on a parameter named MaxFileSize which gets injected into the MaxFileSize property of the validator instance.
I then registered the above validator in Sitecore:
I then decided to create a custom WFFM field type for the purposes of mapping our validators above, so that we don’t enforce these restrictions on the “out of the box” WFFM “File Upload” field type:
I then set the new field type on a file field I added to a test WFFM form:
Let’s see how we did!
Let’s try to upload an executable that exceeds the maximum upload size limit:
As you can see both validators were triggered for this file:
How about a JavaScript file? Let’s try to attach one:
Nope, we can’t attach that file either:
How about an image that is larger than the size limit? Let’s try one:
Nope, we can’t upload that either:
Let’s try an image that is under 100KB:
Yes, we can attach that file since it’s not restricted, and the form did submit:
If you have any thoughts on this, or other ideas around preventing certain files from being attached to WFFM form submissions, please share in a comment.