Home » Security

Category Archives: Security

Advertisements

Restrict Certain Files from Being Uploaded Through Web Forms for Marketers Forms in Sitecore: an Alternative Approach

This past weekend I noticed the <formUploadFile> pipeline in Web Forms for Marketers (WFFM), and wondered whether I could create an alternative solution to the one I had shared in this post — I had built a custom WFFM field type to prevent certain files from being uploaded through WFFM forms.

After some tinkering, I came up the following class to serve as a <formUploadFile> pipeline processor:

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

using Sitecore.Diagnostics;

using Sitecore.Form.Core.Pipelines.FormUploadFile;

namespace Sitecore.Sandbox.Form.Pipelines.FormUploadFile
{
    public class CheckMimeTypesNotAllowed
    {
        static CheckMimeTypesNotAllowed()
        {
            MimeTypesNotAllowed = new List<string>();
        }

        public void Process(FormUploadFileArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            string mimeType = GetMimeType(args.File.FileName);
            if (IsMimeTypeAllowed(mimeType))
            {
                return;
            }

            throw new Exception(string.Format("Uploading a file with MIME type {0} is not allowed", mimeType));
        }

        protected virtual string GetMimeType(string fileName)
        {
            Assert.ArgumentNotNullOrEmpty(fileName, "fileName");
            return MimeMapping.GetMimeMapping(fileName);
        }

        protected virtual bool IsMimeTypeAllowed(string mimeType)
        {
            foreach (string mimeTypeNotAllowed in MimeTypesNotAllowed)
            {
                if (AreEqualIgnoreCase(mimeTypeNotAllowed, mimeType))
                {
                    return false;
                }    
            }

            return true;
        }

        private static bool AreEqualIgnoreCase(string stringOne, string stringTwo)
        {
            return string.Equals(stringOne, stringTwo, StringComparison.CurrentCultureIgnoreCase);
        }

        private static void AddMimeTypeNotAllowed(string mimeType)
        {
            if (string.IsNullOrWhiteSpace(mimeType) || MimeTypesNotAllowed.Contains(mimeType))
            {
                return;
            }

            MimeTypesNotAllowed.Add(mimeType);
        }

        private static IList<string> MimeTypesNotAllowed { get; set; }
    }
}

The class above adds restricted MIME types to a list — these MIME types are defined in a configuration file shown below — and checks to see if the uploaded file’s MIME type is in the restricted list. If it is restricted, an exception is thrown with some information — throwing an exception in WFFM prevents the form from being submitted.

MIME types are inferred using System.Web.MimeMapping.GetMimeMapping(string fileName), a method that is new in .NET 4.5 (this solution will not work in older versions of .NET).

I then registered the class above as a <formUploadFile> pipeline processor via the following configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <formUploadFile>
        <processor patch:before="processor[@type='Sitecore.Form.Core.Pipelines.FormUploadFile.Save, Sitecore.Forms.Core']"
                   type="Sitecore.Sandbox.Form.Pipelines.FormUploadFile.CheckMimeTypesNotAllowed">
          <mimeTypesNotAllowed hint="list:AddMimeTypeNotAllowed">
            <mimeType>application/octet-stream</mimeType>
          </mimeTypesNotAllowed >
        </processor>  
      </formUploadFile>
    </pipelines>
  </sitecore>
</configuration>

In case you are wondering, the MIME type being passed to the processor in the configuration file is for executables.

Let’s see how we did. 😉

I whipped up a test form, and pulled it up in a browser:

form-for-testing

I then selected an executable:

select-executable

I saw this after an attempt to submit the form:

form-upload-exception

And my log file expressed why:

exception-in-log

I then copied a jpeg into my test folder, and selected it to be uploaded:

select-jpg

After clicking the submit button, I was given a nice confirmation message:

success-upload

There is one problem with the solution above that I would like to point out: it does not address the issue of file extensions being changed. I could not solve this problem since the MIME type of the file being uploaded cannot be determined from the Sitecore.Form.Core.Media.PostedFile instance set in the arguments object: there is no property for it. 😦

If you know of another way to determine MIME types on Sitecore.Form.Core.Media.PostedFile instances, or have other ideas for restricting certain files from being uploaded through WFFM, please share in a comment.

Advertisements

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:

no-access

If you have any thoughts on this, or know of other solutions, please share in a comment.

Perform a Virus Scan on Files Uploaded into Sitecore

Last week I took notice of a SDN forum post asking whether there are any best practices for checking files uploaded into Sitecore for viruses.

Although I am unaware of there being any best practices for this, adding a processor into the <uiUpload> pipeline to perform virus scans has been on my plate for the past month. Not being able to find an open source .NET antivirus library has been my major blocker for building one.

However, after many hours of searching the web — yes, I do admit some of this time is sprinkled with idle surfing — over multiple weeks, I finally discovered nClam — a .NET client library for scanning files using a Clam Antivirus server (I setup one using instructions enumerated here).

Before I continue, I would like to caution you on using this or any antivirus solution before doing a substantial amount of research — I do not know how robust the solution I am sharing actually is, and I am by no means an antivirus expert. The purpose of this post is to show how one might go about adding antivirus capabilities into Sitecore, and I am not advocating or recommending any particular antivirus software package. Please consult with an antivirus expert before using any antivirus software/.NET client library .

The solution I came up with uses the adapter design pattern — it basically wraps and makes calls to the nClam library, and is accessible through the following antivirus scanner interface:

using System.IO;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public interface IScanner
    {
        ScanResult Scan(Stream sourceStream);

        ScanResult Scan(string filePath);
    }
}

