27 October 2016

Easily prevent cookie replay attacks in Sitecore

Imagine you have a site which is (partially) protected and you use forms authentication to let your users gain access. Then you might be vulnerable to a cookie replay attack. Apparently this is a weakness that has been around in the .NET framework for ages. That article explains very well what a cookie replay attack is. There is also an old Microsoft Knowledge Base article on that subject.

To protect your site against such a cookie replay attack, you must implement the following security measures:
  • When signing in, set a boolean session variable to true.
  • When signing out, set the session variable to false.
  • Check the session variable on each request. If the value is false (or non-existing), return not authenticated.
Now to make your life and those of your team easier, you can create a custom forms authentication provider for Sitecore which implement the security measures mentioned above. With this custom provider, you nor any other developer on the team must think about this weakness.  As long as the custom provider is in place, that is.

Developers can keep doing what they did before:
  • On login, call: AuthenticationManager.Login(username, password);
  • On logout, call: AuthenticationManager.Logout();

Time to bring the custom provider!

using System;
using System.Linq;
using System.Web;
using Sitecore;
using Sitecore.Security.Accounts;
using Sitecore.Security.Domains;

namespace Custom.Security.Authentication
{
    public class CustomFormsAuthenticationProvider : Sitecore.Security.Authentication.FormsAuthenticationProvider
    {
        private const string LoggedInSessionKey = "LoggedIn";
        private static readonly string[] SitesToSkip = new[] { "shell", "login", "admin" };

        public override bool Login(User user)
        {
            if (!base.Login(user))
            {
                return false;
            }

            if (IsValidSite())
            {
                HttpContext.Current.Session[LoggedInSessionKey] = true;
            }

            return true;
        }

        public override bool Login(string userName, bool persistent)
        {
            if (!base.Login(userName, persistent))
            {
                return false;
            }

            if (IsValidSite())
            {
                HttpContext.Current.Session[LoggedInSessionKey] = true;
            }

            return true;
        }

        public override bool Login(string userName, string password, bool persistent)
        {
            if (!base.Login(userName, password, persistent))
            {
                return false;
            }

            if (IsValidSite())
            {
                HttpContext.Current.Session[LoggedInSessionKey] = true;
            }

            return true;
        }

        public override void Logout()
        {
            if (HttpContext.Current.Session != null && HttpContext.Current.Session[LoggedInSessionKey] != null)
            {
                HttpContext.Current.Session.Remove(LoggedInSessionKey);
            }

            base.Logout();
        }

        public override User GetActiveUser()
        {
            if (!IsValidSite() || IsMarkedAsLoggedInOrNoSession())
            {
                return base.GetActiveUser();
            }

            return Context.Domain.GetAnonymousUser() ?? Domain.GetDefaultAnonymousUser();
        }
        
        private static bool IsValidSite()
        {
            return !SitesToSkip.Contains(Sitecore.Context.GetSiteName(), StringComparer.OrdinalIgnoreCase);
        }

        public static bool IsMarkedAsLoggedInOrNoSession()
        {
            return HttpContext.Current == null
                || HttpContext.Current.Session == null
                || HttpContext.Current.Session[LoggedInSessionKey] != null;
        }
    }
}

Plain and simple. With one caveat. Sitecore calls GetActiveUser() several times when signing in to the Sitecore backend. It does not call any of the login methods. On recommendation of Sitecore Support, we just call base.GetActiveUser() if we are on any of the backend sites.

The only thing to do, is to let Sitecore know about your custom authentication provider. Adjust the web.config as follows:

<authentication defaultProvider="forms">
  <providers>
    <clear />
    <add name="forms" type="Custom.Security.Authentication.CustomFormsAuthenticationProvider, Custom" />
  </providers>
</authentication>


No comments :

Post a Comment