Best Practices

Fast, Scalable, and Secure Session State Management for Your Web Applications

Michael Volodarsky

Parts of this article are based on a prerelease version of ASP.NET 2.0. Those sections are subject to change.

This article discusses:

  • ASP.NET 2.0 session state architecture
  • Improving state management performance and scalability
  • Addressing session state issues in Web farms
  • Securing your state management infrastructure
This article uses the following technologies:
ASP.NET

Contents

How Session State Works
Improving Performance
Disabling Session State
Reducing Network/Storage Overhead
Optimizing Serialization
In-Process Optimizations
Improving Scalability
ASP.NET 2.0 Session State Partitioning
Securing Session State
Configuring for Security
Conclusion

Due to the stateless nature of the HTTP protocol, Web applications have always shouldered the burden of user state management. Fortunately, ASP.NET provides a number of ways to maintain user state, the most powerful of which is session state. This feature provides a convenient programmatic interface for associating arbitrary application state with a user session, and takes care of back-end state storage and client session management for the application. This article takes an in-depth look at designing and deploying high-performance, scalable, secure session solutions, and presents best practices for both existing and new ASP.NET session state features straight from the ASP.NET feature team.

How Session State Works

ASP.NET session state lets you associate a server-side string or object dictionary containing state data with a particular HTTP client session. A session is defined as a series of requests issued by the same client within a certain period of time, and is managed by associating a session ID with each unique client. The ID is supplied by the client on each request, either in a cookie or as a special fragment of the request URL. The session data is stored on the server side in one of the supported session state stores, which include in-process memory, SQL Server™ database, and the ASP.NET State Server service. The latter two modes enable session state to be shared among multiple Web servers on a Web farm and do not require server affinity (that is, they don't require the session to be tied to one specific Web server).

Session state runtime operation is implemented by the SessionStateModule class which plugs into the request processing pipeline in the application as an IHttpModule. SessionStateModule executes once before handler execution in the AcquireRequestState pipeline stage, and once after handler execution in the ReleaseRequestState pipeline stage (see Figure 1). In the AcquireRequestState stage, SessionStateModule attempts to extract the session ID from the request and retrieve the session data for that session ID from the session state store provider. If the session ID is present and the state is retrieved successfully, the module builds the session state dictionary that can be used by the handler to inspect and change the session state.

Figure 1 Maintaining Session State

Figure 1** Maintaining Session State **

In ASP.NET 2.0, session extraction is encapsulated in the session ID manager component, which can be replaced by a custom implementation in order to support custom session ID management schemes (for example, storing the session ID in a query string or in form fields). The default session ID manager supports both cookie-based and URL-based session IDs. The default ASP.NET 2.0 session ID manager also provides support for automatically detecting which session ID mode should be used per request based on device profile or runtime negotiation with the client.

If no session ID is present in the request, and the handler makes some changes to the session, a new session ID will be created in the ReleaseRequestState stage, and the new ID will be issued to the client by the session ID manager implementation. In this case, and also if changes were made to an existing session, SessionStateModule will use the state store provider to persist the changes made to session data for that session.

In ASP.NET 2.0, the session state store functionality is encapsulated in the session store provider component, which can be one of the built-in InProc, SQLServer, or StateServer providers, or a custom provider that implements the fetching of session data, the creation of sessions, and the saving of changes to existing sessions.

Improving Performance

Using session state in an ASP.NET application can add noticeable overhead to the application performance. This should be expected as you are executing more code during the processing of every request in the application and possibly making network requests to retrieve stored state. However, there are several techniques your application can take advantage of to reduce the performance impact of session state use while still maintaining the desired state management functionality.

The bulk of the session state processing that occurs on every request resides in SessionStateModule. This module executes once before the ASP.NET request handler to fetch the state for the session identified by the request information, and once after the request handler to create a new session or to save the changes made to the existing session during handler execution.

The InProc session state mode is the fastest of the built-in state storage modes. Its overhead is limited to extracting the session ID from the request, performing a cache lookup for the state dictionary stored in the application's memory space, and marking the session as accessed to prevent its expiration. Updates to the data contained in the session are made directly to objects in memory and do not require any additional work to be persisted for the next request. However, because the InProc mode lacks the application restart tolerance and does not work in Web farm scenarios, the application frequently has no choice but to use one of the other two out-of-process modes.

In the default out-of-process modes, SQLServer and StateServer, session state must be fetched from an external store and deserialized from its binary blob representation to the in-memory state dictionary form in the AcquireRequestState stage. In the ReleaseRequestState stage, the state dictionary needs to be serialized again and transferred to external storage. In addition, the session entry in the store must be updated to indicate the last access time to prevent its expiration. In these modes, serialization and deserialization of the state data, and its out-of-process transfer, are by far the most expensive operations introduced by session state to the request path.

SessionStateModule will perform a number of optimizations by default to avoid overhead wherever possible. These optimizations fall into four categories.

First, for handlers or pages that are not marked as requiring session state, no session state work will be performed except for marking the session as accessed in the store. For pages marked as requiring read-only session state access, only the last-accessed marking and initial data fetching will be done.

Second, until any data is actually saved into the session dictionary, no session will be started for requests that do not already specify a session ID.

Third, on requests where no session state accesses are made or where only read accesses are made to session variables that contain immutable primitive types, no state changes will be persisted to the provider at the end of the request. On requests where mutable session data was accessed or where the session was modified, only the accessed variables will be serialized and the rest will just be copied from their binary blob representations.

And fourth, primitive types are serialized directly, but object types are serialized using the relatively slower BinaryFormatter serialization method.

By taking advantage of these optimizations using the best practices illustrated in this article, you can reduce the performance impact of session state management. The strategies discussed here focus on the three most promising approaches to improving application performance when using session state, taking advantage of the session state performance optimizations:

  • Disabling session state when possible in order to avoid the overhead entirely.
  • Reducing the overhead of the serialization and deserialization of state data.
  • Reducing the overhead of out-of-process transfer of session state to and from the state store.

Disabling Session State

Not all pages in your application will need access to session state. For those pages that do not, you can indicate that session state is not needed and prevent session data from being fetched from the store in requests to these pages.

For pages that do not update session state, you can indicate that read-only access is required. This does not prevent the state data from being fetched, but it results in a read lock being taken on the database, which enables multiple read-only requests to access the session simultaneously and prevents lock contention when multiple requests are made to the server with the same session ID. More importantly, it always prevents the session state module from connecting to the store at the end of the request to update the state (there are other ways to avoid this also, which I'll describe later).

Here's how you disable session state:

<%@ Page EnableSessionState="False" %>

Similarly, this is how you indicate read-only session state:

<%@ Page EnableSessionState="ReadOnly" %>

In fact, most typical pages such as shopping carts will only update the state if the user performs a postback action, like adding or removing an item from the cart, but not when she simply views the shopping cart. You can tweak application performance in this case by separating the viewing of the cart into its own page with read-only session state that does not perform an update. Updating the shopping cart can be separated into another page to which the viewing page posts when an action is requested (cross posting is supported in ASP.NET 2.0).

You can also make the default behavior of the application be read-only or you can turn off session state by default by using the <pages> configuration element in your Web.config file. Then you can explicitly enable session state or enable write access in only the pages that need it by setting the page EnableSessionState attribute appropriately, as shown here:

<!--No session state by default--> <configuration> <system.web> <pages enableSessionState="false" /> </system.web> </configuration> <!--Read-only session state by default--> <configuration> <system.web> <pages enableSessionState="ReadOnly" /> </system.web> </configuration>

Similarly, when you are building custom handlers for processing requests in your application, you can disable session state by default by not implementing the marker interface System.Web.State.IRequiresSessionState in your handler class. You can enable read-only mode by implementing the System.Web.State.IRequiresReadOnlySessionState marker interface instead of System.Web.State.IRequiresSessionState.

Note that if you turn off session state in your application, the HttpContext.Session property will throw an exception if the page or handler tries to access it. If you mark the page as read-only, updates to the session in the out-of-process modes will not be preserved across requests. In InProc mode, however, updates will be preserved because they are made to the live objects that stay memory-resident across requests.

Unfortunately, even when you turn off session state for a particular page or handler, SessionStateModule will still mark the session as accessed in the store, so an out-of-process connection to the store will still be made when not using InProc mode. This is normally desired to prevent session expiration as long as the user is actively making requests to the application, regardless of whether the requests are to resources that require session state. But you may want to disable this behavior for some requests for performance reasons, especially if you are using ASP.NET to serve images and page resources that are contained within a parent ASP.NET page that already has marked the session as active.

To optimize this behavior, you can take advantage of the ASP.NET 2.0 custom session ID generation features to hide the session ID for a request, thereby preventing any session state work for that request. You can do this by implementing a custom type that derives from System.Web.SessionState.SessionIDManager, and then implementing the GetSessionID method to return a null session ID for requests that do not require session state (see Figure 2). For all other requests, you can delegate to the default GetSessionID implementation of the SessionIDManager class, which provides the default cookie and cookieless session ID support in ASP.NET.

Figure 2 Custom Session ID Manager

public class SessionDisablementIDManager : SessionIDManager { // override the ID management behavior of the session ID manager public override String GetSessionID(HttpContext context) { // do not return the session ID if no session state is // desired for this request if (ShouldSkipSessionState(context)) return null; // otherwise just delegate to the built-in session // ID manager to get the session ID from the request else return base.GetSessionID(context); } protected virtual bool ShouldSkipSessionState(HttpContext context) { // determine if session state should be skipped for this request } }

There are some interesting ways to implement ShouldSkipSessionState that avoid tracking session state. For example, you could skip session state when the request is for one of the extensions specified in your custom configuration section. You could also ignore session state when the current request handler instance (available as HttpContext.Current.CurrentHandler) implements your own ISkipSessionState interface. You can make pages implement this interface by specifying your own page base class.

Another scenario is when the current request handler instance does not implement IRequiresSessionState and there was another recent request with this session ID on which the session was marked as accessed. (You can keep track of this by writing your own custom cookie, or by storing session ID/timestamp pairs in the ASP.NET cache.) For example, if the last request that marked the session as active was less then 30 seconds ago, do not mark it as active now. This achieves the objective and also does not seriously reduce the session activity window.

You can deploy the custom session ID manager code in the App_Code application directory, or you can compile it into an assembly and deploy it to the \Bin application directory or install it into the Global Assembly Cache (GAC). Then, to enable it, register it with session state configuration as follows:

<configuration> <system.web> <sessionState sessionIDManagerType="IndustryStrengthSessionState. SessionDisablementIDManager"/> </system.web> </configuration>

Note that if you employ this technique when using cookieless session IDs, some ASP.NET functionality that generates links or performs redirects that preserve the cookieless session ID will not work for the requests where you return a null ID.

Reducing Network/Storage Overhead

To reduce the network round-trip overhead, and also lessen the serialization and deserialization impact, you should avoid storing large amounts of data in your session. Design your application to store only a small set of information that doesn't require manipulation in the session, and build the rest of your session object model around that small set of information on every request. Unless recomposing this information is very expensive, you can achieve significantly smaller and simpler sessions that serialize, deserialize, and transfer faster.

To reduce the lock contention that occurs when multiple requests for the same session are made to the server, avoid using frames on your site, and avoid embedding downloadable resources such as images or stylesheets that are served by session-consuming handlers in your ASP.NET application. In both of these cases, the browser will make multiple simultaneous requests to the server with the same session ID, resulting in the session being accessed multiple times and time being wasted waiting for the session lock to be released by other requests. To avoid lock contention, you can also use the read-only session state technique described earlier to prompt the session state module to utilize the reader lock, which allows multiple readers at the same time.

Optimizing Serialization

Session state uses a custom serialization mechanism to convert the session dictionary and its contents to a binary blob before storing the data in an out-of-process store. The serialization mechanism has direct support for .NET Framework primitive types, including String, Boolean, DateTime, TimeSpan, Int16, Int32, Int64, Byte, Char, Single, Double, Decimal, SByte, UInt16, UInt32, UInt64, Guid, and IntPtr. These types are written directly to the blob, while object types are serialized with BinaryFormatter, which is slower. Deserialization follows the same rules. By optimizing the session contents, you can significantly reduce the overhead of serialization and deserialization of the state data.

When designing your session object model, avoid storing object types in the session. Instead, only store primitive types in the session dictionary and rebuild your business layer session objects on every request based on the session data. This avoids the overhead of using BinaryFormatter.

If you are storing a lot of different data items in the session, it's better to flatten your data into many separate session entries instead of grouping them into a single item by placing them in a class or single buffer. This enables more granular access to the needed items, and enables the application to take advantage of on-demand deserialization of needed data for session accesses. It also minimizes the number of data items that need to be serialized again if some are changed during request execution.

If you are using classes instead of primitive types, you can take control over the serialization performed by BinaryFormatter by implementing the System.Runtime.Serialization.ISerializable interface. This allows you to optimize the serialization process by only writing out required data when the object is serialized, and reconstructing the rest of the data when the object is deserialized. Alternatively, if you are simply marking your class with SerializableAttribute, you can use the NonSerializedAttribute to mark the object data members that you do not want to be serialized by the default serialization mechanism. (For more information about .NET Framework binary serialization, see Binary Serialization).

In your code, take advantage of the ASP.NET 2.0 partial deserialization mechanism by accessing only the session items that are needed. The rest of the items in the session will not incur the deserialization overhead, and will simply be copied into the outgoing session blob during the state store update instead of being serialized again. You can do even better by making sure that all of your read accesses on write-enabled session state pages are made to primitive immutable types, which, combined with lack of write accesses of the session state collection, will entirely avoid the round-trip to the server to update session data.

Now, let's see how you can apply these techniques in the real world. Figure 3 shows the initial business logic layer implementation of a session-based shopping cart (simple methods and property assessors have been abbreviated to save space).

Figure 3 A Session-Based Shopping Cart

[Serializable] public class ShoppingCartItem { private int ID; private float price; private String name; private String description; public ShoppingCartItem(int ID, float price, String name, String description) {} public int ID { get; } public float Price { get; } public String Name { get; } public String Description { get; } public static ShoppingCartItem GetItem(int ID) { // implements database / cache lookup of product information // and returns an item instance. } } [Serializable] public class ShoppingCartOrder { private ShoppingCartItem item; private int quantity; public ShoppingCartOrder(ShoppingCartItem item, int quantity) {} public ShoppingCartItem Item { get; } public int Quantity { get; } } [Serializable] public class ShoppingCart { public const String ShoppingCartKey = "__ShoppingCart"; private ArrayList orders; public ShoppingCart() { orders = new ArrayList(); } public ArrayList Orders { get; } public static void SaveShoppingCartToSession(ShoppingCart cart) { if (HttpContext.Current != null) { HttpContext.Current.Session[ShoppingCartKey] = cart; } } public static ShoppingCart GetShoppingCartFromSession() { if (HttpContext.Current != null) { Object shoppingCart = HttpContext.Current.Session[ShoppingCartKey]; if (shoppingCart is ShoppingCart) { return (ShoppingCart)shoppingCart; } } return null; } } <%@ Page Language="c#" %> <%@ Import Namespace="IndustryStrengthSessionState" %> <script runat="server"> void Page_load(Object source, EventArgs e) { // get the cart from session ShoppingCart cart = ShoppingCart.GetShoppingCartFromSession(); ... // use the cart // update the cart ShoppingCart.SaveShoppingCartToSession(); } </script>

This implementation simply defines the shopping cart object model, including a product item, an order containing a product item and quantity, and a shopping cart containing a collection of items. The classes are marked as serializable so that you can store the cart in the session simply by storing a reference to it, and having session state serialize and deserialize the object graph at run time.

Figure 4 is the optimized implementation that applies the techniques discussed earlier—converting the object model into primitive types, not storing any data that can be obtained separately from the session on each request, flattening a collection using primitive types, and optimizing item read/write access to avoid reserialization and updating whenever possible (methods unchanged from previous sample are abbreviated).

Figure 4 Optimized Shopping Cart

[Serializable] public class ShoppingCart { public const String ShoppingCartKey = "__ShoppingCart"; public const String ShoppingCartItemCountKey = "__ItemCount"; public const String ShoppingCartItemIDKeyBase = "__ItemID_"; public const String ShoppingCartOrderQuantityKeyBase = "__OrderQuantity_"; private ArrayList orders; private bool isChanged; public ShoppingCart() { orders = new ArrayList(); } public ShoppingCart(int size) { orders = new ArrayList(size); for (int i = 0; i < size; i++) { orders.Add(null); } } public void AddOrder(ShoppingCartOrder order) { orders.Add(order); isChanged = true; } public ShoppingCartOrder GetOrder(int i) { Object order = orders[i]; if (order == null) { orders[i] = GetOrderFromSession(i); } return (ShoppingCartOrder)orders[i]; } public void SetOrder(int i, ShoppingCartOrder order) { orders[i] = order; isChanged = true; } public int Count { get { return orders.Count; } } // Get the order from the session on demand private static ShoppingCartOrder GetOrderFromSession(int i) { HttpSessionState session = HttpContext.Current.Session; int ID = 0; int quantity = 0; // note: For simplicity session key strings are dynamically // created——for performance reasons they should be precreated. ID = (int)session[ShoppingCartItemIDKeyBase + i]; quantity = (int)session[ShoppingCartOrderQuantityKeyBase + i]; ShoppingCartItem item = ShoppingCartItem.GetItem(ID); return new ShoppingCartOrder(item, quantity); } // Store the order into the session private static void SaveOrderToSession(int i, ShoppingCartOrder order) { HttpSessionState session = HttpContext.Current.Session; // note: For simplicity session key strings are dynamically // created——for performance reasons they should be precreated. session[ShoppingCartItemIDKeyBase + i] = order.Item.ID; session[ShoppingCartOrderQuantityKeyBase + i] = order.Quantity; } public static void SaveShoppingCartToSession(ShoppingCart cart) { if (!cart.isChanged) return; HttpSessionState session = null; int oldCount = -1; int newCount = cart.orders.Count; if (HttpContext.Current != null) { session = HttpContext.Current.Session; object temp = session[ShoppingCartItemCountKey]; if (temp is int) oldCount = (int)temp; for(int i = 0; i < newCount; i++) { // only set the items that changed or were added if (cart.orders[i] != null) { SaveOrderToSession(i, (ShoppingCartOrder)cart.orders[i]); } } // if the number of items in the cart changed, need to set // it in the session if (oldCount != newCount) { session[ShoppingCartItemCountKey] = newCount; } } } public static ShoppingCart GetShoppingCartFromSession() { HttpSessionState session = null; if (HttpContext.Current != null) { session = HttpContext.Current.Session; Object count = null; count = session[ShoppingCartItemCountKey]; if (count is int) return new ShoppingCart((int)count); } return null; } }

This implementation decouples the item information, which can be generated at run time, from the ID and quantity information, which need to be stored in the session. It also provides a mechanism to flatten the order collection into the session. This, in turn, enables reading and writing shopping cart orders independently of each other, taking advantage of the partial deserialization and serialization mechanisms. Finally, the implementation is careful to avoid the update round-trip when no changes to the cart are made, by avoiding any set calls and using only immutable primitive types for read accesses.

This implementation does not change any part of the object model, only how the cart is stored and retrieved within the session using the SaveShoppingCartToSession and GetShoppingCartFromSession methods. This isolates the required changes from most of the application. This example code does not implement the delete or get collection functionality for clarity reasons, but it is also possible to implement either or both without losing the benefits of optimization.

In-Process Optimizations

Although InProc mode is significantly faster than the out-of-process modes, there are a few guidelines to keep in mind to keep in-process performance optimal.

First, you should avoid storing single-threaded apartment (STA) COM objects in the session. This guidance has been valid since the ASP days. Since STA COM objects can only be accessed by the same thread, ASP.NET will serialize access to the COM object by ensuring that all requests for the session are processed by the same thread. Note that this will only happen when the AspCompat page attribute is set to true. Second, you should attempt to reduce session size altogether to avoid increased memory pressure that results in cache churn and negatively impacts the performance and capacity of the entire application.

Improving Scalability

ASP.NET session state enables horizontal scaling for ASP.NET applications by supporting out-of-process state storage, which allows multiple Web farm machines to process requests for the same session without losing the session data. However, while the capacity of the Web farm can be almost linearly increased by adding more Web server nodes, the single session state store becomes a bottleneck as session capacity increases. For InProc mode, which can be used outside of Web-farmed applications or in Web farms with proper affinity, the memory usage of sessions in Web server memory can also become prohibitive.

ASP.NET 2.0 Session State Partitioning

ASP.NET 2.0 provides a solution to the problems encountered when scaling up by enabling horizontal scale-out of session state stores through its state partitioning feature. State partitioning enables the session data and the associated processing load to be divided between multiple out-of-process state stores, allowing the session state load to scale as the Web farm grows and the number of concurrent sessions increases. It works by supplying a custom partitioning algorithm to SessionStateModule, which uses the algorithm to determine the state store connection string to be used for the current request based on the session ID. Both the SQLServer and the StateServer providers will then use the appropriate connection string to fetch and save the session.

You can implement a partitioning scheme by deriving a class from the System.Web.IPartitionResolver interface, and building the session ID-to-connection string mapping inside the ResolvePartition method. The basic implementation shown in Figure 5 creates an array of hardcoded connection strings corresponding to available state store partitions in the Initialize method. In the ResolvePartition method, the resolver hashes the session ID string into one of the buckets corresponding to one of the loaded connection strings, and selects the resulting connection string.

Figure 5 Session Partitioning

public class PartitionResolver : System.Web.IPartitionResolver { private String[] partitions; public void Initialize() { // create the partition connection string table partitions = new String[] { "tcpip=192.168.1.1:42424", "tcpip=192.168.1.2:42424", "tcpip=192.168.1.3:42424" }; } public String ResolvePartition(Object key) { String sid = (String)key; // hash the incoming session ID into // one of the available partitions int partitionID = Math.Abs(sid.GetHashCode()) % partitions.Length; return partitions[partitionID]; } }

Ideally, you will want to implement either a configuration collection for specifying the available partitions that you will load in the Initialize method, or obtain the collection from a centralized location over the network on a Web farm. The simple uniform hashing implementation results in a relatively even distribution of sessions to stores over time because the session IDs are generated randomly. However, you may want to implement a load-balancing scheme where you dynamically determine the partition in which to place a given session based on current partition load. To do this, you will need to encode the partition ID into the session ID by using a custom SessionIDManager derivation together with the PartitionResolver to determine the partition for a new session, create the session ID with the partition ID encoded, and then determine the partition ID in future requests by pulling it out from the session ID in the partition resolver.

The partition resolver implementation can be deployed in the App_Code application directory, or it can be compiled into an assembly and deployed in the \Bin application directory or installed into the GAC. Finally, the resolver type has to be added to the session state configuration by specifying its fully qualified name in the partitionResolverType attribute:

<configuration> <system.web> <sessionState mode="StateServer" partitionResolverType= "IndustryStrengthSessionState.PartitionResolver" /> </system.web> </configuration>

Note that the partition resolver can only be used when session state is using the SQLServer or StateServer modes, and no connection string can be specified using the sqlConnectionString or stateConnectionString attributes.

Session state also provides an alternative approach for Web farm session state management, which allows the application to harness the speed of distributed InProc state storage (or out-of-process state storage for reliability purposes) provided that a session ID-encompassing affinity scheme can be used on the Web farm. The affinity scheme needs to ensure that all requests with a given session ID are passed to the same Web server, in which case each Web server can maintain its own session state store without sharing it with other Web servers.

The affinity scheme needs to be based on session IDs or other characteristics of the request that guarantee all requests containing a given session ID will be directed to the same Web server. Such schemes can be based on client IP network ranges (keeping in mind that clients may be coming from dynamically assigned Web proxies) or user agent headers. The problems with implementing such affinity schemes include the fact that they are not readily available on hardware load-balancing systems as they require HTTP-level routing of the requests (as opposed to the more common IP or TCP-level connection routing). In addition, these schemes prevent you from doing any real load balancing because routing needs to be deterministic with respect to session ID to state store mappings to preserve state.

Securing Session State

ASP.NET session state provides an important security advantage over client state management techniques in that the actual state is stored on the server side and not exposed on the client and other network entities along the HTTP request path. However, there are several important aspects of session state operation that need to be considered in order to maintain application security. Security best practices fall into three major categories: preventing session ID spoofing and injection, securing the state storage in the back-end, and ensuring session state deployment security in dedicated or shared environments.

Because session state uses client tickets to identify the server-side session, it may be susceptible to session ID spoofing and injection attacks. A session ID spoofing attack refers to a malicious entity providing the session ID of another user in its own request, thereby tricking the application into loading the session of that user. An injection attack is practically the opposite of a spoofing attack. In an injection attack, a malicious entity forces a legitimate user to make requests to the server with the attacker's session ID. Because the session ID is a random base64-encoded string of 62 characters, brute-force session ID searches are impractical and not considered a real security threat.

ASP.NET 2.0 session state has been hardened to help guard against session ID spoofing and injection. It takes advantage of the HTTP-only cookie feature (currently supported by Internet Explorer 6.0 with Microsoft® Internet Explorer 6.0 SP1 or Windows® XP SP2 installed), which prevents the session ID cookies from being available to client-side script and therefore to potential cross-site scripting exploits designed to steal session ID cookies. This feature is enabled automatically for supported browsers.

In addition, ASP.NET 2.0 also provides a session ID regeneration feature, which forces expired session IDs that are not found on the server to be replaced with a newly generated ID. This avoids the accidental reuse of session IDs that commonly occurs with cookieless session links (such as those indexed by search engine crawlers), as well as malicious session ID injection attacks through cookieless session link posting. This feature is on by default and is only supported for cookieless session IDs. You should keep this feature enabled if you are using cookieless session IDs in your application. Note that this feature does not prevent session spoofing if the attacker can discover the session ID while the original session is still active.

In previous versions, ASP.NET session state required the application to be configured to use either cookie-based session IDs or cookieless link-based session IDs. For applications that needed to support mobile clients or browsers that do not have cookies enabled, the only option was to use the cookieless mode for the entire application. This is discouraged from the security perspective because cookieless IDs lend themselves better to discovery and spoofing, and to injection by link posting or phishing attacks, but is necessary to support non-cookie browsers.

ASP.NET 2.0 enables two new session ID modes, UseDeviceProfile and AutoDetect, which can use the browser device profile or perform an autodetect of the cookie capability respectively to determine which type of session ID to use on each request. If you need to support non-cookie clients, use the UseDeviceProfile mode, which reduces the cookieless surface area by only using cookieless IDs for clients that require them.

An advanced technique that further minimizes the risk of spoofing attacks is to combine the random session ID with a hash of the client's user agent and the network part of the client IP address, and validate the ID to make sure these pieces of information match. Unfortunately this is not a perfect practice, since many clients may come from a single proxy server and may share the same user agent string, but it does provide an additional level of protection against spoofing attacks. You can use the custom SessionIDManager extensibility mechanism in ASP.NET 2.0 to implement this functionality, as described previously. You can find an example of implementing a similar technique in ASP.NET 1.x in Jeff Prosise's column in the August 2004 issue of MSDN®Magazine.

Reducing the session expiration timeout to the lowest usable value is a good security practice and can help avoid most of the session ID exploits. To provide an even higher level of protection, your app should make it easy for its users to log out and abandon the session before leaving the site, and use JavaScript to detect the browser window closing to force a session abandonment on the server.

Finally, secure sockets layer (SSL) should be used to prevent network-level sniffing of session IDs, authentication tickets, application cookies, and other request/response information. When using SSL, keep in mind that if you have any non-HTTPS URLs within the URL scope specified in the cookies (the entire domain by default), the cookies can be sniffed when they are sent by the browser during requests to those URLs. Because of this, it is recommended that you use SSL to protect your entire domain rather than only a subset of the URLs.

Configuring for Security

When using the SQLServer state provider, you typically deploy the session state schema to the SQL Server database by using the aspnet_regsql.exe utility included inside the framework directory for the appropriate version. This utility creates the needed session state tables, but does not grant any users access to the database objects. You need to explicitly grant a particular Windows or SQL Server account access to the database and required objects, depending on whether you want to use integrated Windows authentication or SQL Server authentication to connect to the server.

I recommend creating a SQL Server database role (let's call it "SessionStateFullAccess"), and grant the role permissions only to execute the session state stored procedures and not give any access to the tables themselves. Grant this role to the login account you will use for the Web server's session state configuration. This is a general least-privilege best practice for all ASP.NET data-driven features, many of which already come with these roles configured.

If you are using Windows integrated authentication in your SQL Server connection string, take advantage of the ASP.NET 2.0 configuration option to connect to the server under the hosting identity rather than request identity like in previous versions. This option is on by default and is configured by using the useHostingIdentity attribute in the <sessionState> configuration element. This simplifies the authentication management on an intranet, as you only grant database access to the ASP.NET worker process or application identity instead of granting access to the entire domain or a set of users.

If you are using SQL Server authentication in your connection string, you can place the connection string in the <connectionStrings> configuration section and just place the connection string name in the <sessionState> configuration. You can then use ASP.NET 2.0 configuration encryption to protect the login credentials by encrypting the <connectionStrings> section.

Also, make sure to protect the SQL Server instance from remote access by any machines other than the Web server. You can achieve this by using Windows Firewall or IPSec policies.

The State Server service does not provide any authentication capabilities, so anyone with network access to it can change the session data and cause undesired behavior. To help secure the State Server instance, you must make sure that it is protected from unauthorized access by machines other than your Web server using Windows Firewall or IPSec policies. You should also change the network port on which the server runs by setting the following registry key to a new port:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state \Parameters\Port=PORT

Since the time of the .NET Framework 1.1 (along with .NET Framework 1.0 SP3 and .NET Framework 2.0), the state server will only listen for connections by default on the local loopback interface. You can configure the State Server to listen for remote connections from your Web server by setting the following registry key on the machine running the State Server instance:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state \Parameters\AllowRemoteConnection=1

Conclusion

ASP.NET session state can provide a compelling mechanism for state management in single application and Web farm environments. ASP.NET 2.0 provides a number of new session state capabilities that enable applications to better leverage the power of session state in order to build rich, stateful experiences traditionally available only to client applications. By applying the design and deployment best practices described in this article, your Web applications can maintain session state with better performance, scalability, and security.

Michael Volodarsky is a technical Program Manager on the Web Platform and Tools Team at Microsoft. He owns the core server infrastructure for ASP.NET and IIS. He is now focusing on improving the Web application platform in the next generation Web server, IIS 7.0.