This interface defines the bare minimum methods our antivirus classes should have. Instances of these classes should offer the ability to scan a file through the file’s Stream, or scan a file located on the server via the supplied file path.

The two scan methods defined by the interface must return an instance of the following data transfer object:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ScanResult
    {
        public string Message { get; set; }

        public ScanResultType ResultType { get; set; }
    }
}

Instances of the above class contain a detailed message coupled with a ScanResultType enumeration value which conveys whether the scanned file is clean, contains a virus, or something else went wrong during the scanning process:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public enum ScanResultType
    {
        Clean,
        VirusDetected,
        Error,
        Unknown
    }
}

I used the ClamScanResults enumeration as a model for the above.

I created and used the ScanResultType enumeration instead of the ClamScanResults enumeration so that this solution can be extended for other antivirus libraries — or calls could be made to other antivirus software through the command-line — and these shouldn’t be tightly coupled to the nClam library.

I then wrapped the nClam library calls in the following ClamScanner class:

using System;
using System.IO;
using System.Linq;

using Sitecore.Diagnostics;

using nClam;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ClamScanner : IScanner
    {
        private ClamClient Client { get; set; }

        public ClamScanner(string server, string port)
            : this(server, int.Parse(port))
        {
        }

        public ClamScanner(string server, int port)
            : this(new ClamClient(server, port))
        {
        }

        public ClamScanner(ClamClient client)
        {
            SetClient(client);
        }

        private void SetClient(ClamClient client)
        {
            Assert.ArgumentNotNull(client, "client");
            Client = client;
        }

        public ScanResult Scan(Stream sourceStream)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(sourceStream));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        public ScanResult Scan(string filePath)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(filePath));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        private static ScanResult CreateNewScanResult(ClamScanResult result)
        {
            Assert.ArgumentNotNull(result, "result");
            if (result.Result == ClamScanResults.Clean)
            {
                return CreateNewScanResult("Yay! No Virus found!", ScanResultType.Clean);
            }

            if (result.Result == ClamScanResults.VirusDetected)
            {
                string message = string.Format("Oh no! The {0} virus was found!", result.InfectedFiles.First().VirusName);
                return CreateNewScanResult(message, ScanResultType.VirusDetected);
            }

            if (result.Result == ClamScanResults.Error)
            {
                string message = string.Format("Something went terribly wrong somewhere. Details: {0}", result.RawResult);
                return CreateNewScanResult(message, ScanResultType.Error);
            }

            return CreateNewScanResult("I have no clue about what just happened.", ScanResultType.Unknown);
        }

        private static ScanResult CreateNewExceptionScanResult(Exception ex)
        {
            string message = string.Format("Something went terribly wrong somewhere. Details:\n{0}", ex.ToString());
            return CreateNewScanResult(ex.ToString(), ScanResultType.Error);
        }

        private static ScanResult CreateNewScanResult(string message, ScanResultType type)
        {
            return new ScanResult
            {
                Message = message,
                ResultType = type
            };
        }
    }
}

Methods in the above class make calls to the nClam library, and return a ScanResult intance containing detailed information about the file scan.

Next I developed the following <uiUpload> pipeline processor to use instances of classes that define the IScanner interface above, and these instances are set via Sitecore when instantiating this pipeline processor — the ClamScanner type is defined in the patch configuration file shown later in this post:

using System.IO;
using System.Web;

using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.Upload;

using Sitecore.Sandbox.Security.Antivirus;

namespace Sitecore.Sandbox.Pipelines.Upload
{
    public class ScanForViruses
    {
        public void Process(UploadArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            foreach (string fileKey in args.Files)
            {
                HttpPostedFile file = args.Files[fileKey];
                ScanResult result = ScanFile(file.InputStream);
                if(result.ResultType != ScanResultType.Clean)
                {
                    args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Reason: {1}", file.FileName, result.Message));
                    Log.Warn(args.ErrorText, this);
                    args.AbortPipeline();
                }
            }
        }

        private ScanResult ScanFile(Stream fileStream)
        {
            Assert.ArgumentNotNull(fileStream, "fileStream");
            return Scanner.Scan(fileStream);
        }

        private IScanner Scanner { get; set; }
    }
}

Uploaded files are passed to the IScanner instance, and the pipeline is aborted if something isn’t quite right — this occurs when a virus is detected, or an error is reported by the IScanner instance. If a virus is discovered, or an error occurs, the message contained within the ScanResult instance is captured in the Sitecore log.

I then glued everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiUpload>
        <processor mode="on" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.Upload.ScanForViruses, Sitecore.Sandbox">
          <Scanner type="Sitecore.Sandbox.Security.Antivirus.ClamScanner">
            <param desc="server">localhost</param>
            <param desc="port">3310</param>
          </Scanner>
        </processor>
      </uiUpload>
    </processors>
  </sitecore>
</configuration>

I wish I could show you this in action when a virus is discovered in an uploaded file. However, I cannot put my system at risk by testing with an infected file.

But, I can show you what happens when an error is detected. I will do this by shutting down the Clam Antivirus server on my machine:

scanner-turned-off

On upload, I see the following:

tried-to-upload-file

When I opened up the Sitecore log, I see what the problem is:

log-error

Let’s turn it back on:

scanner-turned-on

I can now upload files again:

upload-no-error

If you have any suggestions on making this better, or know of a way to test it with a virus, please share in a comment below.

Restrict Certain Types of Files From Being Uploaded in Sitecore

Tonight I was struck by a thought while conducting research for a future blog post: should we prevent users from uploading certain types of files in Sitecore for security purposes?

You might thinking “Prevent users from uploading files? Mike, what on Earth are you talking about?”

