Home » Customization » Prevent Sitecore Users from Using Common Dictionary Words in Passwords

Prevent Sitecore Users from Using Common Dictionary Words in Passwords

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

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!

Advertisement

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: