New information has been added to this article since publication.
Refer to the Editor's Update below.

Wicked Code

Foiling Session Hijacking Attempts

Jeff Prosise

Code download available at:WickedCode0408.exe(118 KB)

Contents

Introducing SecureSessionModule
Deploying SecureSessionModule
Caveats
Conclusion

Let's face it: every minute of every day, someone, somewhere, is patrolling the Web looking for sites to hack. ASP.NET developers must constantly be on their guard to ensure attempted hacks can't be successful. That means constraining and validating user input, accessing databases securely, storing sensitive data securely, and generally writing secure code that repels rather than accommodates these malevolent hackers.

A classic form of hack attack that ASP.NET sites must defend against is session hijacking. Simply put, session hijacking entails connecting to a Web site and accessing someone else's session state. The severity of the damage incurred depends on what's stored in session state. If sessions hold shopping cart information and users are required to verify their identities before checking out, session hijacking might not be very damaging. If sessions contain credit card numbers or similarly sensitive data that can be presented back to the user, you really have to watch out.

Session hijacking attacks are typically perpetrated in one of two ways: session ID guessing and stolen session ID cookies. Session ID guessing involves gathering a sample of session IDs and "guessing" a valid session ID assigned to someone else. ASP.NET apps tend to be much less susceptible to this form of session hijacking because ASP.NET uses highly random 120-bit numbers for its session IDs. Unless you replace ASP.NET session IDs with IDs of your own (last summer I met one developer who had done just that and used sequential session IDs, which are an open door to hackers), you have nothing to fear from session ID guessing.

Stolen session cookies are another matter. Secure Sockets Layer (SSL) can be used to protect session ID cookies on the wire, but few sites restrict session ID cookies to encrypted connections. Even if you use SSL, session ID cookies can be stolen in other ways, notably through cross-site scripting attacks, man-in-the-middle attacks, and gaining physical access to the cookie stores on victims' PCs. Furthermore, executing a successful session hijacking attack with a stolen session ID cookie requires little skill on the part of the attacker. The reason is simple: ASP.NET encodes no information in a session ID cookie other than the session ID. If it receives a cookie containing a valid session ID, ASP.NET connects to the corresponding session, no questions asked.

It's virtually impossible to build a foolproof defense against attacks that rely on stolen session ID cookies, but you can take steps to make it harder. Some Web sites, for example, encode heuristic information such as IP addresses in their session IDs. That doesn't prevent session hijacking altogether—after all, some hackers possess the means to spoof IP addresses, and IP addresses are not a reliable means for identifying callers anyway—but it does raise the bar. Security is all about raising the bar. The harder you make it for a hacker to execute a successful attack, the less likely it is that successful attacks will occur.

Introducing SecureSessionModule