What I’m suggesting is we might want to consider restricting certain types of files — specifically executables (these have an extension of .exe) and DOS command files (these have an extension of .com) — from being uploaded into Sitecore, especially when files can be easily downloaded from the media library.

Why should we do this?

Doing this will curtail the probability of viruses being spread among our Sitecore users — such could happen if one user uploads an executable that harbors a virus, and other users of our Sitecore system download that tainted executable, and run it on their machines.

As a “proof of concept”, I built the following uiUpload pipeline processor — the uiUpload pipeline lives in /configuration/sitecore/processors/uiUpload in your Sitecore instance’s Web.config — to restrict certain types of files from being uploaded into Sitecore:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Xml;

using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.Upload;

namespace Sitecore.Sandbox.Pipelines.Upload
{
    public class CheckForRestrictedFiles : UploadProcessor
    {
        private List<string> _RestrictedExtensions;
        private List<string> RestrictedExtensions
        {
            get
            {
                if (_RestrictedExtensions == null)
                {
                    _RestrictedExtensions = new List<string>();
                }

                return _RestrictedExtensions;
            }
        }

        public void Process(UploadArgs args)
        {
            foreach(string fileKey in args.Files)
            {
                string fileName = GetFileName(args.Files, fileKey);
                string extension = Path.GetExtension(fileName);
                if (IsRestrictedExtension(extension))
                {
                    args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Files with an extension of {1} are not allowed.", fileName, extension));
                    Log.Warn(args.ErrorText, this);
                    args.AbortPipeline();
                }
            }
        }

        private static string GetFileName(HttpFileCollection files, string fileKey)
        {
            Assert.ArgumentNotNull(files, "files");
            Assert.ArgumentNotNullOrEmpty(fileKey, "fileKey");
            return files[fileKey].FileName;
        }

        private bool IsRestrictedExtension(string extension)
        {
            return RestrictedExtensions.Exists(restrictedExtension => string.Equals(restrictedExtension, extension, StringComparison.CurrentCultureIgnoreCase));
        }

        protected virtual void AddRestrictedExtension(XmlNode configNode)
        {
            if (configNode == null || string.IsNullOrWhiteSpace(configNode.InnerText))
            {
                return;
            }

            RestrictedExtensions.Add(configNode.InnerText);
        }
    }
}

The class above ascertains whether each uploaded file has an extension that is restricted — restricted extensions are defined in the configuration file below, and are added to a list of strings via the AddRestrictedExtensions method — and logs an error message when a file possessing a restricted extension is encountered.

I then tied everything together using the following patch configuration file including specifying some file extensions to restrict:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiUpload>
        <processor mode="on" type="Sitecore.Sandbox.Pipelines.Upload.CheckForRestrictedFiles, Sitecore.Sandbox" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']">
          <restrictedExtensions hint="raw:AddRestrictedExtension">
            <!-- Be sure to prefix with a dot -->
            <extension>.exe</extension>
            <extension>.com</extension>
          </restrictedExtensions>
        </processor>
      </uiUpload>
    </processors>
  </sitecore>
</configuration>

Let’s try this out.

I went to the media library, and attempted to upload an executable:

upload-exe-dialog_001

After clicking the “Open” button, I was presented with the following:

upload-exe-error

An error in my Sitecore instance’s latest log file conveys why I could not upload the chosen file:

upload-exe-error-log

If you have thoughts on this, or have ideas for other processors that should be added to uiUpload pipeline, please share in a comment.

Encrypt Web Forms For Marketers Fields in Sitecore

In an earlier post, I walked you through how I experimented with data encryption of field values in Sitecore, and alluded to how I had done a similar thing for the Web Forms For Marketers (WFFM) module on a past project at work.

Months have gone by, and guilt has begun to gnaw away at my entire being — no, not really, I’m exaggerating a bit — but I definitely have been feeling bad for not sharing a solution.

In order to shake feeling bad, I decided to put my nose to the grindstone over the past few days to come up with a different solution than the one I had built at work, and this post shows the fruits of that labor.

I decided to reuse the interface I had created in my older post on data encryption. I am re-posting it here for reference:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Sitecore.Sandbox.Security.Encryption.Base
{
    public interface IEncryptor
    {
        string Encrypt(string input);
 
        string Decrypt(string input);
    }
}

I then asked myself “What encryption algorithm should I use?” I scavenged through the System.Security.Cryptography namespace in mscorlib.dll using .NET Reflector, and discovered some classes, when used together, achieve data encryption using the RC2 algorithm — an algorithm I know nothing about:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Security.Encryption
{
    public class RC2Encryptor : IEncryptor
    {
        public string Key { get; set; }

        private RC2Encryptor(string key)
        {
            SetKey(key);
        }

        private void SetKey(string key)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            Key = key;
        }

        public string Encrypt(string input)
        {
            return Encrypt(input, Key);
        }

        public static string Encrypt(string input, string key)
        {
            byte[] inputArray = UTF8Encoding.UTF8.GetBytes(input);
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
            rc2.Mode = CipherMode.ECB;
            rc2.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rc2.CreateEncryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            rc2.Clear();
            return System.Convert.ToBase64String(resultArray, 0, resultArray.Length);
        }

        public string Decrypt(string input)
        {
            return Decrypt(input, Key);
        }

        public static string Decrypt(string input, string key)
        {
            byte[] inputArray = System.Convert.FromBase64String(input);
            RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
            rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
            rc2.Mode = CipherMode.ECB;
            rc2.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rc2.CreateDecryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            rc2.Clear();
            return UTF8Encoding.UTF8.GetString(resultArray);
        }

        public static IEncryptor CreateNewRC2Encryptor(string key)
        {
            return new RC2Encryptor(key);
        }
    }
}

