It’s not that surprising, there’s so many things that can go wrong. It seems that authentication and session management is so difficult to get right that even the big players occasionally get in trouble. I’ve blogged earlier about a Google 2-step verification vulnerability I discovered back when they were rolling out the system (yes, I admit it took more patience than effort to find that one), and if you do a Google search for "authentication flaw" you’ll get plenty of hits for many high profile sites. This indicates that we need to tighten up our authentication and session management. In this post we’ll focus on some issues related to session management, and at the end I have an announcement to make!
OWASP has a great guide on what you should test for in your session management. If you’re familiar with the Microsoft SDL you’ve probably noticed that it also has a set of recommendations for session management. We’ll dig into some of the details of ASP.NET session management to see how it fares against some of these requirements.
First things first, we’ll need to set the scene with an overview of how ASP.NET handles identities and sessions and then we’ll return to the requirements.
Identity vs. session stateIt is common to let Forms Authentication or Windows Identity Foundation (WIF) keep track of users when they’re logged in to an ASP.NET applications. By default, both Forms Authentication and WIF store the user’s identity information in a cookie. The information is encrypted and protected with a Message Authentication Code (MAC). Encryption ensures confidentiality, while the MAC makes the cookie value tamper-proof. These cookies are usually referred to as "authentication cookies", so we’ll stick with that term in this post. The FormsAuthenticationModule manages the Forms Authentication cookies, in the default configuration you’ll easily spot these as they’re named ".ASPXAUTH". If you’re running WIF the SessionAuthenticationModule handles the cookies, naming them "FEDAUTH" by default. Both modules will set the Principal and User objects on the HttpContext for each request, based on the content of the authentication cookies.
The SessionStateModule on the other hand manages the ASP.NET session state, and it does so without regard to the identity of the current user. Consequently, there’s no connection between the user’s identity and the ASP.NET session. Session IDs are by default managed by the built-in SessionIDManager. It takes care of various things, but most importantly (for this post) the creation and validation of session identifiers.
How session IDs are handled in ASP.NETASP.NET has two ways of transmitting session IDs back and forth to the browser, either embedded in the url or through a session cookie. You can easily spot the session ID when it’s embedded in the url, it’s enclosed in S(xxx). Here’s an example:
http://www.nwebsec.com/(S(r4rgpwcvsv3kpsvadoca4gnq))/Be warned however, you should never run an ASP.NET application with session IDs in URL, Troy Hunt explains why in his OWASP Top 10 for .NET developers part 3: Broken authentication and session management.
With "session IDs in the URL" out of the way, we’ll (mostly) focus on session IDs in cookies for the remainder of this post. With the default session state configuration the session ID it set in a cookie.
Set-Cookie: ASP.NET_SessionId=jvlp2yfgkjbgynioovodcneu; path=/; HttpOnlyASP.NET is quite liberal in its session handling as long as it receives a valid session ID, i.e. a 24-character string consisting of characters a-z and 0-5. If the client does not provide a session ID or provides an invalid session ID, ASP.NET will issue a new one. If the client supplies a valid session ID and there’s no session associated with that ID on the server, ASP.NET will accept the ID and create a new session object for it. Consequently, you will also keep the same session ID until the browser deletes it. This is well-documented behaviour: How and why session IDs are reused in ASP.NET. While reading it, keep in mind though that it’s a rather old article (applies to Microsoft .NET Framework 1.1, though revised in 2006). As a side note, please don’t follow the advice in that article on issuing a Forms Authentication cookie. With that approach, you’d give users access to a valid authentication cookie for the user "test" every time they log in. The example also sets an empty session cookie, we’ll get to the problems related to that later on.
So far we’ve established that the user’s identity and the user’s session are two separate things, and that ASP.NET will accept any session ID from the browser as long as it’s structurally valid. Keep this in mind when reading on, now it’s time to look at those security requirements.
Security requirementsIf you look at the OWASP testing guide for session management and in the SDL’s "Phase Two: Design", you’ll see that there is a variety of requirements. Several of these are discussed in Troy Hunt’s OWASP article so we won’t discuss those here. We’ll zoom in on the requirements that target how session IDs are handled, here they are:
- Strong log-out and session management. Proper session handling is one of the most important parts of web application security. At the most fundamental level, sessions must be initiated, managed, and terminated in a secure manner. If a product employs an authenticated session, it must begin as an encrypted authentication event to avoid session fixation. (SDL 5.2, p.27)
- Session IDs are vulnerable to session fixation attacks. (OWASP)
- Session IDs aren’t rotated after successful login. (OWASP)
I’ve grouped the requirements together since they overlap. You’ll note that session fixation is a concern and it’s also recommended to change the session identifier when the user authenticates. I’m not quite sure about the first SDL requirement, as logging in over https does not necessarily prevent session fixation. Nevertheless, combined with the second requirement you are protected against session fixation attacks.
If you recall how session IDs are handled in ASP.NET — any valid session ID will be accepted and they won’t change after the user logs in — you’ll immediately see that we’re in trouble on the second requirement. We’re also in trouble with regard to the first requirement, we’ll discuss that next.
Session fixation attacksMitja Kolšek’s seminal paper Session Fixation Vulnerability in Web-based Applications from 2002 explains session fixation attacks in detail. You should have a look, it’s a good read. I’ll give a tl;dr version here.
Session fixation is a rather sneaky attack, as it lets an attacker share a session with a victim. A figure speaks thousand words, so here it is:
In a session fixation attack an attacker will attempt to set a victims’s session ID, in most cases before the user logs in. When the user logs in, that shared session will be initialized with the user’s data. Since the attacker is using the same session, she can go to a web page that displays data from the session, and she’ll see the victim’s data. Note that the attacker and the victim have different identities (authentication cookies), but they’re sharing the ASP.NET session.
If you’re running with cookieless ASP.NET sessions (ID in URL) you are vulnerable to this attack unless you have put special checks in place to tie the session to the current user. So in case you didn’t read Troy Hunt’s OWASP article: DO NOT use cookieless ASP.NET sessions.
We’ll now outline how the attacker can launch a successful attack when you’re using session cookies. Keep in mind that if the attacker pulls this off she can set a long-lived cookie for the victim by giving it an expiry date far into the future. That could lead to a long-term compromise of the victim’s session, i.e. until the user clears his cookies or stops using that browser.
I’m not including any scripts here, but you will find one in the demo I’ve prepared. We’ll get to the demos later.
Cross-subdomain attacksIt’s quite common to run different (but related) sites under different subdomains, e.g. importantapp.nwebsec.com and anotherapp.nwebsec.com. This gives you the benefit of the same-origin policy, the fundamental browser security barrier between different sites on the Internet, providing a degree of isolation between the sites. Unfortunately, for cookies there’s a feature (not a bug) that relaxes the same-origin policy for subdomains. You can set a "domain cookie" — a cookie that the browser will send across all subdomains. Any site on a subdomain can do this by setting the domain attribute on a cookie. The following cookie would e.g. be included in all requests to nwebsec.com and all its subdomains:
ASP.NET_SessionId=3da5vd3wjjfww1sj5qwqsfnh; domain=.nwebsec.com; path=/Here’s a figure showing how this can facilitate a session fixation attack on other sites on other subdomains. Note that we’re not specific about how the attacker sets the cookie. The attacker could have complete control of the site, or leverage an XSS vulnerability on the site.
You probably see the problem here, if an attacker can leverage any of the subdomains to set a cookie for the victim that would open the path to a session fixation attack on one or more (possibly all) of the sites running on the other subdomains. For a detailed account of cookies and the same-origin policy, refer to the Same-origin policy for cookies in the Browser Security Handbook. It’s a bit dated, but it’s still an excellent resource on browser security.
I’d like to highlight that this applies to all cookies, including the authentication cookies. However, ASP.NET gives you a strong security boundary for the authentication cookies if you configure different machine keys for each of the applications on different subdomains. Authentication cookies from one application would not be valid for another, which solves the cross-domain issue at the application level. It’s quite easy to forget that the same does not apply to the session cookies.
Middleperson attacksNow for the final example of how you can do a session fixation attack. The attacker can also launch a session fixation attack if the attacker can intercept the victim’s traffic. We’ll look at the attack in its simplest form. All it takes is a single insecure request — to any website!
Later on, the user decides to visit secure.nwebsec.com and takes care to enter https://secure.nwebsec.com in his browser to ensure that the connection is protected by TLS. The browser sets up a secure connection, but sends the cookie previously set by the attacker. Success!
There’s one countermeasure I’ll mention here that could help secure the user’s communication with the website, the HTTP Strict Transport Security header. With that header enabled, depending on the user’s browser, it would have made a secure connection instead of an insecure one to secure.nwebsec.com and the attacker would not be able to set the cookie. It was worth a mention, you can learn more about the header in an earlier blog post of mine on Security through HTTP response headers.
Demonstrating session fixation attacksI’ve expanded the NWebsec demos a bit, so now there’s two sites! You can try out these session fixation attacks at unsecured.nwebsec.com. Use a proxy such as Fiddler when you play around with the attacks and you’ll see exactly what’s going on. You can try the injection attacks using scripts and meta tags, and also the cross-subdomain attack. So go check those out when you’re finished reading this post! Let me know if you run into trouble with any of the demos.
Common counter-measures in ASP.NETA common advice to prevent session fixation is to attempt to expire the ASP.NET session cookie or set it to an invalid value when the user logs in, so ASP.NET issues a new one on the next request. In the normal scenario, this works just fine — but unfortunately it’s not particularly effective during an attack. The issue lies with how browsers handle multiple cookies that have the same name, but different settings for the domain and path attribute. You’ll find a new cookie test on nwebsec.com where you can test your browser’s behaviour. The test lets you set a "host" cookie (without the domain attribute set) and a domain cookie. The site reports back to you in which order the browser sends the cookies. Here’s what I saw during my tests.
- Opera 12 / Safari (iOS 6): The host cookie is always first.
- Chrome 28 / Opera 15: The least recently updated cookie is first (when a cookie is initially set or updated it moves to the back of the line).
- FF 22: The first cookie that is initially set comes first. The cookie order doesn’t change when existing cookies are updated.
- IE 10: The domain cookie is always first.
I find the results quite interesting, when testing six prominent browsers we’ve come out with four different ways of handling cookies. As you probably see, the results are bad news for the session fixation countermeasure. When ASP.NET receives multiple cookies with the same name, it will retrieve the session ID from the first cookie in the list. With that in mind, let’s look at how the countermeasure of invalidating the host cookie holds up in the different cases. We assume that the attacker has successfully set a domain cookie for the user before the user logs in.
First, a general observation. If you expire the host cookie you will lose in all scenarios. The browser will send the domain cookie in the next request, and ASP.NET will accept that session identifier. However, when you’re setting an invalid value for the session cookie the outcome is browser dependent.
For users with Opera 12 / Safari (iOS6) you’d be fine when setting an invalid value for the host cookie, it would take precedence over the domain cookie and ASP.NET would issue a new cookie on the following request. That cookie would also take precedence over the domain cookie, so you’re good.
For users with Chrome and Opera 15, you’d lose. When you set/update the host cookie, it will move to the back of the line and the domain cookie wins. In fact, if the user already had a host cookie before the attacker set a domain cookie, you’re moving that cookie from the front of the line to the back. The countermeasure backfires and does more harm than good in this case.
For Firefox users it’s a battle over who was able to set their cookie as new first. If you expire the host cookie, you lose since the domain cookie then takes precedence. If there were a host cookie present before the attacker set the domain cookie, you’d be fine as the cookie is updated but would still be first in line.
For IE users you always lose. The domain cookie takes precedence in all scenarios.
I think it’s fair to conclude that this is not a robust defence, so we’ll look at another alternative. You could take the approach of writing the username to the session when the user logs in, and then check on all subsequent request that the username stored in the session still matches that from the authentication cookies. You can make this work; I’ve solved the session fixation problem twice before using this approach. You’ll need to take special care to keep the session in a secure state and on how you handle potential errors, so it can get a bit messy. I believe that a general solution to the problem would be beneficial, so we don’t all have to keep solving the same problem again and again. That brings me to my announcement!
Announcing NWebwsec.SessionSecurityI’ve just released the new NWebsec session security library. It includes an AuthenticatedSessionIDManager, which provides a new way of handling session identifiers in ASP.NET. It generates authenticated session identifiers that are cryptographically bound to the logged on user. This ensures a strong connection between the session identifier and the user, preventing users from sharing a session identifier.
For anonymous users there is no identity to associate the session with so they’ll get the traditional ASP.NET session ID behaviour, but once they authenticate they’ll get authenticated session identifiers.
The authenticated session IDs have two parts, 128 random bits and a MAC calculated over those bits and the username. When the server receives a session ID, the AuthenticatedSessionIDManager validates the MAC to ensure that the session ID belongs to the current user. To calculate the MAC we need a secret key. By default, the machineKey validation key is used, but you also have to the option to specify a separate key. I won’t dig into all the details here as I’ve documented how it works in the project documentation.
I mentioned earlier that for authentication cookies there is a strong security boundary between applications when you configure the applications with different machine keys. This compensates for the shortcomings of the same-origin policy for cookies. We also established that ASP.NET was lacking that boundary for session cookies. The AuthenticatedSessionIDManager puts that boundary in place since the session IDs are calculated based on the machine key. A session ID generated under one machine key will not validate under another. Consequently, session IDs are bound directly to the application that generated them, and indirectly to the authentication cookies (as they contain the username). That brings the security models for authentication and session cookies nicely in sync.
Though session fixation attacks have been the main topic for this post, there are some added benefits from the improved security model for session identifiers. Whether you’re facing an attack or not, you must take care to coordinate the ASP.NET session with the logged on user. Since identities and sessions by design are unrelated, you could have situations where one user gets another user’s session ID, e.g. when two users are using the same computer and one user logs into the application shortly after the other user has logged into the same application. The AuthenticatedSessionIDManager avoids those scenarios by automatically issuing a new session identifier if there’s a change of user.
Finally, if we return to our carefully selected security requirements, you’ll recall that session fixation should be avoided and session IDs should be rotated on login. With authenticated session IDs we meet these requirements. Users can no longer share session identifiers, so session fixation is now avoided by design.
On the requirement for rotating session IDs, the AuthenticatedSessionIDManager will meet that requirement for apps that serve both anonymous and authenticated users. When the users first visits the application it will issue a classic ASP.NET session ID. When the user logs in, the session id will not validate and an authenticated session ID will be issued. The same happens when the user logs out, the user will be issued a classic ASP.NET session ID again. For apps that are "authentication only", which is typical when you’re using WIF and the authentication procedure happens elsewhere, session IDs might not be rotated. If the user has a valid authenticated session ID, it will be reused. I assume the intent with this requirement is to avoid mixing unauthenticated sessions with authenticated ones, in that case, we are in line with the requirement as the application only has authenticated sessions.
You can easily get started with the AuthenticatedSessionIDManager. The NuGet installation will add most of the required configuration, you’ll need to add one line of config to enable it and you’ll be on your way!
To learn more about the NWebsec.SessionSecurity package see the docs at the project site and go find it on Nuget. And remember, feedback is always welcome!