[Editor's Update - 7/20/2004: Figure 1 has been updated so as not to provide too much information to an attacker.]

Figure 1 SecureSessionModule.cs

using System; using System.Web; using System.Web.Security; using System.Configuration; using System.Security.Cryptography; using System.Runtime.Serialization; using System.Globalization; using System.Text; public class SecureSessionModule : IHttpModule { private static string _ValidationKey = null; public void Init (HttpApplication app) { // Initialize validation key if not already initialized if (_ValidationKey == null) _ValidationKey = GetValidationKey (); // Register handlers for BeginRequest and EndRequest events app.BeginRequest += new EventHandler (OnBeginRequest); app.EndRequest += new EventHandler (OnEndRequest); } public void Dispose () {} void OnBeginRequest (Object sender, EventArgs e) { // Look for an incoming cookie named "ASP.NET_SessionID" HttpRequest request = ((HttpApplication) sender).Request; HttpCookie cookie = GetCookie (request, "ASP.NET_SessionId"); if (cookie != null) { // Throw an exception if the cookie lacks a MAC if (cookie.Value.Length <= 24) throw new InvalidSessionException ("Access Denied"); // don't tell bad guys too much // Separate the session ID and the MAC string id = cookie.Value.Substring (0, 24); string mac1 = cookie.Value.Substring (24); // Generate a new MAC from the session ID and requestor info string mac2 = GetSessionIDMac (id, request.UserHostAddress, request.UserAgent, _ValidationKey); // Throw an exception if the MACs don't match if (String.CompareOrdinal (mac1, mac2) != 0) throw new InvalidSessionException ("Access Denied"); // don't tell bad guys too much // Strip the MAC from the cookie before ASP.NET sees it cookie.Value = id; } } void OnEndRequest (Object sender, EventArgs e) { // Look for an outgoing cookie named "ASP.NET_SessionID" HttpRequest request = ((HttpApplication) sender).Request; HttpCookie cookie = GetCookie ( ((HttpApplication) sender).Response, "ASP.NET_SessionId"); if (cookie != null) { // Add a MAC cookie.Value += GetSessionIDMac (cookie.Value, request.UserHostAddress, request.UserAgent, _ValidationKey); } } private string GetValidationKey () { string key = ConfigurationSettings.AppSettings ["SessionValidationKey"]; if (key == null || key == String.Empty) throw new InvalidSessionException ("SessionValidationKey missing"); return key; } private HttpCookie GetCookie (HttpRequest request, string name) { HttpCookieCollection cookies = request.Cookies; return FindCookie (cookies, name); } private HttpCookie GetCookie (HttpResponse response, string name) { HttpCookieCollection cookies = response.Cookies; return FindCookie (cookies, name); } private HttpCookie FindCookie (HttpCookieCollection cookies, string name) { int count = cookies.Count; for (int i=0; i<count; i++) { if (String.Compare (cookies[i].Name, name, true, CultureInfo.InvariantCulture) == 0) return cookies[i]; } return null; } private string GetSessionIDMac (string id, string ip, string agent, string key) { StringBuilder builder = new StringBuilder (id, 512); builder.Append (ip.Substring (0, ip.IndexOf ('.', ip.IndexOf ('.') + 1))); builder.Append (agent); using (HMACSHA1 hmac = new HMACSHA1 (Encoding.UTF8.GetBytes (key))) { return Convert.ToBase64String (hmac.ComputeHash (Encoding.UTF8.GetBytes (builder.ToString ()))); } } } [Serializable] public class InvalidSessionException : Exception { public InvalidSessionException () : base ("Session cookie is invalid") {} public InvalidSessionException (string message) : base (message) {} public InvalidSessionException (string message, Exception inner) : base (message, inner) {} protected InvalidSessionException (SerializationInfo info, StreamingContext context) : base (info, context) {} }

Figure 2 shows how SecureSessionModule works. First, it checks every outgoing response for a session ID cookie issued by ASP.NET's SessionStateModule. When it sees such a cookie, SecureSessionModule modifies it by appending a hashed message authentication code (MAC) to the session ID. The MAC is generated from the session ID, the network address portion of the requestor's IP address (for example, the 192.16 in 192.16.0.14), the User-Agent header received in the request, and a secret key stored on the server. The Framework's System.Security.Cryptography.HMACSHA1 class makes the task of generating the MAC really quite easy.Why use the network address instead of the full IP address? The node address of users that access the Internet through public proxy servers such as AOL's can change in every request, but the network address should not.

Figure 2 SecureSessionModule

Figure 2** SecureSessionModule **

Second, SecureSessionModule examines every incoming request for an ASP.NET session ID cookie. Before allowing a request containing a session ID cookie to continue through the pipeline, SecureSessionModule validates the cookie by regenerating the MAC from the requestor's IP address, the User-Agent header, and the secret key. If the freshly computed MAC matches the one in the cookie, the MAC is stripped from the cookie and the request is allowed to proceed. If the MACs don't match, SecureSessionModule throws an InvalidSessionException, as shown in Figure 3.

Figure 3 Error Showing Generated Key

The net result is that once a session ID cookie is issued, it's only considered valid if it's submitted from the same network address and with the same User-Agent header. An attacker who steals a session ID cookie can only use it if she can spoof IP addresses and HTTP headers. Both are certainly possible, but spoofing of this sort requires a higher skill level on the part of the attacker. In addition, getting a response back from a request with a spoofed IP address is much harder than simply submitting the request in the first place and can be defeated with proper egress filtering. It's impossible for the attacker to simply replace the hash in the cookie with one generated from her own IP address and User-Agent header without the secret key used to generate the MAC. To the extent that the key can be secured, casual hackers will find it difficult indeed to use stolen session IDs for nefarious purposes.

Deploying SecureSessionModule

Deploying SecureSessionModule is as simple as copying SecureSessionModule.dll into the application root's bin subdirectory and registering it in Web.config, like so:

<configuration> <appSettings> <add key="SessionValidationKey" value="DAD4D476F80E0148BCD134D7AA5C61D7" /> </appSettings> <system.web> <httpModules> <add name="SecureSession" type="SecureSessionModule, SecureSessionModule" /> </httpModules> </system.web> </configuration>

The SessionValidationKey value in the <appSettings> section of Web.config is required. This is the "secret key" used to generate the MAC; SecureSessionModule looks for it at load time and throws an exception if it's not there. The key should be unique for every application, and it should be long and random. I used a simple tool built around the .NET Framework RNGCryptoServiceProvider class to generate the one in Figure 3. You should use a similar tool to maximize randomness. In addition, if deployed on a Web farm, SessionValidationKey should be the same on every server. Of course, storing plaintext security keys in configuration files poses risks of its own. For an added measure of security, consider encrypting the secret key. The article at Building Secure ASP.NET Applications: Authentication, Authorization, and Secure Communication describes how to use the Windows® Data Protection API (DPAPI) from ASP.NET. DPAPI is ideal for encrypting secrets in configuration files because it offloads the problem of key management—specifically, storing decryption keys—to the operating system itself.

Ideally, SecureSessionModule would use the same secret key that ASP.NET uses for hashing: the value of the validationKey attribute in Machine.config's <machineKey> element. However, there is no public API for retrieving this key, and after inspecting what the Framework does to retrieve validationKey, I elected not to duplicate the logic in the Framework to avoid potential incompatibilities with future versions of .NET. The new configuration API coming in version 2.0 of the .NET Framework will correct this omission and provide a documented means for reading the ASP.NET validation key.

Once deployed, SecureSessionModule works passively—no intervention is required. If you'd like, include an Application_Error method in Global.asax to log InvalidSessionExceptions in the Windows event log or elsewhere. That way you can check the log every morning over your first cup of coffee and find out if someone has been trying to spoof your server with stolen session IDs. Such a method might look like this:

<%@ Import Namespace="System.Diagnostics" %> <script language="C#" runat="server"> void Application_Error(Object sender, EventArgs e) { // Write an entry to the event log EventLog log = new EventLog (); log.Source = "My ASP.NET Application"; log.WriteEntry (Server.GetLastError().ToString(), EventLogEntryType.Error); } </script>

Caveats

Before deploying SecureSessionModule on a production Web server, you should consider several potential issues.

First, SecureSessionModule is not 100 percent effective in detecting illicit session ID cookies. If the attacker has the same network address as the victim (if both, for example, use the same proxy server) or can spoof the victim's network address, then User-Agent headers are the last line of defense. And User-Agent headers are easily spoofed by someone aware that User-Agent headers are being used to validate session IDs.

Second, if for some reason a user's network address or User-Agent headers vary from request to request, that user will lose access to her session state. Third, SecureSessionModule doesn't work with cookieless session state. It assumes session IDs are passed in cookies, not URLs.

Finally, SecureSessionModule hasn't been tested in a production environment. If you use it, I'd love to hear from you—especially about any glitches that arise. Note that Microsoft has considered building similar protections into ASP.NET, but has always shied away from it for backward compatibility concerns.

Conclusion

Session hijacking remains a serious threat to security. SecureSessionModule raises the bar for hackers who hijack sessions using stolen session IDs by factoring evidence about the session owner into the session ID cookie. That evidence isn't conclusive because neither network addresses nor User-Agent headers can be used reliably to distinguish one user from another, but it nonetheless places an additional hurdle in the path of hackers who are actively seeking to compromise your Web servers by connecting to other users' sessions and grabbing their data.

Send your questions and comments to Jeff at  wicked@microsoft.com.

Jeff Prosise is a contributing editor to MSDN Magazine and the author of several books, including Programming Microsoft .NET (Microsoft Press, 2002). He's also a cofounder of Wintellect, a software consulting and education firm that specializes in Microsoft .NET. Contact Jeff at wicked@microsoft.com.