As I had mentioned in my previous post on data encryption, I am not a cryptography expert, nor a security expert.

I am not aware of how strong the RC2 encryption algorithm is, or what it would take to crack it. I strongly advise against using this algorithm in any production system without first consulting with a security expert. I am using it in this post only as an example.

If you happen to be a security expert, or are able to compare encryption algorithms defined in the System.Security.Cryptography namespace in mscorlib.dll, please share in a comment.

In a previous post on manipulating field values for WFFM forms, I had to define a new class that implements Sitecore.Forms.Data.IField in Sitecore.Forms.Core.dll in order to change field values — it appears the property mutator for the “out of the box” class is ignored — and decided to reuse it here:

using System;

using Sitecore.Forms.Data;

namespace Sitecore.Sandbox.Utilities.Manipulators.DTO
{
    public class WFFMField : IField
    {
        public string Data { get; set; }

        public Guid FieldId { get; set; }

        public string FieldName { get; set; }

        public IForm Form { get; set; }

        public Guid Id { get; internal set; }

        public string Value { get; set; }
    }
}

Next, I created a WFFM Data Provider that encrypts and decrypts field names and values:

using System;
using System.Collections.Generic;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Forms.Data;
using Sitecore.Forms.Data.DataProviders;
using Sitecore.Reflection;

using Sitecore.Sandbox.Security.DTO;
using Sitecore.Sandbox.Security.Encryption.Base;
using Sitecore.Sandbox.Security.Encryption;
using Sitecore.Sandbox.Utilities.Manipulators.DTO;

namespace Sitecore.Sandbox.WFFM.Forms.Data.DataProviders
{
    public class WFFMEncryptionDataProvider : WFMDataProviderBase
    {
        private WFMDataProviderBase InnerProvider { get; set; }
        private IEncryptor Encryptor { get; set; }

        public WFFMEncryptionDataProvider(string innerProvider)
            : this(CreateInnerProvider(innerProvider), CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(string connectionString, string innerProvider)
            : this(CreateInnerProvider(innerProvider, connectionString), CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider)
            : this(innerProvider, CreateDefaultEncryptor())
        {
        }

        public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider, IEncryptor encryptor)
        {
            SetInnerProvider(innerProvider);
            SetEncryptor(encryptor);
        }

        private static WFMDataProviderBase CreateInnerProvider(string innerProvider, string connectionString = null)
        {
            Assert.ArgumentNotNullOrEmpty(innerProvider, "innerProvider");
            if (!string.IsNullOrWhiteSpace(connectionString))
            {
                return ReflectionUtil.CreateObject(innerProvider, new[] { connectionString }) as WFMDataProviderBase;
            }

            return ReflectionUtil.CreateObject(innerProvider, new object[0]) as WFMDataProviderBase;
        }
        
        private void SetInnerProvider(WFMDataProviderBase innerProvider)
        {
            Assert.ArgumentNotNull(innerProvider, "innerProvider");
            InnerProvider = innerProvider;
        }

        private static IEncryptor CreateDefaultEncryptor()
        {
            return DataNullTerminatorEncryptor.CreateNewDataNullTerminatorEncryptor(GetEncryptorSettings());
        }

        private static DataNullTerminatorEncryptorSettings GetEncryptorSettings()
        {
            return new DataNullTerminatorEncryptorSettings
            {
                EncryptionDataNullTerminator = Settings.GetSetting("WFFM.Encryption.DataNullTerminator"),
                InnerEncryptor = RC2Encryptor.CreateNewRC2Encryptor(Settings.GetSetting("WFFM.Encryption.Key"))
            };
        }

        private void SetEncryptor(IEncryptor encryptor)
        {
            Assert.ArgumentNotNull(encryptor, "encryptor");
            Encryptor = encryptor;
        }

        public override void ChangeStorage(Guid formItemId, string newStorage)
        {
            InnerProvider.ChangeStorage(formItemId, newStorage);
        }

        public override void ChangeStorageForForms(IEnumerable<Guid> ids, string storageName)
        {
            InnerProvider.ChangeStorageForForms(ids, storageName);
        }

        public override void DeleteForms(IEnumerable<Guid> formSubmitIds)
        {
            InnerProvider.DeleteForms(formSubmitIds);
        }

        public override void DeleteForms(Guid formItemId, string storageName)
        {
            InnerProvider.DeleteForms(formItemId, storageName);
        }

        public override IEnumerable<IPool> GetAbundantPools(Guid fieldId, int top, out int total)
        {
            return InnerProvider.GetAbundantPools(fieldId, top, out total);
        }

        public override IEnumerable<IForm> GetForms(QueryParams queryParams, out int total)
        {
            IEnumerable<IForm> forms = InnerProvider.GetForms(queryParams, out total);
            DecryptForms(forms);
            return forms;
        }

        public override IEnumerable<IForm> GetFormsByIds(IEnumerable<Guid> ids)
        {
            IEnumerable<IForm> forms = InnerProvider.GetFormsByIds(ids);
            DecryptForms(forms);
            return forms;
        }

        public override int GetFormsCount(Guid formItemId, string storageName, string filter)
        {
            return InnerProvider.GetFormsCount(formItemId, storageName, filter);
        }

        public override IEnumerable<IPool> GetPools(Guid fieldId)
        {
            return InnerProvider.GetPools(fieldId);
        }

        public override void InsertForm(IForm form)
        {
            EncryptForm(form);
            InnerProvider.InsertForm(form);
        }

        public override void ResetPool(Guid fieldId)
        {
            InnerProvider.ResetPool(fieldId);
        }

