Code download available at:
WickedCode0408.exe
(118 KB)
Browse the Code Online
Contents
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
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.