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);
}
}
}
}