        public override IForm SelectSingleForm(Guid fieldId, string likeValue)
        {
            IForm form = InnerProvider.SelectSingleForm(fieldId, likeValue);
            DecryptForm(form);
            return form;
        }

        public override bool UpdateForm(IForm form)
        {
            EncryptForm(form);
            return InnerProvider.UpdateForm(form);
        }

        private void EncryptForms(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            foreach (IForm form in forms)
            {
                EncryptForm(form);
            }
        }

        private void EncryptForm(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            Assert.ArgumentNotNull(form.Field, "form.Field");
            form.Field = EncryptFields(form.Field);
        }

        private IEnumerable<IField> EncryptFields(IEnumerable<IField> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            IList<IField> encryptedFields = new List<IField>();
            foreach (IField field in fields)
            {
                encryptedFields.Add(EncryptField(field));
            }

            return encryptedFields;
        }

        private IField EncryptField(IField field)
        {
            Assert.ArgumentNotNull(field, "field");
            return CreateNewWFFMField(field, Encrypt(field.FieldName), Encrypt(field.Value));
        }

        private void DecryptForms(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            foreach (IForm form in forms)
            {
                DecryptForm(form);
            }
        }

        private void DecryptForm(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            Assert.ArgumentNotNull(form.Field, "form.Field");
            form.Field = DecryptFields(form.Field);
        }

        private IEnumerable<IField> DecryptFields(IEnumerable<IField> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            IList<IField> decryptedFields = new List<IField>();
            foreach (IField field in fields)
            {
                decryptedFields.Add(DecryptField(field));
            }

            return decryptedFields;
        }

        private IField DecryptField(IField field)
        {
            Assert.ArgumentNotNull(field, "field");
            return CreateNewWFFMField(field, Decrypt(field.FieldName), Decrypt(field.Value));
        }

        private string Encrypt(string input)
        {
            return Encryptor.Encrypt(input);
        }

        private string Decrypt(string input)
        {
            return Encryptor.Decrypt(input);
        }

        private static IField CreateNewWFFMField(IField field, string fieldName, string value)
        {
            if (field != null)
            {
                return new WFFMField
                {
                    Data = field.Data,
                    FieldId = field.FieldId,
                    FieldName = fieldName,
                    Form = field.Form,
                    Id = field.Id,
                    Value = value
                };
            }

            return null;
        }
    }
}

The above class employs the decorator pattern. An inner WFFM Data Provider — which is supplied via a parameter configuration node in \App_Config\Include\forms.config, and is created via magic within the Sitecore.Reflection.ReflectionUtil class — is wrapped.

Methods that save and retrieve form data in the above Data Provider decorate the same methods defined on the inner WFFM Data Provider.

Methods that save form data pass form(s) — and eventually their fields — through a chain of Encrypt methods. The Encrypt method that takes in an IField instance as an argument encrypts the instance’s field name and value, and returns a new instance of the WFFMField class using the encrypted data and the other properties on the IField instance untouched.

Similarly, a chain of Decrypt methods are called for form(s) being retrieved from the inner Data Provider — field names and values are decrypted and saved into a new instance of the WFFMField class, and the manipulated form(s) are returned.

I want to point out that the IEncryptor instance is actually an instance of DataNullTerminatorEncryptor — see my earlier post on data encryption to see how this is implemented — which decorates our RC2Encryptor. This decorating encryptor stamps encrypted strings with a special string so we don’t accidentally encrypt a value twice, and it also won’t try to decrypt a string value that isn’t encrypted.

I added a new include configuration file to hold encryption related settings — the IEncryptor’s key, and the string that will be put at the end of all encrypted data via the DataNullTerminatorEncryptor instance:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <!-- TODO: change the terminator so it does not scream "PLEASE TRY TO CRACK ME!" -->
      <setting name="WFFM.Encryption.DataNullTerminator" value="#I_AM_ENCRYPTED#" />

      <!-- I found this key somewhere on the internet, so it must be secure -->
      <setting name="WFFM.Encryption.Key" value="88bca90e90875a" />
    </settings>
  </sitecore>
</configuration>

I then hooked in the encryption WFFM Data Provider in \App_Config\Include\forms.config, and set the type for the inner provider:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
  
	<!-- There is stuff up here -->
  
	<!-- MS SQL -->
	<formsDataProvider type="Sitecore.Sandbox.WFFM.Forms.Data.DataProviders.WFFMEncryptionDataProvider, Sitecore.Sandbox">
		<!-- No, this is not my real connection string -->
		<param desc="connection string">user id=(user);password=(password);Data Source=(database)</param>
		<param desc="inner provider">Sitecore.Forms.Data.DataProviders.WFMDataProvider, Sitecore.Forms.Core</param>
	</formsDataProvider>

	<!-- There is stuff down here -->
	
	</sitecore>
</configuration>

Let’s see this in action.

I created a new WFFM form with some fields for testing:

the-form-in-sitecore

I then mapped the above form to a new page in Sitecore, and published both the form and page.

I navigated to the form page, and filled it out:

the-form-page

After clicking submit, I was given a ‘thank you’ page’:

the-form-page-confirmation

Let’s see what our field data looks like in the WFFM database:

the-form-submission-encrypted-db

As you can see, the data is encrypted.

Now, let’s see if the data is also encrypted in the Forms Report for our test form:

forms-report-encryption

As you can see, the end-user would be unaware that any data manipulation is happening under the hood.

If you have any thoughts on this, please leave a comment.

Enforce Password Expiration in the Sitecore CMS

I recently worked on a project that called for a feature to expire Sitecore users’ passwords after an elapsed period of time since their passwords were last changed.

