Preventing Dictionary Attacks on the Login Form

If your website has a login form, it's your responsibility to prevent hackers from gaining access to protected content on your site. One of the most basic methods hackers use to figure out passwords is called a 'dictionary attack'. A dictionary attack is a method of breaking into a password-protected computer or server by systematically trying every word in a list as a password. If your site is not protected, a hacker can try thousands, or millions, of passwords until they find the right one that allows them entry into your site.

There are a few ways to make it harder to pull off a dictionary attack. The best way is to enforce a password complexity policy - adding multiple words or phrases, numbers, casing and special characters to a password makes it exponentially more difficult to crack with a dictionary.

Another popular method is to limit the number of login attempts. A dictionary attack is far less likely to succeed if you can only test, say, 20 passwords in an hour. This is the method I will outline here. My code example will work on any ASP.NET website, including Sitecore.

The first step is to create a class containing your hack prevention methods, like so.

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

namespace Integryx.Feature.Authentication.Web.Business
{
    public static class DictionaryAttackPrevention
    {
        const int NUM_ATTEMPTS = 5;
        const int LOCKOUT_MINUTES = 15;
        const string USER_LOCKOUT_LIST_KEY = "USER_LOCKOUT_LIST";

        /// <summary>
        /// This method should be called after any failed login attempt.
        /// If the user has tried to login unsuccessfully NUM_ATTEMPTS or more times, then lock them out for LOCKOUT_MINUTES.
        /// </summary>
        /// <param name="userName"></param>
        /// <returns>bool</returns>
        public static bool IsLockedOut(string userName)
        {
            var isLockedOut = false;

            if (HttpContext.Current.Application[USER_LOCKOUT_LIST_KEY] == null)
            {
                HttpContext.Current.Application[USER_LOCKOUT_LIST_KEY] = new List<LoginAttempts>();
            }

            var userLockoutList = HttpContext.Current.Application[USER_LOCKOUT_LIST_KEY] as List<LoginAttempts>;

            var loginAttempt = userLockoutList.Where(i => i.UserName == userName.ToLower());

            if (loginAttempt.Count() > 0)
            {
                // Reset count if LOCKOUT_MINUTES has elapsed.
                if (loginAttempt.First().LastAttempt.Add(TimeSpan.FromMinutes(LOCKOUT_MINUTES)) <= DateTime.Now)
                {
                    loginAttempt.First().Attempts = 0;
                }

                // Check for policy violation.
                if (loginAttempt.First().Attempts >= NUM_ATTEMPTS)
                {
                    isLockedOut = true;
                }
                else
                {
                    // Update existing login attempt record.
                    loginAttempt.First().Attempts++;
                    loginAttempt.First().LastAttempt = DateTime.Now;
                }
            }
            else
            {
                // Create a new login attempt record.
                var newLoginAttempt = new LoginAttempts();
                newLoginAttempt.UserName = userName.ToLower();
                newLoginAttempt.Attempts = 1;
                newLoginAttempt.LastAttempt = DateTime.Now;
                userLockoutList.Add(newLoginAttempt);
            }

            HttpContext.Current.Application[USER_LOCKOUT_LIST_KEY] = userLockoutList;

            return isLockedOut;
        }

        public static void ResetLockOutCounter(string userName)
        {
            if (HttpContext.Current.Application[USER_LOCKOUT_LIST_KEY] == null)
            {
                return;
            }

            var userLockoutList = HttpContext.Current.Application[USER_LOCKOUT_LIST_KEY] as List<LoginAttempts>;

            var loginAttempt = userLockoutList.Where(i => i.UserName == userName.ToLower());

            if (loginAttempt != null && loginAttempt.Count() > 0)
            {
                loginAttempt.First().Attempts = 0;
            }

            HttpContext.Current.Application[USER_LOCKOUT_LIST_KEY] = userLockoutList;
        }

        [Serializable]
        public class LoginAttempts
        {
            public string UserName;
            public int Attempts;
            public DateTime LastAttempt;
        }
    }
}

In this case, the login policy is configured to allow no more than 5 unsuccessful login attempts in a 15 minute period before locking them out of the website for 15 minutes. The IsLockedOut method should be invoked prior to any login attempt.

HackPrevention.IsLockedOut(userName)

This method keeps track of how many times a particular user has tried to login unsuccessfully, and returns a boolean value indicating whether the user has violated the login policy.

When a user logs in successfully, you should reset the attempt count using the ResetLockoutCounter method, like so.

HackPrevention.ResetLockOutCounter(userName);

Any website with a login form should be protected against hackers - and the dictionary attack is one of the easiest to prevent with just a little bit of development. Between enforcing complex passwords, and implementing methods like the one I demonstrated here, you have made great strides in the right direction!

As always, there are many ways to accomplish this, and I'm sure there are ways to improve my code. I'd love to hear your ideas.

~David

Add comment

Loading