Skip to main content

An ASP.NET HttpModule that adds additional protection for the session cookie against session hijacking.

using System;
using System.Globalization;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace MyNameSpace
{
    /// <summary>
    /// <see cref="System.Web.IHttpModule"/> that adds additional protection
    /// for the session cookie against session hijacking.
    /// </summary>
    /// <example>
    /// Enable the module by adding it to your web.config:
    /// <code>
    ///     <configuration>
    ///         <system.webServer>
    ///             <modules>
    ///                 <add name="SecureSessionModule" type="SecureSessionModule" />
    ///             </modules>
    ///         </system.webServer>
    ///     </configuration>
    /// </code>
    /// </example>
    /// <remarks>
    /// NOTE: This approach does NOT work with cookieless sessions.
    /// Code is based on "SecureSessionModule.cs" in "Wrox Real World .NET, C#, and Silverlight: Indispensable Experiences from 15 MVPs".
    /// </remarks>
    public class SecureSessionModule : IHttpModule
    {
        /// <summary>
        /// Initializes a module and prepares it to handle requests.
        /// </summary>
        /// <param name="context">An HttpApplication that provides access to the
        /// methods, properties, and events common to all application objects
        /// within an ASP.NET application.</param>
        public void Init(HttpApplication context)
        {
            context.BeginRequest += OnBeginRequest;
            context.EndRequest += OnEndRequest;
        }

        /// <summary>
        /// Disposes of the resources (other than memory) used by the module.
        /// </summary>
        public void Dispose()
        {
        }

        /// <summary>
        /// Event handler that is called by the ASP.NET runtime at
        /// the start of every request.
        /// </summary>
        /// <param name="sender">The <see cref="HttpApplication" /> that is
        /// the source of the event.</param>
        /// <param name="e">An <see cref="EventArgs" /> that contains no
        /// information.</param>
        public void OnBeginRequest(object sender, EventArgs e)
        {
            // Get the incoming HttpRequest.
            HttpRequest request = HttpContext.Current.Request;

            // Get the session cookie from the incoming request.
            HttpCookie cookie = GetSessionCookie(request.Cookies);

            if (cookie != null)
            {
                // Get the cookie value.
                string value = cookie.Value;

                if (!String.IsNullOrEmpty(value))
                {
                    // Get the session ID and the MAC from the cookie.
                    string sessionId = value.Substring(0, 24);
                    string clientMac = value.Substring(24);

                    // Generate a MAC on the server.
                    string serverMac = GenerateMac(request);

                    // Compare the client and the server MACs.
                    if (!clientMac.Equals(serverMac, StringComparison.OrdinalIgnoreCase))
                    {
                        // Terminate the request but do not provide too much details
                        // for the attacker.
                        throw new SecurityException("Session mismatch.");
                    }

                    // Remove the MAC from the cookie before ASP.NET uses it.
                    cookie.Value = sessionId;
                }
            }
        }

        /// <summary>
        /// Event handler that is called by the ASP.NET runtime at the end of every request.
        /// </summary>
        /// <param name="sender">The <see cref="HttpApplication"/> that is the source of the event.</param>
        /// <param name="e">An <see cref="EventArgs"/> that contains no information.</param>
        public void OnEndRequest(object sender, EventArgs e)
        {
            // Get the session cookie value from the outgoing response.
            HttpCookie cookie = GetSessionCookie(HttpContext.Current.Response.Cookies);

            // Add the MAC if a session cookie is present in the response.
            if (cookie != null)
            {
                // Generate the new MAC.
                string mac = GenerateMac(HttpContext.Current.Request);

                // Add the MAC to the response cookie.
                cookie.Value += mac;
            }
        }

        /// <summary>
        /// Helper method that returns the session cookie from the given cookie
        /// collection.
        /// </summary>
        /// <param name="cookies">The cookie collection to process.</param>
        /// <returns>The ASP.NET session cookie or <c>null</c> if it is not present
        /// in the cookie collection.</returns>
        /// <remarks>
        /// NOTE: Use this method to get the cookie because the string indexer has
        /// the side effect of adding an empty cookie.
        /// </remarks>
        private static HttpCookie GetSessionCookie(HttpCookieCollection cookies)
        {
            // NOTE: Use "for" because "foreach" does not work
            // ("Unable to cast object of type 'System.String' to type 'System.Web.HttpCookie'.")
            for (int i = 0; i < cookies.Count; i++)
            {
                HttpCookie cookie = cookies[i];
                if (cookie != null &&
                    cookie.Name.Equals("ASP.NET_SessionId", StringComparison.OrdinalIgnoreCase))
                {
                    return cookie;
                }
            }

            return null;
        }

        /// <summary>
        /// Helper method that generates the Hash-based Message Authentication Code
        /// for the session cookie.
        /// </summary>
        /// <param name="request">The current <see cref="HttpRequest"/>.</param>
        /// <returns>The Base64-encoded HMAC for the current session and the current client.</returns>
        private static string GenerateMac(HttpRequest request)
        {
            // Build the additional content to store in the cookie. NOTE: On some
            // networks (AOL, mobile) the IP address may change frequently, and
            // behind a proxy multiple clients can have the same IP address.
            string content = String.Format(CultureInfo.InvariantCulture, "{0}|{1}",
                request.UserHostAddress,
                request.UserAgent);

            // The server side secret key for the HMAC.
            // NOTE: This secret should be stored encrypted in the web.config.
            byte[] key = Encoding.UTF8.GetBytes("Any server side secret...");

            // Build the HMAC.
            using (var hmac = new HMACSHA512(key))
            {
                byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(content));

                return Convert.ToBase64String(hash);
            }
        }
    }
}