The idea behind this is to lessen the probability that an attacker will infiltrate a system — or multiple systems if users reuse their passwords across different applications (this is more common than you think) — within an organization by acquiring old database backups containing users’ passwords.

Since I can’t show you what I built for that project, I cooked up another solution — a custom loggingin processor that determines whether a user’s password has expired in Sitecore:

using System;
using System.Web.Security;

using Sitecore.Diagnostics;
using Sitecore.Pipelines.LoggingIn;
using Sitecore.Web;

namespace Sitecore.Sandbox.Pipelines.LoggingIn
{
    public class CheckPasswordExpiration
    {
        private TimeSpan TimeSpanToExpirePassword { get; set; }
        private string ChangePasswordPageUrl { get; set; }

        public void Process(LoggingInArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!IsEnabled())
            {
                return;
            }

            MembershipUser user = GetMembershipUser(args);
            if (HasPasswordExpired(user))
            {
                WebUtil.Redirect(ChangePasswordPageUrl);
            }
        }

        private bool IsEnabled()
        {
            return IsTimeSpanToExpirePasswordSet() && IsChangePasswordPageUrlSet();
        }

        private bool IsTimeSpanToExpirePasswordSet()
        {
            return TimeSpanToExpirePassword > default(TimeSpan);
        }

        private bool IsChangePasswordPageUrlSet()
        {
            return !string.IsNullOrWhiteSpace(ChangePasswordPageUrl);
        }

        private static MembershipUser GetMembershipUser(LoggingInArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.Username, "args.Username");
            return Membership.GetUser(args.Username, false);
        }

        private bool HasPasswordExpired(MembershipUser user)
        {
            return user.LastPasswordChangedDate.Add(TimeSpanToExpirePassword) <= DateTime.Now;
        }
    }
}

The processor above ascertains whether a user’s password has expired by adding a configured timespan — see the configuration file below — to the last date and time the password was changed, and if that date and time summation is in the past — this means the password should have been changed already — then we redirect the user to a change password page (this is configured to be the Change Password page in Sitecore).

I wired up the custom loggingin processor, its timespan on expiring passwords — here I am using 1 minute since I can’t wait around all day 😉 — and set the change password page to be the url of Sitecore’s Change Password page:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <loggingin>
        <processor mode="on" type="Sitecore.Sandbox.Pipelines.LoggingIn.CheckPasswordExpiration, Sitecore.Sandbox"
					patch:before="processor[@type='Sitecore.Pipelines.LoggingIn.CheckStartPage, Sitecore.Kernel']">
          <!-- Number of days, hours, minutes and seconds after the last password change date to expire passwords -->
          <TimeSpanToExpirePassword>00:00:01:00</TimeSpanToExpirePassword>
          <ChangePasswordPageUrl>/sitecore/login/changepassword.aspx</ChangePasswordPageUrl>
        </processor>  
      </loggingin>
    </processors>
  </sitecore>
</configuration>

Let’s test this out.

I went to Sitecore’s login page, and entered my username and password on the login form:

login-page

I clicked the Login button, and was redirected to the Change Password page as expected:

change-password-page

If you can think of any other security measures that should be added to Sitecore, please share in a comment.

Prevent Sitecore Users from Using Common Dictionary Words in Passwords

Lately, enhancing security measures in Sitecore have been on my mind. One idea that came to mind today was finding a way to prevent password cracking — a scenario where a script tries to ascertain a user’s password by guessing over and over again what a user’s password might be, by supplying common words from a data store, or dictionary in the password textbox on the Sitecore login page.

As a way to prevent users from using common words in their Sitecore passwords, I decided to build a custom System.Web.Security.MembershipProvider — the interface (not a .NET interface but an abstract class) used by Sitecore out of the box for user management. Before we dive into that code, we need a dictionary of some sort.

I decided to use a list of fruits — all for the purposes of keeping this post simple — that I found on a Wikipedia page. People aren’t kidding when they say you can virtually find everything on Wikipedia, and no doubt all content on Wikipedia is authoritative — just ask any university professor. 😉

I copied some of the fruits on that Wikipedia page into a patch include config file — albeit it would make more sense to put these into a database, or perhaps into Sitecore if doing such a thing in a real-world solution. I am not doing this here for the sake of brevity.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <dictionaryWords>
      <word>Apple</word>
      <word>Apricot</word>
      <word>Avocado</word>
      <word>Banana</word>
      <word>Breadfruit</word>
      <word>Bilberry</word>
      <word>Blackberry</word>
      <word>Blackcurrant</word>
      <word>Blueberry</word>
      <word>Currant</word>
      <word>Cherry</word>
      <word>Cherimoya</word>
      <word>Clementine</word>
      <word>Cloudberry</word>
      <word>Coconut</word>
      <word>Date</word>
      <word>Damson</word>
      <word>Dragonfruit</word>
      <word>Durian</word>
      <word>Eggplant</word>
      <word>Elderberry</word>
      <word>Feijoa</word>
      <word>Fig</word>
      <word>Gooseberry</word>
      <word>Grape</word>
      <word>Grapefruit</word>
      <word>Guava</word>
      <word>Huckleberry</word>
      <word>Honeydew</word>
      <word>Jackfruit</word>
      <word>Jettamelon</word>
      <word>Jambul</word>
      <word>Jujube</word>
      <word>Kiwi fruit</word>
      <word>Kumquat</word>
      <word>Legume</word>
      <word>Lemon</word>
      <word>Lime</word>
      <word>Loquat</word>
      <word>Lychee</word>
      <word>Mandarine</word>
      <word>Mango</word>
      <word>Melon</word>
    </dictionaryWords>
  </sitecore>
</configuration>

I decided to reuse a utility class I built for my post on expanding Standard Values tokens.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.StringUtilities.Base;
using Sitecore.Sandbox.Utilities.StringUtilities.DTO;

namespace Sitecore.Sandbox.Utilities.StringUtilities
{
    public class StringSubstringsChecker : SubstringsChecker<string>
    {
        private bool IgnoreCase { get; set; }

        private StringSubstringsChecker(IEnumerable<string> substrings)
            : this(null, substrings, false)
        {
        }

        private StringSubstringsChecker(IEnumerable<string> substrings, bool ignoreCase)
            : this(null, substrings, ignoreCase)
        {
        }

        private StringSubstringsChecker(string source, IEnumerable<string> substrings, bool ignoreCase)
            : base(source)
        {
            SetSubstrings(substrings);
            SetIgnoreCase(ignoreCase);
        }

        private void SetSubstrings(IEnumerable<string> substrings)
        {
            AssertSubstrings(substrings);
            Substrings = substrings;
        }

        private static void AssertSubstrings(IEnumerable<string> substrings)
        {
            Assert.ArgumentNotNull(substrings, "substrings");
            Assert.ArgumentCondition(substrings.Any(), "substrings", "substrings must contain as at least one string!");
        }

        private void SetIgnoreCase(bool ignoreCase)
        {
            IgnoreCase = ignoreCase;
        }

        protected override bool CanDoCheck()
        {
            return !string.IsNullOrEmpty(Source);
        }

        protected override bool DoCheck()
        {
            Assert.ArgumentNotNullOrEmpty(Source, "Source");

            foreach (string substring in Substrings)
            {
                if(DoesSourceContainSubstring(substring))
                {
                    return true;
                }
            }

            return false;
        }

        private bool DoesSourceContainSubstring(string substring)
        {
            if (IgnoreCase)
            {
                return !IsNotFoundIndex(Source.IndexOf(substring, StringComparison.CurrentCultureIgnoreCase));
            }

            return !IsNotFoundIndex(Source.IndexOf(substring));
        }

        private static bool IsNotFoundIndex(int index)
        {
            const int notFound = -1;
            return index == notFound;
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(IEnumerable<string> substrings)
        {
            return new StringSubstringsChecker(substrings);
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(IEnumerable<string> substrings, bool ignoreCase)
        {
            return new StringSubstringsChecker(substrings, ignoreCase);
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(string source, IEnumerable<string> substrings, bool ignoreCase)
        {
            return new StringSubstringsChecker(source, substrings, ignoreCase);
        }
    }
}

In the version of our “checker” class above, I added the option to have the “checker” ignore case comparisons for the source and substrings.

Next, I created a new System.Web.Security.MembershipProvider subclass where I am utilizing the decorator pattern to decorate methods around changing passwords and creating users.

By default, we are instantiating an instance of the System.Web.Security.SqlMembershipProvider — this is what my local Sitecore sandbox instance is using, in order to get at the ASP.NET Membership tables in the core database.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Web.Security;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Security;

using Sitecore.Sandbox.Utilities.StringUtilities;
using Sitecore.Sandbox.Utilities.StringUtilities.Base;

namespace Sitecore.Sandbox.Security.MembershipProviders
{
    public class PreventDictionaryWordPasswordsMembershipProvider : MembershipProvider 
    {
        private static IEnumerable<string> _DictionaryWords;
        private static IEnumerable<string> DictionaryWords
        {
            get
            {
                if (_DictionaryWords == null)
                {
                    _DictionaryWords = GetDictionaryWords();
                }

                return _DictionaryWords;
            }
        }

        public override string Name
        {
            get
            {
                return InnerMembershipProvider.Name;
            }
        }

        public override string ApplicationName
        {
            get
            {
                return InnerMembershipProvider.ApplicationName;
            }
            set
            {
                InnerMembershipProvider.ApplicationName = value;
            }
        }

        public override bool EnablePasswordReset
        {
            get
            {
                return InnerMembershipProvider.EnablePasswordReset;
            }
        }

        public override bool EnablePasswordRetrieval
        {
            get
            {
                return InnerMembershipProvider.EnablePasswordRetrieval;
            }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get
            {
                return InnerMembershipProvider.MaxInvalidPasswordAttempts;
            }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get
            {
                return InnerMembershipProvider.MinRequiredNonAlphanumericCharacters;
            }
        }

        public override int MinRequiredPasswordLength
        {
            get
            {
                return InnerMembershipProvider.MinRequiredPasswordLength;
            }
        }

        public override int PasswordAttemptWindow
        {
            get
            {
                return InnerMembershipProvider.PasswordAttemptWindow;
            }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get
            {
                return InnerMembershipProvider.PasswordFormat;
            }
        }

        public override string PasswordStrengthRegularExpression
        {
            get
            {
                return InnerMembershipProvider.PasswordStrengthRegularExpression;
            }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get
            {
                return InnerMembershipProvider.RequiresQuestionAndAnswer;
            }
        }

        public override bool RequiresUniqueEmail
        {
            get
            {
                return InnerMembershipProvider.RequiresUniqueEmail;
            }
        }

        private MembershipProvider InnerMembershipProvider { get; set; }
        private ISubstringsChecker<string> DictionaryWordsSubstringsChecker { get; set; }

        public PreventDictionaryWordPasswordsMembershipProvider()
            : this(CreateNewSqlMembershipProvider(), CreateNewDictionaryWordsSubstringsChecker())
        {
        }
        
        public PreventDictionaryWordPasswordsMembershipProvider(MembershipProvider innerMembershipProvider, ISubstringsChecker<string> dictionaryWordsSubstringsChecker)
        {
            SetInnerMembershipProvider(innerMembershipProvider);
            SetDictionaryWordsSubstringsChecker(dictionaryWordsSubstringsChecker);
        }

        private void SetInnerMembershipProvider(MembershipProvider innerMembershipProvider)
        {
            Assert.ArgumentNotNull(innerMembershipProvider, "innerMembershipProvider");
            InnerMembershipProvider = innerMembershipProvider;
        }

        private void SetDictionaryWordsSubstringsChecker(ISubstringsChecker<string> dictionaryWordsSubstringsChecker)
        {
            Assert.ArgumentNotNull(dictionaryWordsSubstringsChecker, "dictionaryWordsSubstringsChecker");
            DictionaryWordsSubstringsChecker = dictionaryWordsSubstringsChecker;
        }

        private static MembershipProvider CreateNewSqlMembershipProvider()
        {
            return new SqlMembershipProvider();
        }

        private static ISubstringsChecker<string> CreateNewDictionaryWordsSubstringsChecker()
        {
            return CreateNewStringSubstringsChecker(DictionaryWords);
        }

        private static ISubstringsChecker<string> CreateNewStringSubstringsChecker(IEnumerable<string> substrings)
        {
            Assert.ArgumentNotNull(substrings, "substrings");
            const bool ignoreCase = true;
            return StringSubstringsChecker.CreateNewStringSubstringsContainer(substrings, ignoreCase);
        }

        private static IEnumerable<string> GetDictionaryWords()
        {
            return Factory.GetStringSet("dictionaryWords/word");
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            if (DoesPasswordContainDictionaryWord(newPassword))
            {
                return false;
            }

            return InnerMembershipProvider.ChangePassword(username, oldPassword, newPassword);
        }

        private bool DoesPasswordContainDictionaryWord(string password)
        {
            Assert.ArgumentNotNullOrEmpty(password, "password");
            DictionaryWordsSubstringsChecker.Source = password;
            return DictionaryWordsSubstringsChecker.ContainsSubstrings();
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            return InnerMembershipProvider.ChangePasswordQuestionAndAnswer(username, password, newPasswordQuestion, newPasswordAnswer);
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            if (DoesPasswordContainDictionaryWord(password))
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            return InnerMembershipProvider.CreateUser(username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status);
        }

        public override bool DeleteUser(string userName, bool deleteAllRelatedData)
        {
            return InnerMembershipProvider.DeleteUser(userName, deleteAllRelatedData);
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            return InnerMembershipProvider.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection FindUsersByName(string userNameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            return InnerMembershipProvider.FindUsersByName(userNameToMatch, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            return InnerMembershipProvider.GetAllUsers(pageIndex, pageSize, out totalRecords);
        }

        public override int GetNumberOfUsersOnline()
        {
            return InnerMembershipProvider.GetNumberOfUsersOnline();
        }

        public override string GetPassword(string username, string answer)
        {
            return InnerMembershipProvider.GetPassword(username, answer);
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            return InnerMembershipProvider.GetUser(providerUserKey, userIsOnline);
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            return InnerMembershipProvider.GetUser(username, userIsOnline);
        }

        public override string GetUserNameByEmail(string email)
        {
            return InnerMembershipProvider.GetUserNameByEmail(email);
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            InnerMembershipProvider.Initialize(name, config);
        }

        public override string ResetPassword(string username, string answer)
        {
            return InnerMembershipProvider.ResetPassword(username, answer);
        }

        public override bool UnlockUser(string userName)
        {
            return InnerMembershipProvider.UnlockUser(userName);
        }

        public override void UpdateUser(MembershipUser user)
        {
            InnerMembershipProvider.UpdateUser(user);
        }

        public override bool ValidateUser(string username, string password)
        {
            return InnerMembershipProvider.ValidateUser(username, password);
        }
    }
}

In our MembershipProvider above, we have decorated the ChangePassword() and CreateUser() methods by employing a helper method to delegate to our DictionaryWordsSubstringsChecker instance — an instance of the StringSubstringsChecker class above — to see if the supplied password contains any of the fruits found in our collection, and prevent the workflow from moving forward in changing a user’s password, or creating a new user if one of the fruits is found in the provided password.

If we don’t find one of the fruits in the password, we then delegate to the inner MembershipProvider instance — this instance takes care of the rest around changing passwords, or creating new users.

I then had to register my MemberProvider in my Web.config — this cannot be placed in a patch include file since it lives outside of the <sitecore></sitecore> element.

<configuration>
	<membership defaultProvider="sitecore" hashAlgorithmType="SHA1">
		<providers>
			<clear/>
			<add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="sql" providerWildcard="%" raiseEvents="true"/>
			
			<!-- our new provider -->
			<add name="sql" type="Sitecore.Sandbox.Security.MembershipProviders.PreventDictionaryWordPasswordsMembershipProvider, Sitecore.Sandbox" connectionStringName="core" applicationName="sitecore" minRequiredPasswordLength="6" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="256"/>
			
			<add name="switcher" type="Sitecore.Security.SwitchingMembershipProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/membership"/>
		</providers>
    </membership>
</configuration>

So, let’s see what the code above does.

I first tried to create a new user with a password containing a fruit.

new-user-lime

I then used a fruit that was not in the collection of fruits above, and successfully created my new user.

Next, I tried to change an existing user’s password to one that contains the word “apple”.

change-password-apples

I successfully changed this same user’s password using the word orange — it does not live in our collection of fruits above.

change-password-oranges

And that’s all there is to it. If you can think of alternative ways of doing this, or additional security features to implement, please drop a comment.

Until next time, have a Sitecoretastic day!